Чому один користувач використовує ін'єкцію залежності?


536

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

Я не зовсім розумію, яку проблему вона вирішує. Це виглядає як спосіб сказати: "привіт. Коли ви запустите цю функцію, поверніть об'єкт такого типу і використовує ці параметри / дані".
Але ... чому я б коли-небудь цим користувався? Зауважте, мені ніколи не потрібно було користуватися object, але я розумію, для чого це потрібно.

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


3
Також це може бути корисною інформацією: martinfowler.com/articles/injection.html
ta.speot.is




5
Ще одне дуже просте пояснення DI: codearsenal.net/2015/03/…
ybonda

Відповіді:


840

По-перше, я хочу пояснити припущення, яке я висловлюю за цю відповідь. Це не завжди так, але досить часто:

Інтерфейси - прикметники; класи - це іменники.

(Насправді, є і інтерфейси, які є іменниками, але я хочу тут узагальнити.)

Так, наприклад, інтерфейс може бути чимось таким, як IDisposable, IEnumerableабо IPrintable. Клас - це реальна реалізація одного або декількох цих інтерфейсів: Listабо вони Mapможуть бути реалізацією IEnumerable.

Щоб зрозуміти: Часто ваші заняття залежать один від одного. Наприклад, у вас може бути Databaseклас, який має доступ до вашої бази даних (так, сюрприз! ;-)), але ви також хочете, щоб цей клас робив журнал щодо доступу до бази даних. Припустимо, у вас є інший клас Logger, то Databaseмає залежність від Logger.

Все йде нормально.

Ви можете змоделювати цю залежність у своєму Databaseкласі за допомогою наступного рядка:

var logger = new Logger();

і все добре. Це добре до дня, коли ви зрозумієте, що вам потрібна купа реєстраторів: Іноді ви хочете увійти до консолі, іноді до файлової системи, іноді за допомогою TCP / IP та віддаленого сервера реєстрації і так далі ...

І звичайно, ви НЕ хочете змінювати весь код (тим часом у вас є gazillions) і замінювати всі рядки

var logger = new Logger();

автор:

var logger = new TcpLogger();

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

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

ICanLog logger = new Logger();

Тепер умовивід типу більше не змінює тип, у вас завжди є один єдиний інтерфейс, на який потрібно розробитись. Наступним кроком є ​​те, що ви не хочете мати new Logger()знову і знову. Таким чином, ви покладете надійність для створення нових примірників до одного, центрального фабричного класу, і ви отримаєте такий код, як:

ICanLog logger = LoggerFactory.Create();

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

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

ICanLog logger = TypeFactory.Create<ICanLog>();

Десь цей TypeFactory потребує даних конфігурації, фактичний клас для інстанціювання, коли запитується певний тип інтерфейсу, тому вам потрібне відображення. Звичайно, ви можете зробити це зіставлення всередині коду, але тоді зміна типу означає перекомпіляцію. Але ви також можете помістити це відображення у XML-файл, наприклад. Це дозволяє змінити фактично використаний клас навіть після часу компіляції (!), Що означає динамічно, без перекомпіляції!

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

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

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

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

Що Databaseстосується ін'єкцій залежностей, ваш клас тепер має конструктор, який вимагає параметра типу ICanLog:

public Database(ICanLog logger) { ... }

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

І ось тут починає діяти рамка DI: Ви знову налаштовуєте свої зіставлення, а потім просите рамки DI, щоб вони запровадили вашу програму. Оскільки Applicationклас вимагає ICanPersistDataреалізації, Databaseвводиться екземпляр, але для цього він повинен спершу створити екземпляр типу реєстратора, для якого налаштовано ICanLog. І так далі ...

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

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

Але в основному, це все.

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


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

137
Ось що я ніколи не замислююся про DI: це робить архітектуру значно складнішою. І все-таки, як я бачу, використання досить обмежене. Приклади, безумовно, завжди однакові: взаємозамінні реєстратори, взаємозамінна модель / доступ до даних. Іноді взаємозамінний вигляд. Але це все. Чи справді ці кілька випадків виправдовують значно складнішу архітектуру програмного забезпечення? - Повне розкриття: я вже використовував DI для чудового ефекту, але це було для дуже особливої ​​архітектури плагінів, з якої я б не узагальнював.
Конрад Рудольф

17
@GoloRoden, чому ви викликаєте інтерфейс ICanLog замість ILogger? Я працював з іншим програмістом, який часто робив це, і я ніколи не міг зрозуміти конвенцію? Для мене це як дзвонити IEnumerable ICanEnumerate?
DermFrench

28
Я назвав це ICanLog, тому що ми занадто часто працюємо зі словами (іменниками), які нічого не означають. Наприклад, що таке Брокер? Менеджер? Навіть сховище не визначено унікальним чином. І мати всі ці речі як іменники є типовою хворобою мов ОО (див. Steve-yegge.blogspot.de/2006/03/… ). Що я хочу висловити, це те, що у мене є компонент, який може вести журнал для мене - так чому б не назвати його таким чином? Звичайно, це також грає з Я як перша людина, отже ICanLog (ForYou).
Голо Роден

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

