Запитання в одному з аргументів для структур введення залежності: Чому важко створити графік об'єкта?


13

Рамки введення залежностей, такі як Google Guice, дають таку мотивацію їх використання ( джерело ):

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

Створення графіків об'єктів вручну є трудомістким (...) і ускладнює тестування.

Але я не купую цей аргумент: Навіть не маючи структури введення залежності, я можу писати класи, які прості в інстанції та зручні для тестування. Наприклад, приклад зі сторінки мотивації Guice можна переписати наступним чином:

class BillingService
{
    private final CreditCardProcessor processor;
    private final TransactionLog transactionLog;

    // constructor for tests, taking all collaborators as parameters
    BillingService(CreditCardProcessor processor, TransactionLog transactionLog)
    {
        this.processor = processor;
        this.transactionLog = transactionLog;
    }

    // constructor for production, calling the (productive) constructors of the collaborators
    public BillingService()
    {
        this(new PaypalCreditCardProcessor(), new DatabaseTransactionLog());
    }

    public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard)
    {
        ...
    }
}

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


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

Зауважте, що я не шукаю інших переваг введення залежності - мені цікаво лише розуміння аргументу про те, що "обґрунтування об'єкта важке без введення залежності"
oberlies

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

@Izkata Ні, це не питання розміру. При описаному підході код з цієї посади просто був би new ShippingService().
oberlies

@oberlies Все ще є; Тоді вам доведеться викопати визначення 9 класів, щоб визначити, які класи використовуються для цієї ShippingService, а не одне місце розташування
Ізката

Відповіді:


12

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

  • Оригінальний зріз весни створював звичайний об'єкт, а потім вводив залежність методами сеттера.

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

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

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

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

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

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

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

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

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

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

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

...

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

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

Не обов'язковий підхід, але FWIW

...

Загалом, шаблон DI / інтерфейс зменшує складність вашого коду, виконуючи дві речі:

  • абстрагування залежностей за течією в інтерфейсах

  • "підняття" залежностей за течією з коду та в якийсь контейнер

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


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

1
Але якщо у конструктора PayPalProcessor було два аргументи, звідки вони беруться?
Роб

Це не так. Так само, як BillingService, PayPalProcessor має конструктор з нульовими аргами, який створює потрібних співпрацівників.
oberlies

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

2
@oberlies: Ваш приклад коду розбивається між двома стилями, стилем аргументу конструктора та стилем, який можна сконструювати з нульовими аргами. Помилковість полягає в тому, що конструктивні нульові дуги не завжди можливі. Краса DI або Factory полягає в тому, що вони якимось чином дозволяють будувати об’єкти за допомогою того, що схоже на конструктор з нульовою стрілкою.
rwong

4
  1. Коли ви плаваєте на мілководді кінця басейну, все "легко і зручно". Щойно ви проходите повз десяток або більше предметів, це вже не зручно.
  2. У своєму прикладі ви назавжди прив’язали свій процес оплати до PayPal. Припустимо, ви хочете використовувати інший процесор кредитної картки? Припустимо, ви хочете створити спеціалізований процесор кредитних карт, який обмежений у мережі? Або вам потрібно перевірити обробку номерів кредитної картки? Ви створили не портативний код: "пишіть один раз, використовуйте лише один раз, оскільки це залежить від конкретного графіка об'єкта, для якого він був розроблений".

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

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


Коли я слідую за вашим аргументом, міркування щодо DI повинно було бути "створення об’єктного графіка вручну легко, але призводить до коду, який важко підтримувати". Однак було твердження, що "створити графік об'єкта вручну важко", і я шукаю лише аргументи, що підтверджують це твердження. Тож ваша публікація не відповідає на моє запитання.
oberlies

1
Я думаю , якщо ви зменшуєте питання «створення в графі об'єктів ...», то це просто. Моя думка полягає в тому, що DI вирішує проблему, з якою ви ніколи не маєте справу лише з одним об’єктним графіком; ви маєте справу з родиною з них.
BobDalgleish

Насправді, просто створити один об’єктний графік вже може бути складним, якщо спільних співробітників .
oberlies

4

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

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


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

1
@Xion Графік залежності повинен бути спрямованим ациклічним графіком. Немає вимоги, що між двома вузлами, як у дерев, існує лише рівно один шлях.
oberlies

@Xion: Не обов’язково. Розглянемо, UnitOfWorkякий має сингулярне DbContextта декілька сховищ. Усі ці сховища повинні використовувати один і той же DbContextоб’єкт. З запропонованим ОП "самонаданням", це зробити неможливо.
Flater

1

Я не використовував Google Guice, але багато часу переносить старі застарілі N-ярусні додатки в.

Чому введення залежності?

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

Чому я взагалі повинен турбуватися про з'єднання?

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

Чому для правильного тестування мені потрібна ін'єкція залежності?

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

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

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

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

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


1
Написання тестів, використовуючи значну частину, але не весь графік залежності, насправді є гарним аргументом для DI.
oberlies

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

0

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

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

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


0

Я думаю, що це велике непорозуміння тут.

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

Цей конструктор:

BillingService(CreditCardProcessor processor, TransactionLog transactionLog)
{
    this.processor = processor;
    this.transactionLog = transactionLog;
}

вже використовує ін'єкційну залежність. Ви в основному просто сказали, що використовувати DI легко.

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

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

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