Що таке принцип інверсії залежності і чому він важливий?
Що таке принцип інверсії залежності і чому він важливий?
Відповіді:
Перевірте цей документ: Принцип інверсії залежності .
Це в основному говорить:
Щодо того, чому це важливо, коротше: зміни є ризиковими, і залежно від концепції, а не від реалізації, ви зменшуєте потребу в змінах на сайтах викликів.
Ефективно, DIP зменшує зв'язок між різними фрагментами коду. Ідея полягає в тому, що, хоча існує багато способів реалізації, скажімо, лісозаготівлі, спосіб, яким ви користуєтесь ним, повинен бути відносно стабільним у часі. Якщо ви можете витягти інтерфейс, який представляє концепцію ведення журналів, цей інтерфейс повинен бути набагато стабільнішим за час, ніж його реалізація, а сайти викликів повинні бути значно менше впливати на зміни, які ви могли внести під час збереження або розширення цього механізму реєстрації.
Завдяки тому, що реалізація залежить від інтерфейсу, ви отримуєте можливість у процесі виконання вибирати, яка реалізація краще підходить для вашого конкретного середовища. Залежно від випадків, це може бути цікавим.
Книги Agile Development Software, Principles, Patterns, and Practices та Agile Principles, Patterns and Practices в C # - найкращі ресурси для повного розуміння початкових цілей та мотивацій, що стоять за принципом інверсії залежності. Стаття "Принцип інверсії залежності" також є хорошим ресурсом, але через те, що це узагальнена версія проекту, який врешті-решт пробився у вищезгадані книги, він залишає важливу дискусію щодо концепції володіння пакетом та інтерфейсом, які є ключовими для розрізнення цього принципу, від загальної радить "програмувати на інтерфейс, а не на реалізацію", знайдений у книзі Шаблони дизайну (Gamma, et al.).
Для надання підсумків, Принцип інверсії залежності залежить насамперед про зміну звичайного напрямку залежності від компонентів "вищого рівня" до компонентів "нижчого рівня", так що компоненти "нижчого рівня" залежать від інтерфейсів, що належать компонентам "вищого рівня". . (Примітка: компонент "вищого рівня" тут відноситься до компонента, що вимагає зовнішніх залежностей / послуг, не обов'язково його концептуальної позиції в шаруватої архітектури.) При цьому зв'язок не зменшується настільки, як він зміщений від компонентів, які теоретично менш цінний для компонентів, які теоретично є більш цінними.
Це досягається шляхом проектування компонентів, зовнішні залежності яких виражаються в інтерфейсі, для якого реалізацію повинен забезпечити споживач компонента. Іншими словами, визначені інтерфейси виражають те, що потрібно компоненту, а не як ви використовуєте компонент (наприклад, "INeedSomething", а не "IDoSomething").
Принцип інверсії залежності не посилається на просту практику абстрагування залежностей за допомогою інтерфейсів (наприклад, MyService → [ILogger ⇐ Logger]). Хоча це відокремлює компонент від конкретної деталі реалізації залежності, він не інвертує взаємозв'язок між споживачем та залежністю (наприклад, [MyService → IMyServiceLogger] ⇐ Logger.
Важливість принципу інверсії залежності залежить від єдиної мети - можливості повторно використовувати компоненти програмного забезпечення, які залежать від зовнішніх залежностей для частини їх функціональності (реєстрація, перевірка та ін.)
У рамках цієї загальної мети повторного використання ми можемо виділити два підтипи повторного використання:
Використання програмного компонента в декількох додатках з реалізаціями підзалежності (наприклад, ви розробили контейнер DI і хочете забезпечити ведення журналів, але не хочете з'єднувати ваш контейнер із певним реєстратором, таким чином, щоб також використовувався кожен, хто використовує ваш контейнер використовувати обрану бібліотеку журналів).
Використання компонентів програмного забезпечення в контексті, що розвивається (наприклад, ви розробили бізнес-логічні компоненти, які залишаються однаковими для декількох версій програми, де змінюються деталі реалізації).
У першому випадку повторного використання компонентів у кількох додатках, таких як бібліотека інфраструктури, мета полягає в тому, щоб забезпечити ваші споживачі основної інфраструктури, не прив'язуючи споживачів до субзалежностей вашої власної бібліотеки, оскільки для залежності від таких залежностей потрібна ваша споживачі також вимагають однакових залежностей. Це може бути проблематично, коли споживачі вашої бібліотеки вирішили використовувати іншу бібліотеку для тих же інфраструктурних потреб (наприклад, NLog проти log4net) або якщо вони вирішили використовувати більш пізню версію потрібної бібліотеки, яка не сумісна з версією потрібна вашій бібліотеці.
У другому випадку повторного використання бізнес-логічних компонентів (тобто "компонентів вищого рівня") мета полягає в тому, щоб виділити реалізацію основного домену вашої програми від мінливих потреб деталей вашої реалізації (тобто зміни / оновлення стійкості бібліотек, бібліотек обміну повідомленнями , стратегії шифрування тощо). В ідеалі, зміна деталей реалізації програми не повинно порушувати компоненти, що інкапсулюють ділову логіку програми.
Примітка. Деякі можуть заперечити, щоб цей другий випадок описати як фактичне повторне використання, мотивуючи це тим, що такі компоненти, як компоненти бізнес-логіки, що використовуються в межах однієї еволюціонуючої програми, представляють лише одне використання. Однак ідея полягає в тому, що кожна зміна деталей реалізації додатка створює новий контекст і, отже, інший випадок використання, хоча кінцеві цілі можна виділити як ізоляція проти переносимості.
Хоча дотримання принципу інверсії залежності у цьому другому випадку може принести певну користь, слід зазначити, що його значення, застосоване до сучасних мов, таких як Java та C #, значно знижується, можливо, до того, що воно не має значення. Як обговорювалося раніше, DIP передбачає повне розділення деталей реалізації на окремі пакети. Однак у випадку що розвивається додатку просто використання інтерфейсів, визначених у домені бізнесу, захистить від необхідності змінювати компоненти більш високого рівня через зміни потреб у деталях деталізації про реалізацію, навіть якщо деталі про реалізацію остаточно містяться в одному пакеті . Ця частина принципу відображає аспекти, що стосувалися мови з огляду на те, коли принцип був кодифікований (тобто C ++), які не стосуються нових мов. Це сказав:
Більш тривале обговорення цього принципу, оскільки воно стосується простого використання інтерфейсів, залежної ін'єкції та схеми розділеного інтерфейсу, можна знайти тут . Крім того, обговорення того , як цей принцип відноситься до динамічно типізованих мов , таких як JavaScript можна знайти тут .
Коли ми розробляємо програмні програми, ми можемо вважати класи низького рівня класами, які реалізують основні та основні операції (доступ до диска, мережеві протоколи, ...), а класи високого рівня - класи, які інкапсулюють складну логіку (бізнес-потоки, ...).
Останні покладаються на класи низького рівня. Природним способом реалізації таких структур було б писати класи низького рівня, і як тільки ми їх маємо написати складні класи високого рівня. Оскільки класи високого рівня визначаються з точки зору інших, це здається логічним способом зробити це. Але це не гнучка конструкція. Що станеться, якщо нам потрібно замінити клас низького рівня?
Принцип інверсії залежності визначає, що:
Цей принцип прагне "перевернути" загальноприйняте уявлення про те, що модулі високого рівня в програмному забезпеченні повинні залежати від модулів нижчого рівня. Тут модулі високого рівня володіють абстракціями (наприклад, визначаючи методи інтерфейсу), які реалізуються модулями нижчого рівня. Таким чином, роблячи модулі нижчого рівня залежними від модулів вищого рівня.
Добре застосована інверсія залежності залежить від гнучкості та стабільності на рівні всієї архітектури вашої програми. Це дозволить вашій програмі розвиватися більш безпечно та стабільно.
Традиційно інтерфейс багатошарової архітектури залежав від бізнес-рівня, а це в свою чергу залежало від рівня доступу до даних.
Ви повинні розуміти шар, пакет чи бібліотеку. Подивимось, яким би був код.
У нас буде бібліотека або пакет для рівня доступу до даних.
// DataAccessLayer.dll
public class ProductDAO {
}
І ще одна логіка бізнесу бібліотеки чи пакету, що залежить від рівня доступу до даних.
// BusinessLogicLayer.dll
using DataAccessLayer;
public class ProductBO {
private ProductDAO productDAO;
}
Інверсія залежності вказує на наступне:
Модулі високого рівня не повинні залежати від модулів низького рівня. Обидва повинні залежати від абстракцій.
Абстракції не повинні залежати від деталей. Деталі повинні залежати від абстракцій.
Що таке модулі високого рівня та низький рівень? Мислячі модулі, такі як бібліотеки або пакети, модулем високого рівня будуть такі, від яких традиційно є залежності та низький рівень, від яких вони залежать.
Іншими словами, високий рівень модуля був би там, де викликається дія, і низький рівень, де виконується дія.
З цього принципу можна зробити розумний висновок про те, що між конкрементами не повинно бути залежності, але повинна бути залежність від абстракції. Але відповідно до прийнятого нами підходу ми можемо неправильно застосовувати залежність від інвестицій, а абстракцію.
Уявіть, що ми адаптуємо наш код наступним чином:
У нас буде бібліотека або пакет для рівня доступу до даних, який визначає абстракцію.
// DataAccessLayer.dll
public interface IProductDAO
public class ProductDAO : IProductDAO{
}
І ще одна логіка бізнесу бібліотеки чи пакету, що залежить від рівня доступу до даних.
// BusinessLogicLayer.dll
using DataAccessLayer;
public class ProductBO {
private IProductDAO productDAO;
}
Хоча ми залежні від абстракції, залежність між бізнесом та доступом до даних залишається однаковою.
Щоб отримати інверсію залежності, інтерфейс стійкості повинен бути визначений в модулі або пакеті, де ця логіка або домен високого рівня, а не в модулі низького рівня.
Спочатку визначте, що таке доменний шар, і абстрагування його зв'язку визначається стійкістю.
// Domain.dll
public interface IProductRepository;
using DataAccessLayer;
public class ProductBO {
private IProductRepository productRepository;
}
Після того, як рівень стійкості залежить від домену, переходимо до інвертування зараз, якщо визначена залежність.
// Persistence.dll
public class ProductDAO : IProductRepository{
}
(джерело: xurxodev.com )
Важливо добре засвоїти концепцію, поглиблюючи мету та переваги. Якщо ми залишимось механічно та вивчимо типовий сховище випадків, ми не зможемо визначити, де ми можемо застосувати принцип залежності.
Але чому ми інвертуємо залежність? Яка головна мета поза конкретними прикладами?
Таке зазвичай дозволяє найчастіше змінювати найбільш стійкі речі, які не залежать від менш стійких речей.
Для типу стійкості легше змінити доступ до бази даних чи технології до тієї самої бази даних, ніж логіка домену або дії, призначені для постійної комунікації. Через це залежність повертається назад, оскільки так простіше змінити стійкість, якщо ця зміна відбудеться. Таким чином нам не доведеться змінювати домен. Доменний шар є найбільш стабільним з усіх, тому він не повинен залежати ні від чого.
Але є не лише цей приклад сховища. Існує багато сценаріїв, де застосовується цей принцип, і є архітектури, засновані на цьому принципі.
Є архітектури, де інверсія залежності є ключовою для її визначення. У всіх доменах це найважливіше, і саме абстракції вказують на протокол зв'язку між доменом та рештою пакетів або бібліотек.
У чистій архітектурі домен розташований в центрі, і якщо ви дивитесь у напрямку стрілок, що вказують на залежність, зрозуміло, які є найважливіші та стабільні шари. Зовнішні шари вважаються нестабільними інструментами, тому уникайте залежно від них.
(джерело: 8thlight.com )
Це відбувається так само з шестикутною архітектурою, де домен також розташований у центральній частині, а порти - це абстракції зв'язку від доміно назовні. Тут знову видно, що домен є найбільш стійкою і традиційна залежність перевернута.
На мій принцип, принцип інверсії залежності, як описано в офіційній статті , є дійсно помилковою спробою збільшити повторне використання модулів, які за своєю суттю є менш використаними, а також спосіб вирішити проблему мовою C ++.
Проблема в C ++ полягає в тому, що файли заголовків зазвичай містять декларації приватних полів і методів. Тому, якщо модуль високого рівня C ++ включає заголовки для модуля низького рівня, це буде залежати від фактичної реалізації деталей цього модуля. І це, очевидно, не дуже добре. Але це не проблема в сучасних мовах, які часто використовуються сьогодні.
Модулі високого рівня за своєю суттю менш багаторазові для використання, ніж модулі низького рівня, оскільки перші зазвичай більше конкретного додатка / контексту, ніж останні. Наприклад, компонент, який реалізує екран інтерфейсу користувача, є найвищим рівнем, а також дуже (повністю?) Специфічним для програми. Спроба повторно використовувати такий компонент в іншому додатку є контрпродуктивною і може призвести лише до надмірної інженерії.
Отже, створення окремої абстракції на тому самому рівні компонента A, що залежить від компонента B (який не залежить від A), можна здійснити лише в тому випадку, якщо компонент A буде дійсно корисним для повторного використання в різних програмах або контекстах. Якщо це не так, то застосування DIP було б поганим дизайном.
В основному це говорить:
Клас повинен залежати від абстракцій (наприклад, інтерфейс, абстрактні класи), а не конкретних деталей (реалізації).
Хороші відповіді та хороші приклади тут вже дають інші.
Причина DIP важлива в тому, що вона забезпечує принципом ОО "слабко пов'язана конструкція".
Об'єкти у вашому програмному забезпеченні НЕ повинні потрапляти в ієрархію, де деякі об'єкти - це найвищі рівні, залежні від об'єктів низького рівня. Зміни об’єктів низького рівня потім перейдуть до об'єктів верхнього рівня, що робить програмне забезпечення дуже крихким для змін.
Ви хочете, щоб ваші об'єкти вищого рівня були дуже стабільними і не були крихкими для змін, тому вам потрібно перевернути залежності.
Набагато чіткіший спосіб заявити про принцип інверсії залежності:
Ваші модулі, які інкапсулюють складну бізнес-логіку, не повинні безпосередньо залежати від інших модулів, які інкапсулюють бізнес-логіку. Натомість вони повинні залежати лише від інтерфейсів до простих даних.
Тобто, замість того, щоб реалізувати свій клас, Logic
як це зазвичай роблять люди:
class Dependency { ... }
class Logic {
private Dependency dep;
int doSomething() {
// Business logic using dep here
}
}
ви повинні зробити щось на кшталт:
class Dependency { ... }
interface Data { ... }
class DataFromDependency implements Data {
private Dependency dep;
...
}
class Logic {
int doSomething(Data data) {
// compute something with data
}
}
Data
і DataFromDependency
повинен жити в тому ж модулі Logic
, що і не Dependency
.
Навіщо це робити?
Dependency
змін вам не потрібно змінювати Logic
.Logic
робить, є набагато простішим завданням: воно діє лише на те, що схоже на ADT.Logic
тепер можна легше перевірити. Тепер ви можете безпосередньо створювати інстанції Data
з фальшивими даними та передавати їх. Не потрібно макети чи складні тестові риштування.DataFromDependency
, на який безпосередньо посилаються Dependency
, знаходиться той самий модуль Logic
, що і Logic
модуль, він все ще безпосередньо залежить від Dependency
модуля під час компіляції. За поясненням дядька Боба принципу , уникаючи в цьому вся точка DIP. Швидше, щоб слідувати DIP, Data
має бути в тому ж модулі, що і Logic
, але DataFromDependency
бути в тому ж модулі, що і Dependency
.
Інверсія управління (IoC) - це схема дизайну, коли об'єкт передає свою залежність поза рамками, а не запитує рамки для своєї залежності.
Приклад псевдокоду з використанням традиційного пошуку:
class Service {
Database database;
init() {
database = FrameworkSingleton.getService("database");
}
}
Аналогічний код за допомогою IoC:
class Service {
Database database;
init(database) {
this.database = database;
}
}
Перевагами IoC є:
Суть інверсії залежності полягає у створенні програмного забезпечення для багаторазового використання.
Ідея полягає в тому, що замість двох фрагментів коду, що спираються один на одного, вони покладаються на якийсь абстрагований інтерфейс. Тоді ви можете повторно використовувати будь-який шматок без іншого.
Шлях цього найчастіше досягається за допомогою інверсії керуючого (IoC) контейнера, як Spring у Java. У цій моделі властивості об'єктів встановлюються через конфігурацію XML замість того, щоб об'єкти виходили і знаходили їх залежність.
Уявіть цей псевдокод ...
public class MyClass
{
public Service myService = ServiceLocator.service;
}
MyClass безпосередньо залежить як від класу Service, так і від класу ServiceLocator. Це потрібно обом, якщо ви хочете використовувати його в іншому додатку. А тепер уявіть це ...
public class MyClass
{
public IService myService;
}
Тепер MyClass покладається на єдиний інтерфейс, інтерфейс IService. Ми дозволили контейнеру IoC фактично встановити значення цієї змінної.
Тож зараз MyClass можна легко використовувати в інших проектах, не привозячи залежність цих двох класів разом із ним.
Ще краще, вам не доведеться перетягувати залежності MyService, а також залежності цих залежностей, і ... ну, ви отримуєте ідею.
Якщо ми можемо сприймати це як те, що працівник корпорації "високого рівня" платить за виконання своїх планів, і що ці плани здійснюються сукупним виконанням багатьох планів "низького рівня" працівника, то можна сказати це загалом жахливий план, якщо опис плану працівника високого рівня будь-яким чином поєднаний з конкретним планом будь-якого працівника нижчого рівня.
Якщо керівник вищого рівня має план "покращити час доставки" і вказує, що працівник на судноплавній лінії повинен пити каву і робити розтяжки щоранку, то цей план сильно пов'язаний і має низьку згуртованість. Але якщо в плані не згадується жоден конкретний працівник, а він фактично вимагає "суб'єкт, який може виконувати роботу, готовий до роботи", тоді план нещільно пов'язаний і згуртованіший: плани не перетинаються і легко можуть бути замінені . Підрядники, або роботи, можуть легко замінити співробітників, і план високого рівня залишається незмінним.
"Високий рівень" в принципі інверсії залежності означає "важливіший".
Я можу побачити, що в вищезазначених відповідях було добре пояснення. Однак я хочу надати просте пояснення простим прикладом.
Принцип інверсії залежності дозволяє програмісту усунути твердо кодовані залежності, щоб додаток було вільно пов'язане та розширюване.
Як цього досягти: за допомогою абстракції
Без інверсії залежності:
class Student {
private Address address;
public Student() {
this.address = new Address();
}
}
class Address{
private String perminentAddress;
private String currentAdrress;
public Address() {
}
}
У наведеному вище фрагменті коду адресний об'єкт жорстко закодований. Натомість, якщо ми можемо використовувати інверсію залежності та ввести об'єкт адреси, пропустивши метод конструктора або сеттера. Подивимось.
З інверсією залежності:
class Student{
private Address address;
public Student(Address address) {
this.address = address;
}
//or
public void setAddress(Address address) {
this.address = address;
}
}
Інверсія залежності: залежить від абстракцій, а не від конкрементів.
Інверсія управління: Main vs Abstraction, і як Main є клеєм систем.
Про це говорять кілька хороших постів:
https://coderstower.com/2019/03/26/dependency-inversion-why-you-shouldnt-avoid-it/
https://coderstower.com/2019/04/02/main-and-abstraction-the-decoupled-peers/
https://coderstower.com/2019/04/09/inversion-of-control-putting-all-together/
Скажімо, у нас є два класи: Engineer
і Programmer
:
Інженер класів залежить від класу програміста, як нижче:
class Engineer () {
fun startWork(programmer: Programmer){
programmer.work()
}
}
class Programmer {
fun work(){
//TODO Do some work here!
}
}
У цьому прикладі клас Engineer
має залежність від нашого Programmer
класу. Що буде, якщо мені потрібно змінити Programmer
?
Очевидно, що мені теж потрібно змінити Engineer
. (Нічого собі, в цьому пункті OCP
теж порушено)
Тоді, що ми маємо, щоб почистити цей безлад? Відповідь - абстракція насправді. Абстрагуванням ми можемо зняти залежність між цими двома класами. Наприклад, я можу створити Interface
клас для програміста, і відтепер кожен клас, який бажає використовувати Programmer
, повинен використовувати його. Interface
Потім, змінюючи клас програміста, нам не потрібно змінювати будь-які класи, які його використовували, через абстракцію, яку ми б / в.
Примітка: DependencyInjection
може допомогти нам зробити DIP
і SRP
теж.
Додаючи до шквалу загально хороших відповідей, я хотів би додати крихітний зразок власного, щоб продемонструвати гарну та погану практику. І так, я не один кидати каміння!
Скажімо, ви хочете, щоб маленька програма перетворила рядок у формат base64 за допомогою консольного вводу / виводу. Ось наївний підхід:
class Program
{
static void Main(string[] args)
{
/*
* BadEncoder: High-level class *contains* low-level I/O functionality.
* Hence, you'll have to fiddle with BadEncoder whenever you want to change
* the I/O mode or details. Not good. A good encoder should be I/O-agnostic --
* problems with I/O shouldn't break the encoder!
*/
BadEncoder.Run();
}
}
public static class BadEncoder
{
public static void Run()
{
Console.WriteLine(Convert.ToBase64String(Encoding.UTF8.GetBytes(Console.ReadLine())));
}
}
DIP в основному говорить, що компоненти високого рівня не повинні залежати від низькорівневої реалізації, де "рівень" - це відстань від вводу / виводу за словами Роберта К. Мартіна ("Чиста архітектура"). Але як вийти з цього затруднення? Просто зробивши центральний Енкодер залежним лише від інтерфейсів, не турбуючись про те, як вони реалізовані:
class Program
{
static void Main(string[] args)
{
/* Demo of the Dependency Inversion Principle (= "High-level functionality
* should not depend upon low-level implementations"):
* You can easily implement new I/O methods like
* ConsoleReader, ConsoleWriter without ever touching the high-level
* Encoder class!!!
*/
GoodEncoder.Run(new ConsoleReader(), new ConsoleWriter()); }
}
public static class GoodEncoder
{
public static void Run(IReadable input, IWriteable output)
{
output.WriteOutput(Convert.ToBase64String(Encoding.ASCII.GetBytes(input.ReadInput())));
}
}
public interface IReadable
{
string ReadInput();
}
public interface IWriteable
{
void WriteOutput(string txt);
}
public class ConsoleReader : IReadable
{
public string ReadInput()
{
return Console.ReadLine();
}
}
public class ConsoleWriter : IWriteable
{
public void WriteOutput(string txt)
{
Console.WriteLine(txt);
}
}
Зауважте, що вам не потрібно торкатися GoodEncoder
, щоб змінити режим вводу / виводу - цей клас задоволений інтерфейсами вводу / виводу, які він знає; будь-яка низькорівнева реалізація IReadable
і IWriteable
ніколи її не буде турбувати.
GoodEncoder
вашого другого прикладу. Щоб створити приклад DIP, вам потрібно ввести поняття про те, що "володіє" інтерфейсами, які ви витягли тут, і, зокрема, помістити їх у той самий пакет, що і GoodEncoder, поки їх реалізації залишаються назовні.
Принцип інверсії залежності (DIP) говорить про це
i) Модулі високого рівня не повинні залежати від модулів низького рівня. Обидва повинні залежати від абстракцій.
ii) Абстракції ніколи не повинні залежати від деталей. Деталі повинні залежати від абстракцій.
Приклад:
public interface ICustomer
{
string GetCustomerNameById(int id);
}
public class Customer : ICustomer
{
//ctor
public Customer(){}
public string GetCustomerNameById(int id)
{
return "Dummy Customer Name";
}
}
public class CustomerFactory
{
public static ICustomer GetCustomerData()
{
return new Customer();
}
}
public class CustomerBLL
{
ICustomer _customer;
public CustomerBLL()
{
_customer = CustomerFactory.GetCustomerData();
}
public string GetCustomerNameById(int id)
{
return _customer.GetCustomerNameById(id);
}
}
public class Program
{
static void Main()
{
CustomerBLL customerBLL = new CustomerBLL();
int customerId = 25;
string customerName = customerBLL.GetCustomerNameById(customerId);
Console.WriteLine(customerName);
Console.ReadKey();
}
}
Примітка: Клас повинен залежати від абстракцій, таких як інтерфейс або абстрактні класи, а не конкретних деталей (реалізація інтерфейсу).