499

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

Ін'єкційна залежність - дуже проста концепція. Замість цього коду:

public class A {
  private B b;

  public A() {
    this.b = new B(); // A *depends on* B
  }

  public void DoSomeStuff() {
    // Do something with B here
  }
}

public static void Main(string[] args) {
  A a = new A();
  a.DoSomeStuff();
}

ви пишете код так:

public class A {
  private B b;

  public A(B b) { // A now takes its dependencies as arguments
    this.b = b; // look ma, no "new"!
  }

  public void DoSomeStuff() {
    // Do something with B here
  }
}

public static void Main(string[] args) {
  B b = new B(); // B is constructed here instead
  A a = new A(b);
  a.DoSomeStuff();
}

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

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


7
Чому я віддаю перевагу другому коду, а не першому? Перший має лише нове ключове слово, як це допомагає?
user962206

17
@ user962206 подумайте, як би ви протестували A незалежно від B
jk.

66
@ user962206, також подумайте про те, що буде, якби B потрібні певні параметри в його конструкторі: щоб інстанціювати це, A мав би знати про ці параметри, те, що може бути абсолютно не пов'язане з A (воно просто хоче залежати від B , а не від того, від чого залежить В). Передача вже побудованого B (або будь-якого підкласу чи макету B з цього питання) конструктору A вирішує це і робить A залежним лише від B :)
епідемія

17
@ acidzombie24: Як і багато моделей дизайну, DI не дуже корисний, якщо ваша кодова база не є достатньо великою, щоб простий підхід став проблемою. Я відчуваю, що DI насправді не буде поліпшенням, поки у вашої програми не буде більше 20 000 рядків коду та / або більше 20 залежностей від інших бібліотек чи рамок. Якщо ваша програма менше, ніж ви, можливо, ви все ще віддасте перевагу програмуванню в стилі DI, але різниця не буде майже такою драматичною.
Даніель Приден

2
@DanielPryden Я не думаю, що розмір коду має значення стільки, скільки динамічний ваш код. якщо ви регулярно додаєте нові модулі, що відповідають одному інтерфейсу, вам не доведеться міняти залежний код так часто.
FistOfFury

35

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

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

Моя особливість:

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

Ви можете перевірити це так:

[Test]
public void ShouldUpdateTimeStamp
{
    // Arrange
    var formdata = { . . . }

    // System under Test
    var weasel = new OfferWeasel();

    // Act
    var offer = weasel.Create(formdata)

    // Assert
    offer.LastUpdated.Should().Be(new DateTime(2013,01,13,13,01,0,0));
}

Тож десь із програми OfferWeasel, він створює вам такий об’єкт пропозиції:

public class OfferWeasel
{
    public Offer Create(Formdata formdata)
    {
        var offer = new Offer();
        offer.LastUpdated = DateTime.Now;
        return offer;
    }
}

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

public interface IGotTheTime
{
    DateTime Now {get;}
}

public class CannedTime : IGotTheTime
{
    public DateTime Now {get; set;}
}

public class ActualTime : IGotTheTime
{
    public DateTime Now {get { return DateTime.Now; }}
}

public class OfferWeasel
{
    private readonly IGotTheTime _time;

    public OfferWeasel(IGotTheTime time)
    {
        _time = time;
    }

    public Offer Create(Formdata formdata)
    {
        var offer = new Offer();
        offer.LastUpdated = _time.Now;
        return offer;
    }
}

Інтерфейс - це абстракція. Одне - РЕАЛЬНА річ, а інше - дозволяє підробити деякий час там, де це потрібно. Потім тест можна змінити так:

[Test]
public void ShouldUpdateTimeStamp
{
    // Arrange
    var date = new DateTime(2013, 01, 13, 13, 01, 0, 0);
    var formdata = { . . . }

    var time = new CannedTime { Now = date };

    // System under test
    var weasel= new OfferWeasel(time);

    // Act
    var offer = weasel.Create(formdata)

    // Assert
    offer.LastUpdated.Should().Be(date);
}

Так, ви застосували принцип "інверсії управління", ввівши залежність (отримуючи поточний час). Основною причиною цього є легше ізольоване тестування одиниць, є й інші способи зробити це. Наприклад, інтерфейс і клас тут непотрібні, оскільки в C # функції можна передавати як змінні, тож замість інтерфейсу ви можете використовувати a Func<DateTime>для досягнення того ж. Або, якщо ви користуєтесь динамічним підходом, ви просто передаєте будь-який об’єкт, що має еквівалентний метод ( набирання качок ), і інтерфейс вам взагалі не потрібен.

