Інтерфейси на абстрактному класі


30

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

interface IFooWorker { void Work(); }

abstract class BaseWorker {
    ... base class behaviors ...
    public abstract void Work() { }
    protected string CleanData(string data) { ... }
}

class DbWorker : BaseWorker, IFooWorker {
    public void Work() {
        Repository.AddCleanData(base.CleanData(UI.GetDirtyData()));
    }
}

DbWorker - це те, що отримує інтерфейс IFooWorker, оскільки це миттєва реалізація інтерфейсу. Він повністю виконує договір. Мій колега віддає перевагу майже однакові:

interface IFooWorker { void Work(); }

abstract class BaseWorker : IFooWorker {
    ... base class behaviors ...
    public abstract void Work() { }
    protected string CleanData(string data) { ... }
}

class DbWorker : BaseWorker {
    public void Work() {
        Repository.AddCleanData(base.CleanData(UI.GetDirtyData()));
    }
}

Там, де базовий клас отримує інтерфейс, і завдяки цьому всі спадкоємці базового класу також є цим інтерфейсом. Це помиляє мене, але я не можу придумати конкретних причин, чому поза "базовий клас не може стояти самостійно як реалізація інтерфейсу".

Які плюси і мінуси його методу порівняно з моїм, і чому його слід використовувати над іншим?


Ваша пропозиція дуже нагадує спадщину алмазів , що може призвести до великої плутанини.
Спойк

@Spoike: Як це бачити?
Нінг

@ Посилання на посилання, яке я надав у коментарі, має просту діаграму класів та звичайне однолінійне пояснення того, що це таке. її називають "алмазною" проблемою, оскільки структура успадкування в основному намальована як алмазна форма (тобто ♦ ️).
Спойк

@Spoike: чому це питання спричинить спадкування алмазів?
Нінг

1
@Niing: Успадкування BaseWorker та IFooWorker одним і тим же методом називається Workпитання спадкування алмазів (у цьому випадку вони обидва виконують контракт за Workметодом). У Java вам потрібно реалізувати методи інтерфейсу, щоб Workуникнути питання про те, який метод повинна використовувати програма. Мови, такі як C ++, однак, не розрізняють це для вас.
Спойк

Відповіді:


24

Я вважаю, що клас не повинен реалізовувати інтерфейс, якщо цей клас не можна використовувати, коли потрібна реалізація інтерфейсу.

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

Крім того, з спрощеного прикладу, який ви опублікували, важко сказати, але частина проблеми може полягати в тому, що інтерфейс та абстрактний клас є зайвими. Якщо у вас є реалізація інших класів IFooWorker, які не випливають із цього BaseWorker, вам взагалі не потрібен інтерфейс. Інтерфейси - це просто абстрактні класи з деякими додатковими обмеженнями, які полегшують багатократне успадкування.

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


1
+1 за вказівку на те, що те, що BaseWorkerце не миттєво безпосередньо, не означає, що BaseWorker myWorkerце не реалізується IFooWorker.
Авнер Шахар-Каштан

2
Я не згоден, що інтерфейси - це лише абстрактні класи. Абстрактні класи зазвичай визначають деякі деталі реалізації (інакше був би використаний інтерфейс). Якщо ці деталі вам не сподобалися, тепер ви повинні змінити кожне використання базового класу на щось інше. Інтерфейси дрібні; вони визначають взаємозв'язок "підключення та розетки" між залежностями та залежними. Таким чином, щільне з'єднання з будь-якими деталями реалізації вже не викликає занепокоєння. Якщо клас, що успадковує інтерфейс, вам не сподобався, зніміть його та вставте щось інше цілком; ваш код може менше турбуватися.
KeithS

5
Вони мають обидві переваги, але, як правило, інтерфейс завжди повинен використовуватися для залежності, незалежно від того, чи є абстрактний клас, що визначає базові реалізації. Поєднання інтерфейсу та абстрактного класу дає найкраще з обох світів; інтерфейс підтримує взаємодію «підключення та розетки» дрібної поверхні, а абстрактний клас надає корисний загальний код. Ви можете закреслити і замінити будь-який рівень цього під інтерфейсом за бажанням.
KeithS

Під "вказівником" ви маєте на увазі екземпляр об'єкта (до якого вказівник посилався б)? Інтерфейси не є класами, вони є типами і можуть виступати як неповна заміна багаторазового успадкування, а не заміною - тобто вони "включають поведінку з декількох джерел у класі".
Саміс

