Dependency inversion principle реалізується за допомогою. Принцип інверсії залежностей Багаторівнева архітектура з інверсією залежностей


DISCLAIMER: Автор цієї статті не має на меті підірвати авторитет або якимось чином образити настільки шанованого камрада, як «дядечко» Боб Мартін Йдеться тут швидше про ретельніше обмірковування принципу інверсії залежностей та аналіз прикладів, використаних при його описі.

Під час статті я наводитиму всі необхідні цитати та приклади з вищезгаданих джерел. Але щоб не було «спойлерів» і ваша думка залишалася об'єктивною, я рекомендував би витратити 10-15 хвилин і ознайомитися з оригінальним описом цього принципу в статті або книзі.

Принцип інверсії залежностей звучить так:

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

Розбиття на шари

У цибулі є шари, у торта є шари, у людожерів є шари та у програмних систем – теж є шари! - Шрек (с)
Будь-яка складна система є ієрархічною: кожен шар будується на базі перевіреного і добре працюючого шару нижчого рівня. Це дозволяє зосередитись у кожний момент часу на обмеженому наборі концепцій, не замислюючись про те, як реалізовані шари нижнього рівня.
В результаті ми отримуємо приблизно таку діаграму:

Малюнок 1 – «Наївна» схема розбиття на шари

З погляду Боба Мартіна така схема розбиття системи на шари є. наївний. Недоліком такого дизайну є «підступна особливість: шар Policyзалежить від змін у всіх шарах на шляху до Utility. Ця залежність транзитивна.» .

Хм… Незвичайне твердження. Якщо говорити про платформу.NET, то залежність буде транзитивною лише в тому випадку, якщо поточний модуль «виставлятиме» модулі нижніх рівнів у своєму відкритому інтерфейсі. Іншими словами, якщо в МеханізмLayerє відкритий клас, який приймає як аргумент екземпляр StringUtilUtilityLayer), то всі клієнти рівня МеханізмLayerстають залежними на UtilityLayer. В іншому випадку транзитивність змін відсутня: всі зміни нижнього рівня обмежені поточним рівнем і не поширюються вище.

Щоб зрозуміти думку Боба Мартіна, треба згадати, що вперше принцип інверсії залежностей був описаний у далекому 1996-му році, і як приклади використовувалася мова С++. У вихідній статті сам автор пише про те, що проблема транзитивності є лише мовами без чіткого поділу інтерфейсу класу від реалізації. У С++ і справді проблема транзитивних залежностей актуальна: якщо файл PolicyLayer. hвключає за допомогою директиви «include» MechanismLayer. h, який, у свою чергу включає UtilityLayer. h, то при будь-якій зміні в заголовному файлі UtilityLayer. h(навіть у «закритій» секції класів, оголошених у цьому файлі) нам доведеться перекомпілювати та розвернути знову всіх клієнтів. Однак у С++ ця проблема вирішується шляхом використання ідіоми PIml, запропонованої Гербом Саттером і зараз теж не така актуальна.

Вирішення цієї проблеми з точки зору Боба Мартіна полягає в наступному:

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



Малюнок 2 – Інвертовані шари

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

Але з іншого боку, коли мова заходить саме про шари, які представляють зазвичай складання (або пакети в термінах UML), то запропонований підхід навряд чи можна назвати життєздатним. За своїм визначенням допоміжні класи нижнього рівня використовуються в десятці різних модулях вищого рівня. Utility Layerбуде використовуватися не тільки в Mechanism Layer, але ще й у Data Access Layer, Transport Layer, Some Other Layer. Чи повинен він у такому разі реалізовувати інтерфейси, визначені у всіх модулях вищого рівня?

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

Поняття абстракції

Багато термінів настільки «в'їдаються» в наш мозок, що ми перестаємо звертати на них увагу. Для більшості «об'єктно-орієнтованих» програмістів це означає, що ми перестаємо замислюватися над багатьма заїждженими термінами, як «абстракція», «поліморфізм», «інкапсуляція». Чого над ними думати, адже все й так зрозуміло? ;)

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

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

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

Повернімося до визначення: Абстракції не повинні залежати від деталей. Деталі мають залежати від абстракцій.

Який приклад виникає в голові тепер після того, як ми згадали, що ж таке абстракція? Коли абстракція починає залежати від деталей? Прикладом порушення цього принципу може бути абстрактний клас GZipStream, який приймає MemoryStream, а не абстрактний клас Stream:

