Я «открыл» интерфейсы и начал любить их. Прелесть интерфейса в том, что это контракт, и любой объект, который выполняет этот контракт, может использоваться везде, где требуется этот интерфейс.
Проблема с интерфейсом состоит в том, что у него не может быть реализации по умолчанию, что является проблемой для мирских свойств и побеждает 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?
источник
Ответы:
Один из способов решить проблему «забыть о вызове суперкласса» - вернуть управление суперклассу! Я повторно показал ваш первый пример, чтобы показать как (и сделал его компиляцией;)). О, я также предполагаю, что
do_work()
вEmployee
должен был бытьvirtual
в вашем первом примере.Теперь
do_work()
нельзя изменить. Если вы хотите расширить его, вы должны сделать это с помощьюon_do_work()
которогоdo_work()
контролирует.Это, конечно, можно использовать и с интерфейсом из вашего второго примера, если
Employee
он расширяет его. Так что, если я вас правильно понимаю, я думаю, что это делает Case 3, но без использования гипотетического C ++! Это СУХОЙ и имеет сильную инкапсуляцию.источник
По моему мнению, интерфейсы должны иметь только чистые методы - без реализации по умолчанию. Это никак не нарушает принцип СУХОГО, потому что интерфейсы показывают, как получить доступ к некоторой сущности. Просто для справки, я смотрю на СУХОЕ объяснение здесь :
«Каждая часть знаний должна иметь одно, однозначное, авторитетное представление в системе».
С другой стороны, SOLID говорит вам, что у каждого класса должен быть интерфейс.
Нет, случай 3 не превосходит случай 1. Вы должны принять решение. Если вы хотите иметь реализацию по умолчанию, сделайте это. Если вы хотите чистый метод, тогда используйте его.
Тогда Джо Кодер должен получить то, что он заслуживает за игнорирование провальных модульных тестов. Он тестировал этот класс, не так ли? :)
Один размер не подходит для всех. Невозможно сказать, какой из них лучше. Есть несколько случаев, когда один подходил бы лучше, чем другой.
Может быть, вы должны изучить некоторые шаблоны проектирования, а не пытаться изобрести некоторые свои собственные.
Я только что понял, что вы ищете не виртуальный шаблон проектирования интерфейса , потому что именно так выглядит ваш класс case 3.
источник
Интерфейсы могут иметь реализации по умолчанию в C ++. Ничто не говорит о том, что реализация функции по умолчанию не зависит исключительно от других виртуальных членов (и аргументов), поэтому не увеличивает никакой связи.
Для случая 2 DRY заменяет здесь. Инкапсуляция существует для защиты вашей программы от изменений, от разных реализаций, но в этом случае у вас нет разных реализаций. Итак, инкапсуляция ЯГНИ.
Фактически, интерфейсы времени выполнения обычно считаются ниже их эквивалентов времени компиляции. В случае времени компиляции вы можете иметь и случай 1, и случай 2 в одном пакете, не говоря уже о многочисленных других преимуществах. Или даже во время выполнения, вы можете просто
Employee : public IEmployee
эффективно использовать то же преимущество. Есть множество способов справиться с такими вещами.Я перестал читать. YAGNI. C ++ - это то, чем является C ++, и комитет по стандартам никогда не собирается внедрять такие изменения по уважительным причинам.
источник
get_name
. Все ваши предложенные реализации будут иметь одинаковую реализациюget_name
. Кроме того, как я уже сказал, нет причин выбирать, вы можете иметь оба. Кроме того, случай 3 совершенно бесполезен. Вы можете переопределить не чистые виртуалы, поэтому забудьте о дизайне, где вы не можете.Из того, что я вижу в вашей реализации, вашей реализации Case 3 требуется абстрактный класс, который может реализовывать чисто виртуальные методы, которые затем могут быть изменены в производном классе. Случай 3 был бы лучше, поскольку производный класс может изменять реализацию do_work по мере необходимости, и все производные экземпляры будут в основном принадлежать базовому абстрактному типу.
Я бы сказал, что это зависит только от вашей реализации проекта и цели, которую вы хотите достичь. Абстрактный класс и интерфейсы реализованы на основе проблемы, которая должна быть решена.
Изменить на вопрос
Модульные тесты могут быть выполнены, чтобы проверить, подтверждает ли каждый класс ожидаемое поведение. Поэтому, если применяются надлежащие модульные тесты, ошибки могут быть предотвращены, когда Джо Кодер реализует новый класс.
источник
Использование интерфейсов нарушает DRY, только если каждая реализация является дубликатом любой другой. Вы можете решить эту дилемму, применяя как интерфейс, так и наследование, однако в некоторых случаях вы можете захотеть реализовать один и тот же интерфейс для нескольких классов, но изменить поведение в каждом из классов, и это все равно будет придерживаться принципа СУХОЙ. Независимо от того, выберете ли вы какой-либо из 3 описанных вами подходов, все сводится к тому, что вам нужно сделать, чтобы применить наилучшую технику для соответствия конкретной ситуации. С другой стороны, вы, вероятно, обнаружите, что со временем вы используете больше интерфейсов и применяете наследование только там, где хотите удалить повторы. Это не значит, что это единственный причина наследования, но лучше свести к минимуму использование наследования, чтобы позволить вам оставить ваши параметры открытыми, если вы обнаружите, что ваш дизайн нуждается в дальнейшем изменении, и если вы хотите минимизировать влияние на классы-потомки от эффектов, которые изменяет введет в родительский класс.
источник