Коли НЕ застосовувати принцип інверсії залежності?


43

На даний момент я намагаюся розібратися з твердим. Отже, Принцип інверсії залежності означає, що будь-які два класи повинні спілкуватися через інтерфейси, а не безпосередньо. Приклад: Якщо class Aє метод, який очікує на вказівник на об’єкт типу class B, тоді цей метод повинен очікувати об'єкта типу abstract base class of B. Це допомагає і для Open / Close.

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

Я скептично вважаю, що ми платимо певну ціну за наступний принцип. Скажіть, мені потрібно реалізувати функцію Z. Після аналізу, я висновок , що функція Zскладається з функціональних можливостей A, Bі C. Я створити фасад клас Z, який, через інтерфейси, використовує класи A, Bі C. Я починаю кодувати реалізацію і в якийсь момент я розумію, що завдання Zнасправді складається з функціональності A, Bі D. Тепер мені потрібно брухтувати Cінтерфейс, Cпрототип класу та написати окремий Dінтерфейс та клас. Без інтерфейсів потрібно було б замінити лише клас.

Іншими словами, щоб щось змінити, мені потрібно змінити 1. абонент 2. інтерфейс 3. декларація 4. реалізація. У python, безпосередньо пов'язаному з реалізацією, мені потрібно змінити лише реалізацію.


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

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

@rwong Інтерфейс фіксує контрактних інваріантів, лише якщо ви використовуєте мову, яка підтримує договірні інваріанти. У загальних мовах (Java, C #) інтерфейс - це просто набір підписів API. Додавання зайвих інтерфейсів лише погіршує дизайн.
Френк Хілеман,

Я б сказав, що ви зрозуміли це неправильно. DIP полягає в тому, щоб уникнути залежностей від компіляції в часі від компонента "високого рівня" до "низького рівня", щоб дозволити повторне використання компонента високого рівня в інших контекстах, де ви використовували б іншу реалізацію для низьких -рівневий компонент; це робиться шляхом створення абстрактного типу на високому рівні, який реалізується компонентами низького рівня; тому і компоненти високого та низького рівня залежать від цієї абстракції. Зрештою, компоненти високого та низького рівня спілкуються через інтерфейс, але це не суть DIP.
Rogério

Відповіді:


87

У багатьох мультфільмах чи інших засобах масової інформації сили добра і зла часто ілюструються ангелом і демоном, що сидить на плечах персонажа. У нашому сюжеті тут, замість добра і зла, ми маємо СОЛІД на одному плечі, а ЯГНІ (вам це не потрібно!), Що сидить на другому.

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

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

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


19
Моє перше вступ до SOLID було приблизно 15 років тому про нову систему, яку ми будували. Ми всі пили людину, що допомагає. Якщо хтось згадав щось, що звучало як YAGNI, ми були схожі на "Pfffft ... плебей". Я мав честь (жах?) Спостерігати за розвитком цієї системи протягом наступного десятиліття. Це стало незграбним безладом, якого ніхто не міг зрозуміти, навіть ми не засновники. Архітектори люблять SOLID. Люди, які насправді заробляють на життя люблять ЯГНИ. Це не ідеально, але YAGNI ближче до досконалого, і він повинен бути вашим дефолтом, якщо ви не знаєте, що ви робите. :-)
Calphool

11
@NWard Так, ми це зробили на проекті. Ходили з ним горіхи. Зараз наші тести неможливо прочитати чи підтримувати, частково через надмірну глузування. Крім того, через ін'єкцію залежності - це біль в тилу, щоб орієнтуватися по коду, коли ви намагаєтесь щось зрозуміти. СОЛІД - це не срібна куля. ЯГНІ - це не срібна куля. Автоматизоване тестування не є срібною кулею. Ніщо не може врятувати вас від важкої роботи, замислюючись над тим, що ви робите, і приймати рішення щодо того, чи допоможе це чи перешкоджає вашій роботі чи чужому.
jpmc26

22
Тут дуже багато настроїв проти SOLID. SOLID і YAGNI - це не два кінці спектру. Вони схожі на координати X і Y на графіку. Хороша система має дуже мало зайвого коду (ЯГНІ) І дотримується принципів ТВОРНОГО.
Стівен

31
Мех, (а) Я не згоден, що SOLID = підприємливість і (b) вся суть SOLID полягає в тому, що ми, як правило, вкрай бідні прогнози того, що буде потрібно. Я маю згоду з @Stephen тут. ЯГНІ каже, що ми не повинні намагатися передбачити майбутні вимоги, які сьогодні чітко не прописані. SOLID каже, що ми повинні очікувати, що дизайн буде розвиватися з часом і застосовувати певні прості методи для його полегшення. Вони не є взаємовиключними; обидва - це пристосування для адаптації до змін, що змінюються. Справжні проблеми трапляються, коли ви намагаєтесь спроектувати незрозумілі або дуже віддалені вимоги.
Aaronaught

