Коли слід вибирати успадкування над інтерфейсом при розробці бібліотек класу C #? [зачинено]


78

У мене є кілька Processorкласів, які виконуватимуть дві дуже різні речі, але їх викликають із загального коду (ситуація "інверсія контролю").

Мені цікаво, з яких міркувань щодо дизайну я маю бути підозрілим (або пізнаючим для вас, користувачі США), приймаючи рішення про те, чи всі вони повинні успадкувати BaseProcessorабо реалізувати IProcessorяк інтерфейс.


4
Також див. Більш загальне запитання, не характерне для C #: Інтерфейс проти базового класу
Коді Грей

Відповіді:


187

Як правило, правило звучить приблизно так:

  • Спадкування описаний аналіз є різновидом відносин.
  • Реалізація інтерфейсу описує взаємозв'язок, що може зробити .

Щоб дещо конкретніше сказати, давайте розглянемо приклад. System.Drawing.BitmapКлас є-ан зображень (і як такий, він успадковує від Imageкласу), але вона також може робити- утилізації, тому він реалізує IDisposableінтерфейс. Він також може виконувати серіалізацію, тому реалізується з ISerializableінтерфейсу.

Але більш практично, інтерфейси часто використовуються для імітації множинного успадкування в C #. Якщо ваш Processorклас повинен успадкувати щось на зразок System.ComponentModel.Component, то вам залишається лише вибір, як реалізувати IProcessorінтерфейс.

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

Щоб отримати відповіді на подібні запитання, я часто звертаюся до Керівних принципів проектування .NET Framework , де сказано про вибір між класами та інтерфейсами:

Загалом класи є найкращою конструкцією для викриття абстракцій.

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

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

[. . . ]

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

Їх загальні рекомендації такі:

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

Кріс Андерсон висловлює особливу згоду з цим останнім принципом, аргументуючи, що:

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


1
спасибі - чудова відповідь ... Так само. лише з одним застереженням: якщо ви збираєтеся опублікувати базовий клас як "загальнодоступний тип", то порекомендуйте КОРИСТУВАЧАМ цього класу успадкувати від нього ТАМ ВЛАСНИМ базовим класом ... дати їм десь розмістити ТАМ загальне поведінка (в майбутньому) .... І спасибі за посилання. У мене є хороше читання ;-)
corlettk

17
Слід зазначити, що інтерфейс є скоріше "обов'язковим завданням".
awiebe

Ці відповіді стають більш актуальними у світлі нещодавніх змін, здійснених у Java 8 щодо запровадження методів за замовчуванням в інтерфейсах. В іншому випадку не було іншого способу ввести новий контракт / метод, не порушивши кожен фрагмент коду, що реалізує інтерфейс. (Хоча межа між абстрактним класом та інтерфейсом із цією зміною стає більш розмитою)
Харшдіп

1
Хіба ця порада не є майже протилежною спрямованості "Шаблонів дизайну" Перш за все ". Я не фахівець ні в якому разі, але їх мантра воліла композицію перед спадщиною. Хтось може це розплутати?
alimack 12.03.15

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

10

Річард,

Чому ВИБІР між ними? Я мав би інтерфейс IP-процесора як опублікований тип (для використання в іншому місці в системі); і якщо трапляється так, що ваші різні СУЧАСНІ реалізації IProcessor мають спільну поведінку, то абстрактний клас BaseProcessor буде справді хорошим місцем для реалізації цієї загальної поведінки.

Таким чином, якщо вам потрібен IProcessor в майбутньому, який НЕ був для послуг BaseProcessor, він НЕ МОЖЕ його мати (і, можливо, приховати) ... але ті, хто хоче, можуть його мати ... у дубльованому коді / поняттях.

Просто моя скромна ДУМКА.

Ура. Кіт.


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

1
Хм, я з цим не погоджуюсь. Як правило, ви не повинні виставляти інтерфейси без конкретних реалізацій цього інтерфейсу. І ще однією проблемою викриття інтерфейсу є встановлення версій (докладніше див. Мою відповідь). Я не думаю, що вам потрібно робити те й інше. Це не завжди найкраща відповідь на дизайнерські рішення. Спроба зробити і те, і інше коштує підвищеної складності, і я намагаюся зрозуміти, як це насправді вигідно тут. Ви нічого не втрачаєте, просто використовуючи абстрактний базовий клас, а не інтерфейс.
Коді Грей

5

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

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

Коли використовувати інтерфейси?

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

Коли використовувати базові класи (конкретні та / або абстрактні)

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


Приклади:

Поговоримо про таблиці.

  • Ми приймаємо таблиці, що підлягають переробці => Це повинно бути визначено за допомогою інтерфейсу типу "IRecyclableTable", що гарантує, що всі таблиці, що підлягають переробці, матимуть метод "Recycle" .

  • Ми хочемо таблиці робочого столу => Це повинно бути визначено з успадкуванням. "Настільний стіл" - це "стіл". Усі таблиці мають загальні властивості та поведінку, а настільні матимуть однакові, додаючи такі речі, які змушують таблицю робочого столу відрізнятися від інших типів таблиць.

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


