Інтерфейс та спадщина: найкраще з обох світів?


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, оскільки він відповідає сухій?


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він також розширює його. Отже, якщо я вас правильно зрозумів, я думаю, що це робить цей випадок 3, але без використання гіпотетичного C ++! Це СУХО, і він має сильну інкапсуляцію.


3
І ось такий шаблон дизайну знають як "шаблонний метод" ( en.wikipedia.org/wiki/Template_method_pattern ).
Joris Timmermans

Так, це відповідає справці 3. Це виглядає перспективно. Докладно вивчимо. Також це якась система подій. Чи є назва цього "шаблону"?
MustafaM

@MadKeithV Ви впевнені, що це "шаблонний метод"?
MustafaM

@illmath - так, це невіртуальний публічний метод, який делегує частини деталей його реалізації віртуальним захищеним / приватним методам.
Жоріс Тіммерманс

@illmath Я раніше не думав про це як шаблонний метод, але я вважаю, що це основний приклад. Щойно я знайшов цю статтю, яку ви, можливо, захочете прочитати там, де автор вважає, що вона заслуговує власного імені: Ідіома
невіртуального

1

Проблема інтерфейсу полягає в тому, що він не може мати реалізацію за замовчуванням, що є болем за життєві властивості та перемагає DRY.

На мою власну думку, інтерфейси повинні мати лише чисті методи - без реалізації за замовчуванням. Це жодним чином не порушує принцип DRY, оскільки інтерфейси показують, як отримати доступ до якоїсь сутності. Щойно для довідок, я дивлюсь на тут пояснення DRY :
"Кожен предмет повинен мати єдине, однозначне, авторитетне представлення в системі".

З іншого боку, SOLID повідомляє вам, що кожен клас повинен мати інтерфейс.

Чи справді випадок 3 кращий за випадок 1? Це тому, що це "краща інкапсуляція" або "більш вільно пов'язана", чи якась інша причина?

Ні, випадок 3 не перевершує випадок 1. Ви повинні вирішити свою думку. Якщо ви хочете мати типову реалізацію, зробіть це. Якщо ви хочете чистий метод, тоді перейдіть з ним.

Що робити, якщо через 3 місяці Джо Кодер створить «Докторського працівника», але він забуває викликати increment_money_earned () у do_work ()?

Тоді Джо Кодер повинен отримати те, що він заслуговує за ігнорування провальних одиничних тестів. Він тестував цей клас, чи не так? :)

Який випадок найкращий для програмного проекту, який може мати 40 000 рядків коду?

Один розмір підходить не всім. Неможливо сказати, який із них кращий. Є деякі випадки, коли одне підходило б краще, ніж інше.

Можливо, вам слід навчитися деяким моделям дизайну, а не намагатися вигадувати деякі власні.


Я щойно зрозумів, що ви шукаєте невіртуальний шаблон дизайну інтерфейсу , адже саме так виглядає ваш клас 3 класу.


Дякуємо за коментар Я оновив Case 3, щоб зробити свій намір більш зрозумілим.
MustafaM

1
Мені доведеться -1 ти тут. Немає жодної причини говорити про те, що всі інтерфейси повинні бути чистими або що всі класи повинні успадковувати інтерфейс.
DeadMG

@DeadMG ISP
BЈoviћ

@VJovic: Існує велика різниця між SOLID та "Все має успадковуватись від інтерфейсу".
DeadMG

"Один розмір не відповідає всім" та "вивчити деякі шаблони дизайну" є правильними - решта вашої відповіді порушує вашу власну думку про те, що один розмір підходить не всім.
Joris Timmermans

0

Інтерфейси можуть мати реалізовані за замовчуванням C ++. Ніщо не говорить про те, що реалізація функції за замовчуванням не залежить лише від інших віртуальних членів (та аргументів), тому не збільшує будь-якого типу з’єднання.

Для другого випадку тут витісняється DRY. Інкапсуляція існує для захисту вашої програми від змін, від різних реалізацій, але в цьому випадку у вас немає різних реалізацій. Отже, інкапсуляція YAGNI.