8
"Ви можете легко поміняти читання з файлу на мережевий потік" - це хороший приклад, коли надто спрощений опис DI збиває людей з збитків. Люди іноді думають (фактично), що "цей метод використовуватиме" File, тому замість цього потрібно буде IFileвиконати роботу ". Тоді вони не можуть легко замінити мережевий потік, оскільки вони надто вимагали інтерфейс, а операції в IFileметоді навіть не використовуються, які не стосуються сокетів, тому сокет не може реалізувати IFile. Одна з речей, для якої DI не є срібною кулею, - це винайдення правильних абстракцій (інтерфейсів) :-)
Стів Джессоп,

11

Словами мирянина:

Застосовувати DIP - це просто і весело . Неправильне оформлення з першої спроби не є достатньою причиною взагалі відмовлятися від DIP.

  • Зазвичай IDE допомагають вам робити такий вид рефакторингу, деякі навіть дозволяють отримати інтерфейс із уже реалізованого класу
  • Практично неможливо отримати дизайн вперше
  • Нормальний робочий процес передбачає зміну та переосмислення інтерфейсів на перших етапах розробки
  • Із розвитком розвитку він дозріває, і у вас буде менше причин змінювати інтерфейси
  • На передовій стадії інтерфейси (дизайн) будуть зрілими і навряд чи будуть змінюватися
  • З цього моменту ви починаєте отримувати переваги, оскільки ваша програма відкрита для збільшення масштабів.

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

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


5
Я не знаю, якими мовами чи IDE використовуються ОП, але в VS 2013 працювати сміливо з інтерфейсами, витягувати інтерфейси та реалізовувати їх, і критично, якщо TDD використовується. Немає зайвих витрат на розробку з використанням цих принципів.
stephenbayer

Чому ця відповідь говорить про DI, якщо питання стосується DIP? DIP - це концепція з 1990-х, тоді як DI - з 2004 року. Вони дуже різні.
Rogério

1
(Мій попередній коментар був призначений для іншої відповіді; ігноруйте його.) "Програмування на інтерфейси" набагато загальніше, ніж DIP, але справа не в тому, щоб кожен клас реалізував окремий інтерфейс. І це лише спрощує "тестування / глузування", якщо інструменти для тестування / знущань зазнають серйозних обмежень.
Rogério

@ Rogério Зазвичай при використанні DI не кожен клас реалізує окремий інтерфейс. Один інтерфейс, який реалізується декількома класами, є загальним.
Tulains Córdova

@ Rogério я виправляв свою відповідь, кожного разу, коли я згадував про DI, я мав на увазі DIP.
Tulains Córdova

9

Використовуйте інверсію залежності там, де це має сенс.

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

Якщо у вас є група об'єктів, які використовуються всередині модуля, які не піддаються впливу інших модулів або повторно використовуються ніде, мабуть, не варто докладати зусиль для використання DI.

На мою думку, два місця повинні автоматично використовуватися:

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

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

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

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

* Виняток із вищезазначеного правила: поршнева пила - ідеальний інструмент для будь-якої роботи. Якщо вона не виправить вашу проблему, вона її видалить. Постійно.


3
Що робити, якщо "ваша проблема" - це отвір у стіні? Пила не зніме її; це зробило б це гірше. ;)
Мейсон Уілер

3
@MasonWheeler із потужною та веселою у використанні пилкою "дірка в стіні" може перетворитися на "дверний проріз", що є корисним активом :-)

1
Ви не можете використовувати пилку для виготовлення латки для отвору?
JeffO

Незважаючи на те, що деякі Stringтипи розширення , які не можна використовувати для користувачів, є деякі переваги , але багато випадків, коли альтернативні представлення були б корисними, якби тип мав хороший набір віртуальних операцій (наприклад, скопіюйте підрядку у вказану частину short[], повідомте, чи substring містить або може містити тільки ASCII, спробуйте скопіювати підрядку, яка, як вважається, містить лише ASCII, до вказаної частини a byte[]тощо). Це занадто погані рамки, щоб їх типи рядків не реалізовували будь-яких корисних інтерфейсів, пов’язаних з рядками.
supercat

1
Чому ця відповідь говорить про DI, якщо питання стосується DIP? DIP - це концепція з 1990-х, тоді як DI - з 2004 року. Вони дуже різні.
Rogério

5

Мені здається, що в первісному питанні відсутня частина пункту ДІП.

Я скептично вважаю, що ми платимо певну ціну за дотримання цього принципу. Скажіть, мені потрібно реалізувати функцію Z. Після аналізу я роблю висновок, що функція Z складається з функціоналу A, B і C. Я створюю клас фасадів Z, який через інтерфейси використовує класи A, B і C. Я починаю кодувати реалізація і в якийсь момент я усвідомлюю, що завдання Z насправді складається з функціоналу A, B і D. Тепер мені потрібно зірвати інтерфейс C, прототип класу C і написати окремий D інтерфейс і клас. Без інтерфейсів, тільки клас буде махнути необхідним для заміни.