46

Я мав би погодитися з вашим колегою.

В обох наведених прикладах BaseWorker визначає абстрактний метод Work (), що означає, що всі підкласи можуть відповідати договору IFooWorker. У цьому випадку я думаю, що BaseWorker повинен реалізувати інтерфейс, і це реалізація успадковується його підкласами. Це позбавить вас від необхідності чітко вказати, що кожен підклас справді є IFooWorker (принцип DRY).

Якщо ви не визначали Work () як метод BaseWorker або якщо у IFooWorker були інші методи, які підкласи BaseWorker не хотіли б і не потребували, то (очевидно) вам доведеться вказати, які підкласи насправді реалізують IFooWorker.


11
+1 написав би те саме. Вибачте, інста, але я думаю, що ваш колега має рацію і в цьому випадку, якщо щось гарантує виконання контракту, він повинен успадкувати цей договір, незалежно від того, чи гарантія виконується інтерфейсом, абстрактним класом або конкретним класом. Простіше кажучи: гарант повинен успадкувати договір, який він гарантує.
Джиммі Хоффа

2
Я загалом погоджуюся, але хотів би зазначити, що такі слова, як "база", "стандарт", "за замовчуванням", "загальний характер" тощо, мають кодові запахи у назві класу. Якщо абстрактний базовий клас має майже те саме ім'я, що й інтерфейс, але входить одне із цих слів, це часто є ознакою неправильної абстракції; інтерфейси визначають, що щось робить , тип успадкування визначає, що це таке . Якщо у вас є IFooі BaseFooтоді, це означає, що або IFooце не належним чином дрібнозернистий інтерфейс, або BaseFooвикористовується спадкування, коли композиція може бути кращим вибором.
Aaronaught

@Aaronaught Добре, хоча може бути, що дійсно є щось, що всі або більшість реалізацій IFoo потрібно успадковувати (наприклад, обробляти службу чи вашу DAO-реалізацію).
Меттью Флінн

2
Тоді не базовий клас можна назвати MyDaoFooабо SqlFooабо HibernateFooабо незалежно від того , щоб вказати , що це тільки один з можливих дерево класів , що реалізують IFoo? Мені ще більше пахне, якщо базовий клас приєднаний до певної бібліотеки чи платформи, і про це в імені не згадується ...
Aaronaught

@Aaronaught - Так. Я згоден повністю.
Меттью Флінн

11

Я взагалі згоден з вашим колегою.

Візьмемо вашу модель: інтерфейс реалізований лише дочірніми класами, хоча базовий клас також застосовує вплив методів IFooWorker. По-перше, це зайве; незалежно від того, чи реалізує дочірній клас інтерфейс чи ні, від них вимагається перекрити відкриті методи BaseWorker, а також будь-яка реалізація IFooWorker повинна розкрити функціональність, отримуючи вони допомогу від BaseWorker чи ні.

Це додатково робить вашу ієрархію неоднозначною; "Всі IFooWorkers є BaseWorkers" не обов'язково є правдивим твердженням, і також "Всі BaseWorkers - це IFooWorkers". Отже, якщо ви хочете визначити змінну, параметр або тип повернення екземпляра, яка могла б бути будь-якою реалізацією або IFooWorker, або BaseWorker (скориставшись загальноприйнятою функціональною здатністю, що є однією з причин успадкування в першу чергу), жодна з вони гарантовано є всеохоплюючими; деякі BaseWorkers не можуть бути віднесені до змінної типу IFooWorker, і навпаки.

Модель вашого колеги набагато простіше у використанні та заміні. "Всі BaseWorkers - це IFooWorkers" - це справжнє твердження; ви можете надати будь-який екземпляр BaseWorker будь-якій залежності IFooWorker, жодних проблем. Протилежне твердження "Всі IFooWorkers є BaseWorkers" не відповідає дійсності; що дозволяє замінити BaseWorker на BetterBaseWorker, і ваш споживчий код (який залежить від IFooWorker) не повинен говорити про різницю.


Мені подобається звук цієї відповіді, але я не дуже дотримуюся останньої частини. Ви припускаєте, що BetterBaseWorker був би незалежним абстрактним класом, який реалізує IFooWorker, так що мій гіпотетичний плагін міг просто сказати "о, хлопче! Кращий базовий працівник нарешті вийшов!" і змінити будь-які посилання BaseWorker на BetterBaseWorker і назвати його щодня (можливо, пограти з новими крутими поверненими значеннями, які дозволяють мені викреслити величезні шматки мого зайвого коду тощо)?
Ентоні