Abstract class GZipStream ( // Абстракція GZipStream приймає конкретний потік protected GZipStream(MemoryStream memoryStream) () )

Іншим прикладом порушення цього принципу може бути абстрактний клас репозиторію з шару доступу до даних, який приймає в конструкторі PostgreSqlConnectionабо рядок підключення для SQL Server, що робить будь-яку реалізацію такої абстракції, зав'язаної на конкретну реалізацію. Але чи це має на увазі Боб Мартін? Якщо судити з прикладів, наведених у статті чи книзі, то під поняттям «абстракції» Боб Мартін розуміє щось зовсім інше.

ПринципDIPпо Мартіну

Для пояснення свого визначення Боб Мартін дає таке пояснення.

Ледве спрощена, але все ще дуже дієва інтерпретація принципу DIP виражається простим евристичним правилом: «Залежати треба від абстракцій». Воно говорить, що має бути залежностей від конкретних класів; всі зв'язки в програмі повинні вести абстрактний клас або інтерфейс.

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

Як ілюстрація порушення принципу DIP взагалі, і першого «прояснюючого» пункту, зокрема, наводиться наступний приклад:

Public class Button ( private Lamp lamp; public void Poll() ( if (/* якась умова */) lamp.TurnOn(); ) )

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

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

Дивлячись на клас Buttonя не можу сказати помилковий дизайн чи ні. Я можу достеменно сказати, що ім'я класу не відповідає його реалізації. Клас потрібно перейменувати на LampButtonабо прибрати з класу Buttonполе Lamp.

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

По перше, я не бачу в цьому прикладі «стратегій верхнього рівня» та «модулів нижнього рівня»: на мій погляд, класи Buttonі Lampзнаходяться на одному рівні абстракції (принаймні, я не бачу аргументів, що доводять протилежне). Той факт, що клас Buttonможе кимось керувати не робить його більш високорівневим. По-друге, тут немає «абстракції, яка залежить від деталей», тут є «реалізація абстракції, яка від деталей», що зовсім одне й теж.

Рішення щодо Мартіна таке:



Малюнок 3 – «Інвертування залежностей»

Чи краще це рішення? Давайте подивимося…

Головним плюсом інвертування залежностей «за Мартіном» є інвертування володіння. У вихідному дизайні, при зміні класу Lampдовелося б змінюватися класу Button. Тепер клас Button«володіє» інтерфейсом ButtonServer, а він не може змінитися через зміну «нижніх рівнів», таких як Lamp. Все якраз навпаки: зміна класу ButtonServerможливо тільки під впливом змін класу Button, що призведе до зміни всіх спадкоємців класу ButonServer!

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

  • модулі верхнього рівня не повинні залежати від нижнього модулів. Обидва мають залежати від абстракції.
  • абстракції не повинні залежати від деталей. Деталі мають залежати від абстракцій.

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

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

А тепер розглянемо приклад, який наочно продемонструє, як виглядає слабке зв'язування в дії.

Скажімо, ми вирішили замовити торт на день народження. Для цього ми вирушили до чудової пекарні на розі вулиці. Дізналися, чи можуть вони для нас спекти торт під гучною назвою “Розкіш”, і, отримавши позитивну відповідь, замовили. Все просто.

А тепер визначимося з тим, які абстракції слід передбачити у цьому коді. Для цього просто поставимо собі кілька запитань:

  • Чому саме торт? Можна було замовити і пиріг чи кекси
  • Чому саме до дня народження? А якби це було весілля чи випускний?
  • Чому саме “Розкіш”, а раптом мені більше подобається “Мурашник” чи “Празький”?
  • Чому саме в цю пекарню, а не в кондитерську майстерню в центрі міста чи ще кудись?

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

Тепер займемося конструювання самих типів.

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

Interface IPastry (string Name (get; set;))

А ось і конкретна реалізація під наш випадок – торт до дня народження. Як бачимо, традиційно торт до дня народження передбачає свічки)))

Class BirthdayCake: IPastry ( public int NumberOfCandles ( get; set; ) public string Name ( get; set; ) ) ; ) )

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

Наступне питання – чим відрізняються один від одного кондитерські вироби (крім назви, звичайно). Звісно ж, рецептом!