Насправді інтерфейси часу роботи, як правило, вважаються неповноцінними еквівалентам часу компіляції. У випадку компіляції час ви можете мати і випадок 1, і випадок 2 в одному пакеті, не кажучи вже про численні інші переваги. Або навіть під час виконання, ви можете просто зробити Employee : public IEmployeeдля ефективної тієї ж переваги. Є численні способи впоратися з такими речами.

Case 3: (best of both worlds?)

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

Я перестав читати. ЯГНІ. C ++ - це те, що є C ++, а комітет зі стандартів ніколи і ніколи не збирається впроваджувати таку зміну з відмінних причин.


Ви кажете "у вас немає різних реалізацій". Але я роблю. У мене медсестра реалізує Співробітника, і пізніше я можу мати інші реалізації (Лікар, Двірник тощо). Я оновив Case 3, щоб зрозуміти, що я мав на увазі.
MustafaM

@illmath: Але у вас немає інших реалізацій get_name. Усі запропоновані вами реалізації будуть мати однакову реалізацію get_name. Крім того, як я вже сказав, вибору немає підстав, ви можете мати і те, і інше. Також Справа 3 є абсолютно нікчемною. Ви можете перекрити нечисті віртуальні, тому забудьте про дизайн, де не можете.
DeadMG

Не тільки інтерфейси можуть мати типові реалізації в C ++, вони можуть мати типові реалізації та ще бути абстрактними! тобто віртуальний пустовий IMethod () = 0 {std :: cout << "Ni!" << std :: endl; }
Йоріс Тіммерманс

@MadKeithV: Я не вірю, що ти можеш визначити їх вбудованим, але справа все одно.
DeadMG

@MadKeith: Начебто Visual Studio колись був особливо точним представленням Standard C ++.
DeadMG

0

Чи справді випадок 3 кращий за випадок 1? Це тому, що це "краща інкапсуляція" або "більш вільно пов'язана", чи якась інша причина?

З того, що я бачу у вашій реалізації, ваша реалізація Case 3 вимагає абстрактного класу, який може реалізувати чисті віртуальні методи, які згодом можуть бути змінені у похідному класі. Випадок 3 буде кращим, оскільки похідний клас може змінити реалізацію do_work як і коли потрібно, і всі похідні екземпляри в основному належатимуть до базового абстрактного типу.

Який випадок найкращий для програмного проекту, який може мати 40 000 рядків коду.

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

Редагувати за запитанням

Що робити, якщо через 3 місяці Джо Кодер створить «Докторського працівника», але він забуває викликати increment_money_earned () у do_work ()?

Одиничні тести можуть бути виконані, щоб перевірити, чи підтверджує кожен клас очікувану поведінку. Тож якщо застосовуються належні тестові одиниці, помилки можна запобігти, коли Джо Кодер реалізує новий клас.


0

Використання інтерфейсів розбиває DRY лише тоді, коли кожна реалізація є дублікатом кожної іншої. Ви можете вирішити цю дилему, застосувавши як інтерфейс, так і успадкування, але є деякі випадки, коли ви хочете реалізувати один і той же інтерфейс у ряді класів, але змінювати поведінку в кожному з класів, і це все одно буде дотримуватися принципу СУХОГО. Незалежно від того, чи вирішите ви скористатися будь-яким із описаних 3 підходів, зводиться до вибору, який потрібно зробити, щоб застосувати найкращу техніку для відповідності даній ситуації. З іншого боку, ви, ймовірно, виявите, що з часом ви більше використовуєте інтерфейси та застосовуєте успадкування лише там, де ви хочете видалити повторення. Це не означає, що це єдине причина спадкування, але в тому, що краще звести до мінімуму використання успадкування, щоб ви могли відкрити свої параметри відкритими, якщо виявите, що ваш дизайн потребує змін згодом, і якщо ви хочете мінімізувати вплив на класи нащадків від наслідків, що змінюються введе в батьківський клас.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.