Більше чи менше, так. Велика перевага полягає в тому, що ви зменшите кількість посилань на BaseWorker, які повинні змінитись на BetterBaseWorkers; більшість вашого коду не посилається на конкретний клас безпосередньо, замість цього використовується IFooWorker, тому при зміні того, що призначено цим залежностям IFooWorker (властивості класів, параметри конструкторів або методів), код за допомогою IFooWorker не повинен бачити різниці у користуванні.
КітС

6

Мені потрібно додати щось попередження до цих відповідей. З'єднання базового класу з інтерфейсом створює силу в структурі цього класу. У вашому базовому прикладі, це не розум, що обидва повинні бути зв'язані, але це може бути неправдивим у всіх випадках.

Візьміть рамкові класи колекції Java:

abstract class AbstractList
class LinkedList extends AbstractList implements Queue

Той факт, що контракт на чергу реалізується LinkedList , не підштовхував стурбованість у AbstractList .

Чим відрізняються два випадки? Метою BaseWorker завжди було (як повідомляється його ім'я та інтерфейс) здійснювати операції в IWorker . Мета AbstractList і черга черги розходяться, але спадковий працівник все ще може реалізувати останній контракт.


Це відбувається вдвічі. Він завжди вважає за краще реалізувати інтерфейс на базовому класі, а я завжди вважаю за краще реалізовувати його на кінцевому конкретному. Представлений вами сценарій трапляється часто і є частиною причинних інтерфейсів на базі.
Брайан Боттчер

1
Правильно, інсталюю, і я розглядаю використання інтерфейсів таким чином як аспект надмірного використання спадщини, який ми спостерігаємо в багатьох середовищах розвитку. Як сказано в їхній книзі зразків GoF, віддавайте перевагу композиції над успадкуванням , а збереження інтерфейсу поза базовим класом є одним із способів просування цього принципу.
Михай Даніла

1
Я отримую попередження, але ви зауважте, що абстрактний клас ОП включав абстрактні методи, що відповідають інтерфейсу: BaseWorker явно IFooWorker. Якщо зробити це явним, цей факт стає більш корисним. У Черзі є численні методи, які не входять у AbstractList, і як такий, AbstractList не є чергою, навіть якби його діти могли бути. Анотація списку та черги є ортогональними.
Меттью Флінн

Дякую за це розуміння; Я погоджуюсь, що справа ОП трапляється до цієї категорії, але я відчув, що відбулася більша дискусія в пошуках більш глибокого правила, і я хотів поділитися своїми спостереженнями щодо тенденції, про яку я читав (і навіть мав себе на короткий час) надмірного використання спадщини, можливо, тому, що це один із інструментів у наборі інструментів OOP.
Михай Даніла

Данг, минуло шість років. :)
Mihai Danila

2

Я хотів би задати питання, що відбувається при зміні IFooWorker, наприклад, додавання нового методу?

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

З цієї причини я б змусив базовий клас реалізувати інтерфейс , оскільки я міг би реалізувати всю функціональність нового методу в базовому класі, не торкаючись похідних класів.


1

Спочатку подумайте, що таке абстрактний базовий клас та що таке інтерфейс. Подумайте, коли б ви використовували те чи інше, а коли ні.

Люди звичні думати, що обидва є дуже схожими поняттями, насправді це загальне питання інтерв'ю (різниця між ними - ??)