2
Я насправді не впевнений, що таке "таблиця, що підлягає переробці", але, мабуть, я б реалізував IRecyclableінтерфейс, оскільки здається ймовірним, що кілька непов'язаних об'єктів повинні бути перероблені (тобто інші класи, які не представляють таблиці). І тоді я все одно мав би реалізовувати всі таблиці з Tableкласу, включаючи DesktopTableі PicnicTableта FoldingTable.
Коді Грей

І, якщо ви перевірте мій текст, чи виявите, що я кажу щось інше, ніж ви? :) Прочитайте, як починається справа про "таблиці, що переробляються": "Приклади". Це лише приклад, абсолютно ізольований від будь-якого графіку об’єкта, дизайну чи фактичного випадку.
Matías Fidemraizer

У будь-якому разі, дякую за будь-яку пропозицію, не помиляйтеся з моїми словами, ей! :)
Matías Fidemraizer

Деякі кажуть, що "Інтерфейси не є контрактом" : blogs.msdn.microsoft.com/kcwalina/2004/10/24/…
MikeSchinkel

2

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


@jitendra - дякую, чи не могли б Ви надати детальну інформацію про те, чому це кращий варіант?
ймовірно, на пляжі

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

Якщо функцій лише пара, успадкування має більше сенсу, оскільки вам не потрібно реалізовувати всі функції, доступні в базовому класі. І зовсім необов’язково, щоб у вас було достатньо інформації про повний перелік функцій та параметрів базового класу. Отже, у такому випадку успадкування має більше сенсу.
jitendragarg

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

@cody grey Якщо я не помиляюся, ви повинні реалізувати весь абстрактний клас, що погана ідея у разі повторного використання. Знову ж таки, це лише моя точка зору. Може помилятися. Будь ласка, відредагуйте коментар, якщо помилився.
jitendragarg

2

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

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

РЕДАГУВАТИ:

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


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

@Cody Gray Про останню частину, але, можливо, базовий клас реалізує інтерфейс, а реалізація інтерфейсу є віртуальною, тому це неоднозначний випадок! Жартую, неважливо.
Matías Fidemraizer

1

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

Крім того, якщо вам потрібно додати "додаткову функціональність" до вашого інтерфейсу, найкращим варіантом є створення нового інтерфейсу, дотримуючись I в SOLID, що є принципом сегрегації інтерфейсу.


0

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

Або якщо уникнення спадщини неможливо, можливо, хороша ідея використовувати їх обох разом. Створіть абстрактний базовий клас, який реалізує інтерфейс. У вашому випадку ProcessorBase реалізує IP-процесор.

Подібно до ControllerBase та IController в ASP.NET Mvc.


3
Цікаво, що ви стверджуєте, що інтерфейси є більш гнучкими, обґрунтовуючи це випадком майбутніх змін. А як щодо того, якщо в майбутньому вам потрібно було додати деякі функції до вашого інтерфейсу? Це було б надзвичайною зміною, оскільки кожен клас, який реалізував цей інтерфейс, також повинен був би реалізувати метод, який ви щойно додали. Це не так з базовим класом, оскільки базовий клас міг просто забезпечити реалізацію за замовчуванням.
Коді Грей

Кожен випадок „майбутніх змін” має різні історії. Тоді у вашому випадку найкращий спосіб - це використання абстрактного класу, але, роблячи це, я міг би також віддати перевагу взаємозв’язаним абстрактним класам (як I-, так і -базі). Крім того, з іншої точки зору, якщо ви використовуєте "клас", а не той, хто його розробляє, переважніше бачити два-три методи, а не цілу купу з них. У цьому випадку інтерфейс кращий для розділення проблем. Ви можете використовувати один інтерфейс, щоб зробити щось інше, щоб зробити інше. І всі ці інтерфейси насправді можуть бути єдиним класом.
Hendry Ten,

Про додавання функціональних можливостей до інтерфейсу: ти зазвичай не додаєш нових функцій до інтерфейсу (не стосується типу контрактів або сховища), ти можеш створити новий (інтерфейс на c # можна отримати з іншого інтерфейсу), за умови розділення питань реалізовано добре.
Hendry Ten,

1
Мені не подобаються такі аргументи. Дизайн не слід робити з причин рефакторингу. Хороший дизайн легко реконструювати, і справа не в успадкуванні використання інтерфейсу. Зміни програмного забезпечення, звичайно, але це можна зробити, звертаючи увагу на те, які вимоги будуть вирішувати надурочні роботи, тому, якщо я не помиляюся, навіть з успадкуванням, хороше дерево успадкування додасть, змінить та / або видалить логіку, але його значення повинно залишатися "як є".
Matías Fidemraizer

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