А до поняття рецепта входить список інгредієнтів та опис процесу приготування. Тому для поняття інгредієнт у нас буде окремий інтерфейс:

Interface IIngredient ( string IngredientName ( get;set;) double Quantity ( get; set; ) string Units ( get; set; ) )

А ось і самі інгредієнти для нашого торта: борошно, олія, цукор та вершки:

Class Flour:IIngredient (public string IngredientName (get; set;) public double Quantity (get; set;) public string Units (get; set;) public string Quality (get; set;)) ( get; set; ) public double Quantity ( get; set; ) public string Units ( get; set; ) ) class Sugar: IIngredient ( public string IngredientName ( get; set; ) public double Quantity ( get; set; ) public string Units ( get; set; ) public string Kind ( get; set; ) ) class Creme: IIngredient ( public string IngredientName ( get; set; ) public double Quantity ( get; set; ) public string Units ( get; set; ) public double Fat (get; set;))

Список інгредієнтів може відрізнятись у різних рецептах та різних кондитерських виробах, але для нашого рецепту цього списку достатньо.

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

Interface IRecipe (Type PastryType (get; set;) string Name (get;set;) IList Ingredients ( get;set;) string Description ( get;set;) )

А саме клас, що представляє рецепт торта до дня народження, виглядає так:

Class BirthdayCakeRecipe: IRecipe ( public Type PastryType ( get; set; ) public string Name ( get;set;) public IList Ingredients ( get; set; ) public string Description ( get; set; ) public BirthdayCakeReipe() ( Ingredients = new List (); } }

Тепер перейдемо до нашої чудової пекарні на розі вулиці.

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

Interface IBakery ( IPastry Bake (IRecipe recipe); )

А ось і клас, який представляє нашу пекарню:

