Интерфейс и наследование: лучшее из обоих миров?

10

Я «открыл» интерфейсы и начал любить их. Прелесть интерфейса в том, что это контракт, и любой объект, который выполняет этот контракт, может использоваться везде, где требуется этот интерфейс.

Проблема с интерфейсом состоит в том, что у него не может быть реализации по умолчанию, что является проблемой для мирских свойств и побеждает DRY. Это также хорошо, потому что это держит реализацию и систему разъединенной. Наследование, с другой стороны, поддерживает более плотную связь и может нарушить инкапсуляцию.

Случай 1 (Наследование с частными членами, хорошая инкапсуляция, тесно связанная)

class Employee
{
int money_earned;
string name;

public:
 void do_work(){money_earned++;};
 string get_name(return name;);
};


class Nurse : public Employee: 
{
   public:
   void do_work(/*do work. Oops, can't update money_earned. Unaware I have to call superclass' do_work()*/);

};

void HireNurse(Nurse *n)
{
   nurse->do_work();
)

Случай 2 (просто интерфейс)

class IEmployee
{
     virtual void do_work()=0;
     virtual string get_name()=0;
};

//class Nurse implements IEmployee.
//But now, for each employee, must repeat the get_name() implementation,
//and add a name member string, which breaks DRY.

Случай 3: (лучший из обоих миров?)

Аналогично случаю 1 . Однако представьте, что (гипотетически) C ++ не допускает переопределения методов, кроме тех, которые являются чисто виртуальными .

Итак, в случае 1 переопределение do_work () может вызвать ошибку во время компиляции. Чтобы это исправить, мы устанавливаем do_work () как чисто виртуальный и добавляем отдельный метод increment_money_earned (). Например:

class Employee
{
int money_earned;
string name;

public:
 virtual void do_work()=0;
 void increment_money_earned(money_earned++;);
 string get_name(return name;);
};


class Nurse : public Employee: 
{
   public:
   void do_work(/*do work*/ increment_money_earned(); ); .
};

Но даже у этого есть проблемы. Что если через 3 месяца Джо Кодер создаст сотрудника-доктора, но он забудет вызвать increment_money_earned () в do_work ()?


Вопрос:

  • Является ли случай 3 выше случай 1 ? Это из-за «лучшей инкапсуляции» или «более слабой связи», или по какой-то другой причине?

  • Является ли случай 3 превосходит случай 2 , поскольку он соответствует DRY?

MustafaM
источник
2
... ты изобретаешь абстрактные классы или как?
ZJR

Ответы:

10

Один из способов решить проблему «забыть о вызове суперкласса» - вернуть управление суперклассу! Я повторно показал ваш первый пример, чтобы показать как (и сделал его компиляцией;)). О, я также предполагаю, что do_work()в Employeeдолжен был быть virtualв вашем первом примере.

#include <string>

using namespace std;

class Employee
{
    int money_earned;
    string name;
    virtual void on_do_work() {}

    public:
        void do_work() { money_earned++; on_do_work(); }
        string get_name() { return name; }
};

class Nurse : public Employee
{
    void on_do_work() { /* do more work. Oh, and I don't have to call do_work()! */ }
};

void HireNurse(Nurse* nurse)
{
    nurse->do_work();
}

Теперь do_work()нельзя изменить. Если вы хотите расширить его, вы должны сделать это с помощью on_do_work()которого do_work()контролирует.

Это, конечно, можно использовать и с интерфейсом из вашего второго примера, если Employeeон расширяет его. Так что, если я вас правильно понимаю, я думаю, что это делает Case 3, но без использования гипотетического C ++! Это СУХОЙ и имеет сильную инкапсуляцию.

Гьян ака Гари Буйн
источник
3
И это шаблон проектирования, известный как «шаблонный метод» ( en.wikipedia.org/wiki/Template_method_pattern ).
Йорис Тиммерманс
Да, это Case 3-совместимый. Это выглядит многообещающе. Рассмотрим подробно. Также это какая-то система событий. Есть ли название для этого «шаблона»?
MustafaM
@MadKeithV вы уверены, что это «метод шаблона»?
MustafaM
@illmath - да, это не виртуальный публичный метод, который делегирует части своих деталей реализации виртуальным защищенным / приватным методам.
Йорис Тиммерманс
@illmath Раньше я не думал об этом как о шаблонном методе, но считаю, что это базовый пример. Я только что нашел эту статью, которую вы, возможно, захотите прочитать, где автор считает, что она заслуживает своего собственного названия: идиома не-виртуального интерфейса
Gyan aka Gary Buyn
1

Проблема с интерфейсом состоит в том, что у него не может быть реализации по умолчанию, что является проблемой для мирских свойств и побеждает DRY.

По моему мнению, интерфейсы должны иметь только чистые методы - без реализации по умолчанию. Это никак не нарушает принцип СУХОГО, потому что интерфейсы показывают, как получить доступ к некоторой сущности. Просто для справки, я смотрю на СУХОЕ объяснение здесь :
«Каждая часть знаний должна иметь одно, однозначное, авторитетное представление в системе».

С другой стороны, SOLID говорит вам, что у каждого класса должен быть интерфейс.

Случай 3 превосходит Случай 1? Это из-за «лучшей инкапсуляции» или «более слабой связи», или по какой-то другой причине?

Нет, случай 3 не превосходит случай 1. Вы должны принять решение. Если вы хотите иметь реализацию по умолчанию, сделайте это. Если вы хотите чистый метод, тогда используйте его.

Что если через 3 месяца Джо Кодер создаст сотрудника-доктора, но он забудет вызвать increment_money_earned () в do_work ()?

Тогда Джо Кодер должен получить то, что он заслуживает за игнорирование провальных модульных тестов. Он тестировал этот класс, не так ли? :)