Щоб по-справжньому скористатися DIP, ви спершу створили б клас Z і навели йому функціональність класів A, B і C (які ще не розроблені). Це дає вам API для класів A, B і C. Потім ви переходите до створення класів A, B і C і заповнюєте деталі. Ви фактично повинні створювати необхідні абстракції під час створення класу Z, повністю базуючись на тому, що потрібно класу Z. Ви навіть можете писати тести навколо класу Z до того, як класи A, B або C навіть будуть написані.

Пам'ятайте, що DIP говорить, що "Модулі високого рівня не повинні залежати від модулів низького рівня. Обидва повинні залежати від абстракцій".

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

Ніколи не буде класу D, тому що ви вирішили, що Z потрібні A, B і C, перш ніж вони були написані. Зміна вимог - це зовсім інша історія.


5

Коротка відповідь - "майже ніколи", але насправді є кілька місць, де DIP не має сенсу:

  1. Заводи чи будівельники, завдання яких - створювати об’єкти. Це, по суті, "листові вузли" в системі, яка повністю охоплює IoC. У якийсь момент щось має насправді створити ваші об’єкти, і нічого іншого не може залежати від цього. На багатьох мовах контейнер IoC може зробити це за вас, але іноді потрібно робити це старомодно.

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

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

  4. Будь-які об'єкти, які надаються як зовнішній API та потребують створення інших об'єктів, деталі реалізації яких ви не бажаєте відкривати публічно. Це підпадає під загальну категорію "дизайн бібліотеки відрізняється від дизайну додатків". Бібліотека або рамки можуть вільно використовувати DI всередині, але з часом доведеться виконати якусь фактичну роботу, інакше це не дуже корисна бібліотека. Скажімо, ви розвиваєте мережеву бібліотеку; ви дійсно не хочете, щоб споживач міг забезпечити власну реалізацію розетки. Ви можете внутрішньо використовувати абстракцію сокета, але API, який ви піддаєте абонентам, створить власні сокети.

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

Може бути і більше; це ті, які я бачу дещо часто.


Як щодо "будь-коли, коли ти працюєш динамічною мовою"?
Кевін

Немає? Я багато працюю в JavaScript, і він все ще однаково добре застосовується там. Однак "O" і "Я" в SOLID можуть трохи розмитися.
Aaronaught

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

DIP абсолютно не має нічого спільного з типовою системою. І яким чином "першокласні типи" унікальні для пітона? Коли ви хочете щось перевірити ізольовано, ви повинні замінити тестові подвійні на його залежності. Ці тестові двійники можуть бути альтернативними реалізаціями інтерфейсу (на мовах статичного типу), або вони можуть бути анонімними об'єктами або альтернативними типами, які мають на них однакові методи / функції (введення качки). В обох випадках вам все ще потрібен спосіб фактичної заміни примірника.
Aaronaught

1
@Kevin python навряд чи був першою мовою, що володіла або динамічним набором тексту, або зграбними макетами. Це також абсолютно не має значення. Питання полягає не в тому, який тип об’єкта, а в тому, як / де створюється цей об’єкт. Коли об’єкт створює власні залежності, ви змушені перевіряти одиниці того, що має бути деталями реалізації, виконуючи жахливі речі, такі як заглушення конструкторів класів, про які публічний API не згадує. І забуття про тестування, поведінку змішування та побудову об'єкта просто призводить до тісного з'єднання. Введення качок не вирішує жодну з цих проблем.
Aaronaught

2

Деякі ознаки, що ви можете застосовувати DIP на занадто мікро рівні, де він не надає значення:

  • у вас є пара C / CImpl або IC / C, лише одна реалізація цього інтерфейсу
  • підписи у вашому інтерфейсі та реалізації відповідають один на один (порушує принцип DRY)
  • ви часто змінюєте C і CImpl одночасно.
  • C є внутрішнім для вашого проекту і не поділяється поза вашим проектом як бібліотека.
  • ви розчаровані F3 в Eclipse / F12 у Visual Studio, переводячи вас в інтерфейс замість фактичного класу

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

Крім того, я не думаю про оформлення методу методом введення залежності / динамічний проксі-сервер (Spring, Java EE) так само, як і справжній SOLID DIP - це більше схоже на деталі реалізації того, як працює декорація методу в цьому стеку технологій. Спільнота Java EE вважає вдосконаленням, що вам не потрібні пари Foo / FooImpl, як раніше ( посилання ). На відміну від цього, Python підтримує оздоблення функції як першокласної функції мови.

Дивіться також цю публікацію в блозі .


0

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

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

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

Більш повне пояснення дивіться у цій статті :

Суть правильного застосування принципу інверсії залежності полягає в наступному:

Розділіть код / ​​послугу /… від вас залежить інтерфейс та реалізація. Інтерфейс реструктурує залежність в жаргоні коду, що використовує його, реалізація реалізує його з точки зору своїх базових методів.

Реалізація залишається там, де вона є. Але зараз інтерфейс має іншу функцію (і використовує інший жаргон / мову), описуючи те, з чим може скористатися використаний код. Перемістіть його до того пакету. Не розміщуючи інтерфейс та реалізацію в одному пакеті, залежність (напрямок) перетворюється від користувача → реалізація до реалізації → користувач.

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