Таким чином, абстрактний базовий клас дає щось інтерфейс, який не може бути реалізованим за замовчуванням. (Є й інші речі. У C # ви можете мати статичні методи на абстрактному класі, а не на інтерфейсі, наприклад).

Як приклад, одне поширене використання цього - з IDisposable. Абстрактний клас реалізує метод Dispose IDisposable, що означає, що будь-яка похідна версія базового класу автоматично буде одноразовою. Потім ви можете грати з кількома варіантами. Зробіть Dispose абстрактним, змушуючи похідні класи виконувати його. Забезпечте реалізацію віртуальної програми за замовчуванням, щоб вони не робили або не робили її ні віртуальною, ні абстрактною, і, наприклад, виртуальними методами називали такі речі, як, наприклад, BeforeDispose, AfterDispose, OnDispose або Disposition.

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

Це все насправді грубе над спрощенням. Іншою альтернативою є отримання похідних класів, які навіть не реалізують інтерфейси, а надають їх за допомогою адаптерного шаблону. Приклад цього, який я нещодавно бачив, був у IObjectContextAdapter.


1

Мені ще років від того, щоб повністю зрозуміти відмінність між абстрактним класом та інтерфейсом. Кожен раз, коли я думаю, що я отримую ручку щодо основних понять, я переглядаю stackexchange і я два кроки назад. Але декілька думок на тему та питання ОП:

Спочатку:

Є два поширених пояснення інтерфейсу:

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

  2. Інтерфейси - це абстрактні класи, які нічого не можуть / не можуть зробити. Вони корисні, тому що ви можете реалізувати декілька, на відміну від середніх батьківських класів. Мовляв, я можу бути об’єктом класу BananaBread і успадковувати від BaseBread, але це не означає, що я не можу також реалізувати як IWithNuts, так і інтерфейси ITastesYummy. Я навіть міг реалізувати інтерфейс IDoesTheDishes, бо я не просто хліб, знаєте?

Існує два поширених пояснення абстрактного класу:

  1. Абстрактний клас, знаєте, річ не те, що річ може зробити. Це як, по суті, зовсім не справжня річ. Почекай, це допоможе. Човен - це абстрактний клас, але сексуальна яхта Playboy була б підкласом BaseBoat.

  2. Я читав книгу про абстрактні класи, і, можливо, ви повинні прочитати цю книгу, тому що ви, мабуть, не зрозумієте її і зробите це неправильно, якщо ви не прочитали цю книгу.

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

Друге:

Щодо того, хтось запитав простішу версію цього питання, класичну, "навіщо використовувати інтерфейси? Яка різниця? Що я пропускаю?" І в одній відповіді простий приклад використав пілот ВПС. Це не зовсім суттєво, але це викликало чудові коментарі, один з яких згадував про інтерфейс IFlyable, що має такі методи, як takeOff, pilotEject тощо. І це дійсно натиснуло на мене як на прикладі реального світу, чому інтерфейси не просто корисні, але вирішальне значення Інтерфейс робить об'єкт / клас інтуїтивно зрозумілим або, принаймні, дає сенс, який він є. Інтерфейс не на користь об'єкта чи даних, а на те, що потрібно взаємодіяти з цим об’єктом. Класичний Фрукт-> Яблуко-> Фуджі або Форма-> Трикутник-> Рівносторонні приклади успадкування є чудовою моделлю для таксономічного розуміння даного об’єкта на основі його нащадків. Він інформує споживача та процесори про його загальні якості, поведінку, чи об’єкт є групою речей, буде шланг вашої системи, якщо ви поставите її в неправильному місці, або описує сенсорні дані, роз'єм до певного сховища даних або фінансові дані, необхідні для складання фонду оплати праці.

У конкретному об'єкті Plane може бути, а може і не бути методу аварійної посадки, але я буду злий, якщо я припускаю його аварійну землю, як і будь-який інший перельотний об'єкт, тільки щоб прокинутися охопленим в DumbPlane і дізнатися, що розробники пішли з quickLand бо вони думали, що це все одно. Так само, як я був би розчарований, якби кожен виробник шнеків мав власну інтерпретацію, або якщо на моєму телевізорі не було кнопки збільшення гучності вище кнопки зменшення гучності.

Абстрактні класи - це модель, яка встановлює те, що повинен мати будь-який нащадковий об'єкт, щоб бути таким класом. Якщо у вас немає всіх якостей качки, неважливо, що у вас реалізований інтерфейс IQuack, ваш просто дивний пінгвін. Інтерфейси - це те, що має сенс навіть тоді, коли ви не можете бути впевнені ні в чому іншому. Джефф Голдблум та Starbuck обидва змогли літати на чужих космічних кораблях, оскільки інтерфейс був надійно схожим.

Третє:

Я погоджуюся з вашим колегою, адже іноді потрібно застосувати певні методи на самому початку. Якщо ви створюєте ORM активного запису, йому потрібен метод збереження. Це не до підкласу, який можна інстанціювати. І якщо інтерфейс ICRUD є достатньо портативним, щоб не бути пов'язаний виключно з одним абстрактним класом, він може бути реалізований іншими класами, щоб зробити їх надійними та інтуїтивними для всіх, хто вже знайомий з будь-яким із класів нащадків цього абстрактного класу.

З іншого боку, раніше був чудовий приклад, коли не переходити до прив’язки інтерфейсу до абстрактного класу, оскільки не всі типи списку (або повинні) реалізувати інтерфейс черги. Ви сказали, що цей сценарій трапляється вдвічі, а це означає, що ви та ваш колега помиляєтесь половину часу, і, отже, найкраще зробити це сперечатися, обговорювати, вважати, і якщо вони виявляться правильними, визнайте та приймайте зв'язок . Але не робіть розробника, який дотримується філософії, навіть коли це не найкраще для роботи.


0

Є ще один аспект, чому DbWorkerслід реалізувати IFooWorker, як ви пропонуєте.

У випадку стилю вашого колеги, якщо з якихось причин відбувається пізніше рефакторинг, і DbWorkerвважається, що не поширюється абстрактний BaseWorkerклас, він DbWorkerвтрачає IFooWorkerінтерфейс. Це може, але не обов'язково, мати вплив на споживачів, які споживають його, якщо вони очікують на IFooWorkerінтерфейс.


-1

Для початку я люблю по-різному визначати інтерфейси та абстрактні класи:

Interface: a contract that each class must fulfill if they are to implement that interface
Abstract class: a general class (i.e vehicle is an abstract class, while porche911 is not)

У вашому випадку всі працівники реалізовують, work()тому що саме цього вимагає контракт. Тому ваш базовий клас Baseworkerповинен реалізувати цей метод явно або як віртуальну функцію. Я б запропонував вам поставити цей метод у свій базовий клас через простий факт, який потребують усі працівники work(). Тому логічно, що ваш базовий клас (навіть якщо ви не можете створити покажчик цього типу) включає його як функцію.

Тепер, DbWorkerце задовольняє IFooWorkerінтерфейс, але якщо є певний спосіб, який робить цей клас work(), то йому дійсно потрібно перевантажити визначення, успадковане від BaseWorker. Причина, по якій він не повинен IFooWorkerбезпосередньо реалізовувати інтерфейс, полягає в тому, що це не щось особливе DbWorker. Якщо ви робили це кожен раз, коли ви реалізовували класи, подібні до DbWorkerтого, ви б порушили DRY .

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


-3

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


3
Це патентна неправда. Інтерфейс - це договір, згідно з яким система, як очікується, поводиться, не зважаючи на реалізацію. Спадковий базовий клас є однією з багатьох можливих реалізацій цього інтерфейсу. Досить масштабна система матиме декілька базових класів, які задовольняють різні інтерфейси (як правило, закриті загальними), саме це і зробила ця установка, і чому було корисно розділити їх.
Брайан Боттчер

Це дійсно поганий приклад з точки зору ООП. Коли ви говорите, що "базовий клас - це лише одна реалізація" (це має бути або не було б сенсу для інтерфейсу), ви говорите, що будуть непрацівникові класи, що реалізують інтерфейс IFooWorker. Тоді ви будете мати базовий клас, який є спеціалістом, котрий працює з кооперативом (ми повинні припустити, що там можуть бути і бармеровики), і у вас будуть інші працівники, які навіть не є основними працівниками! Це відстале. Більш ніж одним способом.
Мартін Мейт

1
Можливо, ви застрягли на слові «Робітник» в інтерфейсі. А як щодо "Джерело даних"? Якщо у нас є IDatasource <T>, ми можемо тривіально мати SqlDatasource <T>, RestDatasource <T>, MongoDatasource <T>. Бізнес вирішує, які закриття інтерфейсу генерики переходять до завершальної реалізації абстрактних джерел даних.
Брайан Боттчер

Це може мати сенс використовувати успадкування лише заради DRY та помістити деяку загальну реалізацію в базовий клас. Але ви б не узгоджували жодних перекритих методів з будь-якими методами інтерфейсу, базовий клас міститиме загальну логіку підтримки. Якщо вони вишикуються в ряд, це показало б, що спадкування вирішує вашу проблему просто чудово, а інтерфейс не буде служити ніякій меті. Ви використовуєте інтерфейс у випадках, коли спадкування не вирішує вашу проблему, оскільки загальні характеристики, які ви хочете моделювати, не відповідають дереву класів singke. І так, формулювання в прикладі робить це ще більше неправильним.
Мартін Мейт
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.