Class NiceBakeryOnTheCornerOFMyStreet (Dictionary menu = New Dictionary (); public void AddToMenu(IRecipe recipe) ( if (!menu.ContainsKey(recipe.Name)) ( menu.Add(recipe.Name, recipe); ) else ( Console.WriteLine("It is already on the menu"); ) ) Public IRecipe FindInMenu(string name) ( if (menu.ContainsKey(name)) ( return menu; ) Console.WriteLine("Sorry... (Irecipe recipe) ( if (recipe != null) ( IPastry pastry = Activator.CreateInstance(recipe.PastryType) як IPastry; if (pastry != null) ( pastry.Name = recipe.Name; return pastry as IPastry; ) ) return null; ) )

Залишилося лише протестувати роботу коду:

Class Program ( static void Main() ( //створення автентичності з bakery class var bakery = New NiceBakeryOnTheCornerOFMyStreet(); //Preparing ingredients для recip var flour = New Flour() ( IngredientName = "Flour",5 , Units = "kg"); var butter = новий Butter() ( IngredientName = "Butter", Quantity = 0.5, Units = "kg"); var sugar = новий Sugar() ( IngredientName = "Sugar", Quantity = 0.7 , Units = "kg" ); = typeof(BirthdayCake), Name = "Birthday Cake Luxury"; .Add(sugar);weddingCakeRecipe.Ingredients.Add(creme); //now let's order it!! BirthdayCake cake = bakery. !!!Console.WriteLine(cake); ))

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

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

Непоганий результат. А все тому, що ми постаралися зробити класи мінімально пов'язаними один з одним.

А тепер подумаємо над наслідками порушення принципу інверсії залежності:

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

Ну, а тепер робіть висновки самі, наскільки принцип інверсії залежності корисний у коді. Думаю, відповідь очевидна.

14 відповідей

В основному говориться:

  • Абстракції ніколи не повинні залежати від деталей. Деталі мають залежати від абстракцій.

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

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

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

Книги Agile Software Development, Принципи, Шаблони та Практики та Agile Принципи, Шаблони та Практики в С# є найкращими ресурсами для повного розуміння початкових цілей та мотивацій, що лежать в основі Принципу інверсії залежності. Стаття "Принцип обігу залежностей" також є хорошим ресурсом, але через те, що вона є стислою версією чернетки, який в кінцевому підсумку потрапив у раніше згадані книги, вона залишає деякі важливі дискусії про концепцію володіння пакетами та інтерфейсами, які є ключовими для відмінності цього принципу від більш загальної ради "програмувати для інтерфейсу, а не реалізації", яку можна знайти в книзі "Шаблони проектування" (Gamma, et al.).

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

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

Те, на що не посилається Принцип поводження залежностей, - це проста практика абстрагування залежностей за допомогою інтерфейсів (наприклад, MyService → ). Хоча це відокремлює компонент від конкретної деталі реалізації залежності, воно не інвертує відносини між споживачем та залежністю (наприклад, ⇐ Logger).

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

В рамках цієї спільної мети повторного використання ми можемо виділити два підтипи повторного використання:

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

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

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

У другому випадку повторного використання компонентів бізнес-логіки (тобто "компонентів вищого рівня") мета полягає в тому, щоб ізолювати реалізацію програми в основній галузі від змінних потреб ваших деталей реалізації (наприклад, зміна/оновлення постійних бібліотек, бібліотек обміну повідомленнями). стратегії шифрування тощо). В ідеалі зміна деталей реалізації програми не повинна порушувати компоненти, що інкапсулюють бізнес-логіку програми.

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

Хоча дотримання принципу інверсії залежності у другому випадку може принести деяку користь, слід зазначити, що його значення стосовно сучасних мов, таких як Java і С#, значно знижено, можливо, настільки, що воно не має значення. Як обговорювалося раніше, DIP включає повний поділ деталей реалізації на окремі пакети. У випадку програми, що розвивається, проте, просте використання інтерфейсів, визначених у термінах бізнес-області, захистить від необхідності модифікувати компоненти вищого рівня через мінливі потреби компонентів деталізації реалізації, навіть якщо деталі реалізації в кінцевому рахунку будуть знаходитися в одному і тому ж пакеті . Ця частина принципу відображає аспекти, які мали відношення до мови в момент її кодифікації (наприклад, C++), які не мають відношення до нових мов. Тим не менш, важливість Принципу інверсії залежності перш за все пов'язана з розробкою програмних компонентів/бібліотек, що повторно використовуються.

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

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

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

Принцип інверсії залежностей свідчить, що:

  • Модулі високого рівня повинні залежати від модулів низького рівня. Обидва мають залежати від абстракцій.

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

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

Традиційна багаторівнева архітектура

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

Ви повинні розуміти шар, пакет чи бібліотеку. Погляньмо, як буде код.

Ми мали б бібліотеку або пакет для шару доступу до даних.

// 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; використання DataAccessLayer; public class ProductBO ( private IProductRepository productRepository; )

Після того, як рівень сталості залежить від домену, тепер можна інвертувати, якщо визначено залежність.

// Persistence.dll public class ProductDAO: IProductRepository( )

Поглиблення принципу

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

Але чому ми інвертуємо залежність? Яка основна мета поза конкретними прикладами?

Це зазвичай дозволяє найбільш стабільним речам, які залежать від менш стабільних речей, змінюватися частіше.

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

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

архітектури

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

Чиста Архітектура

Для мене принцип інверсії залежностей, описаний в офіційній статті

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

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

Таким чином, створення окремої абстракції на тому самому рівні компонента A, яке залежить від компонента B (яке не залежить від A), може бути виконане тільки в тому випадку, якщо компонент A дійсно буде корисним для повторного використання в різних додатках або контекстах. Якщо це не так, застосування DIP буде поганий дизайн.

Найяскравіший спосіб сформулювати принцип інверсії залежностей:

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

I.e., замість того, щоб реалізувати ваш клас 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 .

Навіщо це?

Хороші відповіді та хороші приклади вже дано іншими тут.

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

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

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

Уявіть собі цей псевдокод...

Public class MyClass ( public Service myService = ServiceLocator.service; )

MyClass безпосередньо залежить як від класу Service, так і від класу ServiceLocator. Це необхідно для обох, якщо ви хочете використовувати його в іншому додатку. Тепер уявіть це...

Public class MyClass ( public IService myService; )

Тепер MyClass використовує один інтерфейс, інтерфейс IService. Ми дозволили б контейнеру IoC фактично встановити значення цієї змінної.

Нехай буде готель, який попросить у виробника продуктів його запаси. Готель дає назву їжі (скажімо, курку) Генератору їжі, і Генератор повертає їжу, що запитується, в готель. Але готель не дбає про тип їжі, яку він отримує та подає. Таким чином, генератор поставляє продукти з етикеткою "Їжа" в готель.

Ця реалізація у JAVA

FactoryClass з фабричним методом. Харчовий генератор

Public class FoodGenerator ( Food food ; new Chicken(); )else food = null; return food; ) )

Клас Анотація/Інтерфейс

Public abstract class Food ( //None of the child class will override this method to ensure quality... public void quality()( String fresh = "This is a fresh " + getName(); String tasty = "This is a tasty " + getName(); System.out.println(fresh); System.out.println(tasty); ) public abstract String getName(); )

Курка реалізує Їжу (Конкретний Клас)

Public class Chicken extends Food ( /*All the food types are required to be fresh and tasty so * They won"t be overriding the super class method "property()"*/ public String getName()( return "Chicken"; ) )

Риба реалізує Їжу (Конкретний Клас)

Public class Fish extends Food ( /*All the food types are required to be fresh and tasty so * They won"t be overriding the super class method "property()"*/ public String getName()( return "Fish"; ) )

На закінчення

Готель

Public class Hotel ( public static void main(String args)( //Using a Factory class.... FoodGenerator foodGenerator = новий FoodGenerator(); //A factory method to instantiate the foods... Food food = foodGenerator.getFood( "chicken"); food.quality(); ) )

Як ви могли бачити, готель не знає, чи це курка або риба. Відомо, що це об'єкт харчування, тобто. Готель залежить від класу харчування.

Також ви можете помітити, що клас Fish and Chicken реалізує клас Food і не пов'язаний безпосередньо з готелем. тобто. курка та риба також залежить від класу продуктів харчування.

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

Це називається інверсією залежності.

Принцип інверсії залежності (DIP) свідчить, що

i) Модулі високого рівня не повинні залежати від низьких модулів. Обидва мають залежати від абстракцій.

ii) Абстракції ніколи не повинні залежати від деталей. Деталі мають залежати від абстракцій.

Public interface ICustomer (string GetCustomerNameById(int id);) ICustomer GetCustomerData() ( return new Customer(); ) ) Public class CustomerBLL ( ICustomer _customer; public CustomerBLL() ( _customer = CustomerFactory.GetCustomerData(); ) public string GetCustomerNameById(int id) ( ) ) Public class Program ( static void Main() ( CustomerBLL customerBLL = новий CustomerBLL(); int customerId = 25; string customerName = customerBLL.GetCustomerNameById(customerId); Console.WriteLine(customerName);

Примітка. Клас повинен залежати від абстракцій, таких як інтерфейс чи абстрактні класи, а чи не від конкретних деталей (реалізація інтерфейсу).

поділитися

Останнє оновлення: 11.03.2016

Принцип інверсії залежностей(Dependency Inversion Principle) служить для створення слабозв'язаних сутностей, які легко тестувати, модифікувати та оновлювати. Цей принцип можна сформулювати так:

Модулі верхнього рівня повинні залежати від модулів нижнього рівня. І ті, й інші повинні залежати від абстракцій.

Абстракції не повинні залежати від деталей. Деталі мають залежати від абстракцій.

Щоб зрозуміти принцип, розглянемо такий приклад:

Class Book (public string Text (get; set;) public ConsolePrinter Printer (get; set;) public void Print() ( Printer.Print(Text); ) ) class ConsolePrinter ( public void Print(string text) ( Console.WriteLine (text); ) )

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

Тепер спробуємо привести наші класи у відповідність до принципу інверсії залежностей, відокремивши абстракції від низькорівневої реалізації:

Interface IPrinter ( void Print(string text); ) class Book ( public string Text ( get; set; ) public IPrinter Printer ( get; set; ) public Book(IPrinter printer) ( this.Printer = printer; ) public void Print( ) ( Printer.Print(Text); ) ) class ConsolePrinter: IPrinter ( public void Print(string text) ( Console.WriteLine("Друк на консолі"); ) ) class HtmlPrinter: IPrinter ( public void Print(string text) ( Console.WriteLine("Друк у html"); ) )

Тепер абстракція друку книги відокремлена від конкретних реалізацій. У результаті клас Book і клас ConsolePrinter залежить від абстракції IPrinter. Крім того, ми також можемо створити додаткові низькорівневі реалізації абстракції IPrinter і динамічно застосовувати їх у програмі:

Book book = new Book(new ConsolePrinter()); book.Print(); book.Printer = New HtmlPrinter(); book.Print();