Навряд чи вам колись знадобиться більше одного лісоруба. Тим не менш, введення залежності є важливим для статично введеного коду, такого як Java або C #.

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

Я сподіваюся, що це допомогло.


4
Це насправді виглядає як жахливе рішення. Я б точно написав код більше, як пропонують відповідь Даніеля Прийдена , але для цього конкретного тестового одиниці я б просто зробив DateTime.Now до і після функції і перевірити, чи час між ними? Додавання більше інтерфейсів / набагато більше рядків коду здається мені поганою ідеєю.

3
Мені не подобаються загальні приклади A (B), і я ніколи не відчував, що для реєстратора потрібно мати 100 реалізацій. Це приклад, з яким я нещодавно стикався, і це один із 5 способів вирішити його, де насправді включено за допомогою PostSharp. Це ілюструє класичний підхід до впорскування двигуна в кластерний клас. Не могли б ви надати кращий приклад реального світу, де ви зіткнулися з користю для DI?
цессор

2
Я ніколи не бачив хорошого використання для DI. Ось чому я написав питання.

2
Я не вважаю це корисним. Мій код завжди легко зробити тестом. Здається, DI добре підходить для великих кодів із поганим кодом.

1
Майте на увазі, що навіть у невеликих функціональних програмах, коли у вас є f (x), g (f), ви вже використовуєте ін'єкцію залежності, тому кожне продовження в JS вважатиметься введенням залежності. Я здогадуюсь, що ви вже користуєтесь ним;)
цессор

15

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

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

class PaymentProcessor{

    private String type;

    public PaymentProcessor(String type){
        this.type = type;
    }

    public void authorize(){
        if (type.equals(Consts.PAYPAL)){
            // Do this;
        }
        else if(type.equals(Consts.OTHER_PROCESSOR)){
            // Do that;
        }
    }
}

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

class PaypalProcessor implements PaymentProcessor{

    public void authorize(){
        // Do PayPal authorization
    }
}

class OtherProcessor implements PaymentProcessor{

    public void authorize(){
        // Do other processor authorization
    }
}

class PaymentFactory{

    public static PaymentProcessor create(String type){

        switch(type){
            case Consts.PAYPAL;
                return new PaypalProcessor();

            case Consts.OTHER_PROCESSOR;
                return new OtherProcessor();
        }
    }
}

interface PaymentProcessor{
    void authorize();
}

** Код не збирається, я знаю :)


+1, оскільки це здається, що ви говорите, що вам це потрібно там, де ви використовуєте віртуальні методи / інтерфейси. Але це все ще рідко. Я б все-таки передав це як new ThatProcessor()радше, а потім використовувати фреймворк

@ItaiS Ви можете уникнути незліченної кількості комутаторів із заводським дизайном класу. Використовуйте відображення System.Reflection.Assembly.GetExecutingAssembly (). CreateInstance ()
domenicr

@domenicr звичайно! але я хотів пояснити це на спрощеному прикладі
Ітаї Сагі

Я згоден з вищезазначеним поясненням, за винятком потреби фабричного класу. У той момент, коли ми впроваджуємо фабричний клас, це його просто непростий вибір. Найкраще пояснення вище, яке я знайшов у розділі Пойморфізму та віртуальної функції Брюса Еркеля. Справжній DI повинен бути вільним від вибору, а тип об'єкта повинен визначатися під час виконання через інтерфейс автоматично. Це теж справжня поліморфна поведінка.
Арвінд Крмар

Наприклад (за c ++) у нас є спільний інтерфейс, який бере лише посилання на базовий клас і реалізує поведінку його класу виведення без вибору. анульована мелодія (Instrument & i) {i.play (middleC); } int main () {Вітер флейта; мелодія (флейта); } Інструмент - базовий клас, з нього походить вітер. Відповідно до c ++, віртуальна функція дає можливість реалізувати поведінку похідного класу через загальний інтерфейс.
Арвінд Крмар

6

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

Це вже часто зустрічається в ООП. Багато разів створюючи фрагменти коду, як-от:

I_Dosomething x = new Impl_Dosomething();

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

I_Dosomething x = ServiceLocator.returnDoing(String pKey);

Приклад DI:

I_Dosomething x = DIContainer.returnThat();

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

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

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


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

@ acidzombie24 Може бути ... але порівняйте зусилля щодо впровадження рішення, використовуючи DI від початку, і зусилля, щоб пізніше змінити рішення, яке не стосується DI, якщо вам потрібні інтерфейси. Я майже завжди йшов із першим варіантом. Краще зараз заплатити 100 $, а не завтра платити 100 000 $.
Голо Роден

1
@GoloRoden Дійсно, обслуговування є ключовою проблемою з використанням таких методів, як D (b) I. Це 80% витрат на заявку. Конструкція, в якій необхідна поведінка буде чітко виражена за допомогою інтерфейсів із самого початку, економить організацію пізніше на багато часу та грошей.
Loek Bergman

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