Какой вариант лучше всего подходит для программного проекта, в котором может быть 40000 строк кода?

Один размер не подходит для всех. Невозможно сказать, какой из них лучше. Есть несколько случаев, когда один подходил бы лучше, чем другой.

Может быть, вы должны изучить некоторые шаблоны проектирования, а не пытаться изобрести некоторые свои собственные.


Я только что понял, что вы ищете не виртуальный шаблон проектирования интерфейса , потому что именно так выглядит ваш класс case 3.

BЈовић
источник
Спасибо за комментарий. Я обновил Случай 3, чтобы прояснить свои намерения.
MustafaM
1
Я должен -1 тебя здесь. Нет никаких оснований говорить, что все интерфейсы должны быть чистыми или что все классы должны наследоваться от интерфейса.
DeadMG
@DeadMG ISP
BЈовић
@VJovic: Есть большая разница между SOLID и «Все должно наследоваться от интерфейса».
DeadMG
«Один размер не подходит для всех» и «выучить некоторые шаблоны дизайна» верны - остальная часть вашего ответа нарушает ваше собственное предположение, что один размер не подходит для всех.
Йорис Тиммерманс
0

Интерфейсы могут иметь реализации по умолчанию в C ++. Ничто не говорит о том, что реализация функции по умолчанию не зависит исключительно от других виртуальных членов (и аргументов), поэтому не увеличивает никакой связи.

Для случая 2 DRY заменяет здесь. Инкапсуляция существует для защиты вашей программы от изменений, от разных реализаций, но в этом случае у вас нет разных реализаций. Итак, инкапсуляция ЯГНИ.

Фактически, интерфейсы времени выполнения обычно считаются ниже их эквивалентов времени компиляции. В случае времени компиляции вы можете иметь и случай 1, и случай 2 в одном пакете, не говоря уже о многочисленных других преимуществах. Или даже во время выполнения, вы можете просто Employee : public IEmployeeэффективно использовать то же преимущество. Есть множество способов справиться с такими вещами.

Case 3: (best of both worlds?)

Similar to Case 1. However, imagine that (hypothetically)

Я перестал читать. YAGNI. C ++ - это то, чем является C ++, и комитет по стандартам никогда не собирается внедрять такие изменения по уважительным причинам.

DeadMG
источник
Вы говорите: «У вас нет разных реализаций». Но я делаю. У меня есть медсестринская реализация Employee, и у меня могут быть другие реализации позже (Доктор, Дворник и т. Д.). Я обновил Case 3, чтобы было понятнее, что я имел в виду.
MustafaM
@illmath: Но у вас нет других реализаций get_name. Все ваши предложенные реализации будут иметь одинаковую реализацию get_name. Кроме того, как я уже сказал, нет причин выбирать, вы можете иметь оба. Кроме того, случай 3 совершенно бесполезен. Вы можете переопределить не чистые виртуалы, поэтому забудьте о дизайне, где вы не можете.
DeadMG
Интерфейсы не только могут иметь реализации по умолчанию в C ++, они могут иметь реализации по умолчанию и при этом быть абстрактными! то есть виртуальная пустота IMethod () = 0 {std :: cout << "Ni!" << std :: endl; }
Йорис Тиммерманс
@MadKeithV: я не верю, что вы можете определить их встроенными, но суть все та же.
DeadMG
@MadKeith: Как будто Visual Studio когда-либо была особенно точным представлением стандарта C ++.
DeadMG
0

Случай 3 превосходит Случай 1? Это из-за «лучшей инкапсуляции» или «более слабой связи», или по какой-то другой причине?

Из того, что я вижу в вашей реализации, вашей реализации Case 3 требуется абстрактный класс, который может реализовывать чисто виртуальные методы, которые затем могут быть изменены в производном классе. Случай 3 был бы лучше, поскольку производный класс может изменять реализацию do_work по мере необходимости, и все производные экземпляры будут в основном принадлежать базовому абстрактному типу.

Какой вариант лучше всего подходит для программного проекта, в котором может быть 40000 строк кода.

Я бы сказал, что это зависит только от вашей реализации проекта и цели, которую вы хотите достичь. Абстрактный класс и интерфейсы реализованы на основе проблемы, которая должна быть решена.

Изменить на вопрос

Что если через 3 месяца Джо Кодер создаст сотрудника-доктора, но он забудет вызвать increment_money_earned () в do_work ()?

Модульные тесты могут быть выполнены, чтобы проверить, подтверждает ли каждый класс ожидаемое поведение. Поэтому, если применяются надлежащие модульные тесты, ошибки могут быть предотвращены, когда Джо Кодер реализует новый класс.

Картик Сринивасан
источник
0

Использование интерфейсов нарушает DRY, только если каждая реализация является дубликатом любой другой. Вы можете решить эту дилемму, применяя как интерфейс, так и наследование, однако в некоторых случаях вы можете захотеть реализовать один и тот же интерфейс для нескольких классов, но изменить поведение в каждом из классов, и это все равно будет придерживаться принципа СУХОЙ. Независимо от того, выберете ли вы какой-либо из 3 описанных вами подходов, все сводится к тому, что вам нужно сделать, чтобы применить наилучшую технику для соответствия конкретной ситуации. С другой стороны, вы, вероятно, обнаружите, что со временем вы используете больше интерфейсов и применяете наследование только там, где хотите удалить повторы. Это не значит, что это единственный причина наследования, но лучше свести к минимуму использование наследования, чтобы позволить вам оставить ваши параметры открытыми, если вы обнаружите, что ваш дизайн нуждается в дальнейшем изменении, и если вы хотите минимизировать влияние на классы-потомки от эффектов, которые изменяет введет в родительский класс.

S.Robins
источник