Розуміння ін'єкції залежності


109

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

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

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

Моє розуміння - це не-DI (з використанням псевдокоду):

public void Start()
{
    MyClass class = new MyClass();
}

...

public MyClass()
{
    this.MyInterface = new MyInterface(); 
}

І DI був би

public void Start()
{
    MyInterface myInterface = new MyInterface();
    MyClass class = new MyClass(myInterface);
}

...

public MyClass(MyInterface myInterface)
{
    this.MyInterface = myInterface; 
}

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


5
Спробуйте мати 5 з MyInterface та MyClass AND з декількома класами, що утворюють ланцюг залежностей. Це стає болем в зад, щоб все налаштувати.
Ейфорія

159
" Для мене це лише передача аргументу. Я, мабуть, неправильно зрозумів суть? " Ти маєш це. Це "ін'єкція залежності". Тепер подивіться, які ще шалені жаргони ви можете придумати для простих концепцій, це весело!
Ерік Ліпперт

8
Я не можу достатньо підтримати коментар містера Ліпперта. Я теж вважав це заплутаним жаргоном для чогось, що я робив природним чином у будь-якому випадку.
Алекс Хамфрі

16
@EricLippert: Хоча це правильно, я думаю, що це дещо зменшує. Параметри як-DI - це не відразу зрозуміла концепція для вашого середнього програміста-імператора / ОО, який звик передавати дані навколо. DI тут дозволяє передати поведінку навколо, що вимагає гнучкішого світогляду, ніж посередній програміст.
Фоші

14
@Phoshi: Ви добре зазначаєте, але ви також поклали палець на те, чому я скептично налаштований, що "ін'єкція залежності" - це найкраща ідея в першу чергу. Якщо я пишу клас, який залежить від його правильності та продуктивності поведінки іншого класу, то останнє, що я хочу зробити, - це зробити так, щоб користувач класу відповідав за правильну побудову та налаштування залежності! Це моя робота. Кожен раз, коли ви дозволяєте споживачеві вводити залежність, ви створюєте точку, коли споживач може зробити це неправильно.
Ерік Ліпперт

Відповіді:


106

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

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


13
Це чудово, добре пояснено. Прийміть уявний +1, оскільки моя представниця ще цього не дозволить!
MyDaftQuestions

11
Складний сценарій побудови - хороший кандидат для використання фабричного зразка. Таким чином, фабрика бере на себе відповідальність за створення об'єкта, так що самому класу цього не потрібно, і ви дотримуєтесь єдиного принципу відповідальності.
Метт Гібсон

1
@MattGibson хороший момент.
Стефан Біллієт

2
+1 для тестування, добре структурована DI допоможе як прискорити тестування, так і зробити одиничні тести, що містяться в класі, який ви випробовуєте.
Umur Kontacı

52

Те, як DI може бути реалізовано, дуже залежить від мови, що використовується.

Ось простий неприкладний приклад:

class Foo {
    private Bar bar;
    private Qux qux;

    public Foo() {
        bar = new Bar();
        qux = new Qux();
    }
}

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

class Foo {
    private Bar bar;
    private Qux qux;

    public Foo(Bar bar, Qux qux) {
        this.bar = bar;
        this.qux = qux;
    }
}

// in production:
new Foo(new Bar(), new Qux());
// in test:
new Foo(new BarMock(), new Qux());

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

Ми можемо ввести більше абстракцій, використовуючи фабрики:

  • Один із варіантів полягає у Fooстворенні абстрактної фабрики

    interface FooFactory {
        public Foo makeFoo();
    }
    
    class ProductionFooFactory implements FooFactory {
        public Foo makeFoo() { return new Foo(new Bar(), new Baz()) }
    }
    
    class TestFooFactory implements FooFactory {
        public Foo makeFoo() { return new Foo(new BarMock(), new Baz()) }
    }
    
    FooFactory fac = ...; // depends on test or production
    Foo foo = fac.makeFoo();
    
  • Ще один варіант - передати завод конструктору:

    interface DependencyManager {
        public Bar makeBar();
        public Qux makeQux();
    }
    class ProductionDM implements DependencyManager {
        public Bar makeBar() { return new Bar() }
        public Qux makeQux() { return new Qux() }
    }
    class TestDM implements DependencyManager {
        public Bar makeBar() { return new BarMock() }
        public Qux makeQux() { return new Qux() }
    }
    
    class Foo {
        private Bar bar;
        private Qux qux;
    
        public Foo(DependencyManager dm) {
            bar = dm.makeBar();
            qux = dm.makeQux();
        }
    }
    

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

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

package Foo {
    use signatures;

    sub new($class, $dm) {
        return bless {
            bar => $dm->{bar}->new,
            qux => $dm->{qux}->new,
        } => $class;
    }
}

my $prod = { bar => 'My::Bar', qux => 'My::Qux' };
my $test = { bar => 'BarMock', qux => 'QuxMock' };
$test->{bar} = 'OtherBarMock';  # change conf at runtime

my $foo = Foo->new(rand > 0.5 ? $prod : $test);

У таких мовах, як Java, я можу змусити менеджера залежностей поводитись подібним чином Map<Class, Object>:

Bar bar = dm.make(Bar.class);

Для якого фактичного класу Bar.classвирішено, можна налаштувати під час виконання, наприклад, підтримуючи, Map<Class, Class>який відображає інтерфейси для реалізацій.

Map<Class, Class> dependencies = ...;

public <T> T make(Class<T> c) throws ... {
    // plus a lot more error checking...
    return dependencies.get(c).newInstance();
}

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

class Foo {
    @Inject(Bar.class)
    private Bar bar;

    @Inject(Qux.class)
    private Qux qux;

    ...
}

dm.make(Foo.class);  // takes care of initializing "bar" and "qux"

Ось приклад реалізації крихітної (і дуже обмеженої) рамки DI: http://ideone.com/b2ubuF , хоча ця реалізація є абсолютно непридатною для незмінних об'єктів (ця наївна реалізація не може приймати жодних параметрів для конструктора).


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

33
Приємно, як кожне наступне спрощення складніше
Кромстер

48

Наші друзі в Stack Overflow мають гарну відповідь на це . Моя улюблена - друга відповідь, включаючи цитату:

"Ін'єкційна залежність" - це термін, що становить 25 доларів, для концепції 5 центів. (...) Введення залежності - це надання об'єкту змінних його примірників. (...).

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


10
Я читав це ... і до цього часу це не мало сенсу ... Тепер це має сенс. Залежність насправді не є такою великою концепцією, яку можна зрозуміти (незалежно від того, наскільки вона потужна), але вона має велике ім'я та багато інтернет-шуму, пов’язаного з нею!
MyDaftQuestions

18

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

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

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

До речі, обговорення DI було б незавершеним без рекомендації щодо введення залежності в .NET від Марка Семана. Якщо ви знайомі з .NET, і ви коли-небудь маєте намір брати участь у масштабній розробці SW, це важливе значення. Він пояснює концепцію набагато краще, ніж я можу.

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

public void Main()
{
    ILightFixture fixture = new ClassyChandelier();
    MyRoom room = new MyRoom (fixture);
}
...
public MyRoom(ILightFixture fixture)
{
    this.MyLightFixture = fixture ; 
}

Але ЗАРАЗ, оскільки MyRoomпризначений для інтерфейсу ILightFixture , я можу легко перейти в наступному році і змінити один рядок у функції Main ("ввести" іншу "залежність"), і я одразу отримую нову функціональність у MyRoom (без необхідності відновіть або перезавантажте MyRoom, якщо трапляється в окремій бібліотеці. Це, звичайно, передбачає, що всі ваші реалізації ILightFixture розроблені з урахуванням заміни Ліскова). Все, за допомогою простої зміни:

public void Main()
{
    ILightFixture fixture = new MuchLessFormalLightFixture();
    MyRoom room = new MyRoom (fixture);
}

Крім того , я можу тепер Test Unit MyRoom(ви це модульне тестування, НЕ так?) І використовувати «помилковий» світильник, який дозволяє мені перевірити MyRoomповністю незалежним відClassyChandelier.

Звичайно, є ще багато переваг, але це ті, які продали мені ідею.


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

2
Мене також цікавить книга, на яку ви посилаєтесь, але мені здається тривожною, що на цю тему є книга на 584 сторінки.
Стів

4

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

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

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

Крім того, деякі труднощі в реальному впровадженні ІР виявляються краще у великих проектах. Ви, як правило, перенесите фактичне рішення, які конкретні класи використовувати весь шлях до верху, щоб ваш метод запуску міг виглядати так:

public void Start()
{
    MySubSubInterfaceA mySubSubInterfaceA = new mySubSubInterfaceA();
    MySubSubInterfaceB mySubSubInterfaceB = new mySubSubInterfaceB();     
    MySubInterface mySubInterface = new MySubInterface(mySubSubInterfaceA,mySubSubInterfaceB);
    MyInterface myInterface = new MyInterface(MySubInterface);
    MyClass class = new MyClass(myInterface);
}

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

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

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


Це багато допоміжних інтерфейсів і інтерфейсів! Ви маєте на увазі новий інтерфейс замість класу, який реалізує інтерфейс? Здається, неправильно.
JBRWilkinson

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

4

На рівні класу це легко.

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

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

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

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

І це добре.

На рівні програми ...

Стільки для перегляду речей у класі. Скажімо, у вас є купа занять, які слідують правилу "не інстанціювати власних співробітників", і хочете зробити заявку на них. Найпростіша річ - скористатися старим хорошим кодом (дійсно корисним інструментом для виклику конструкторів, властивостей та методів!), Щоб скласти потрібний графік об'єкта та вкинути на нього деякий вклад. (Так, деякі з цих об’єктів у вашому графіку самі будуть об'єктами-фабриками, які передані в якості співпрацівників іншим довгоживучим об'єктам у графіку, готові до експлуатації ... ви не можете попередньо сконструювати кожен об’єкт! ).

... ваша потреба "гнучко" налаштувати об'єкт-графік вашого додатка ...

Залежно від інших ваших цілей (не пов'язаних з кодом), ви можете надати кінцевому користувачеві деякий контроль над таким чином розгорнутим графіком об'єкта. Це призводить вас до тієї чи іншої схеми конфігурації, будь то текстовий файл вашого власного дизайну з деякими парами імен / значень, XML-файл, спеціальна DSL, декларативна мова опису графа, наприклад YAML - вкрай необхідна мова сценаріїв, наприклад, JavaScript або щось інше, відповідне завданням. Що б не потрібно було скласти дійсний графік об'єкта таким чином, щоб відповідати потребам ваших користувачів.

... може бути значною конструкторською силою.

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

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

Ти бідна дерно!

Оскільки ви не хотіли писати все це самостійно (але серйозно, ознайомтеся з прив’язкою YAML для своєї мови), ви використовуєте якісь рамки DI.

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

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

Завантаження

Ні в якому разі ви ніколи не використовуєте Google Guice, який дає кодеру спосіб позбутися завдання складання об'єкт-графіка з кодом ..., написавши код. Арг!


0

Тут багато людей говорили, що DI - це механізм вихідної ініціалізації змінних екземплярів.

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

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

Наприклад: підключення до бази даних, контекст безпеки, засоби реєстрації, файл конфігурації тощо.

Більше того, в цілому кожен сингтон є хорошим кандидатом в DI. Чим більше їх ви маєте та використовуєте, тим більше шансів на те, що вам потрібно.

Для таких видів ресурсів цілком зрозуміло, що не має сенсу переміщувати їх навколо списків параметрів, і тому DI був винайдений.

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


-2

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


-4

Це ще один варіант на додаток до відповіді Амона

Використовуйте будівельники:

клас Foo {
    приватний бар-бар;
    приватний Qux qux;

    приватний Foo () {
    }

    public Foo (Builder builder) {
        this.bar = builder.bar;
        this.qux = builder.qux;
    }

    громадський статичний клас Builder {
        приватний бар-бар;
        приватний Qux qux;
        громадський Buider setBar (Бар-бар) {
            this.bar = bar;
            повернути це;
        }
        публічний набір BuilderQQ (Qux qux) {
            this.qux = qux;
            повернути це;
        }
        public Foo build () {
            повернути новий Foo (це);
        }
    }
}

// у виробництві:
Foo foo = новий Foo.Builder ()
    .setBar (новий бар ())
    .setQux (новий Qux ())
    .build ();

// в тесті:
Foo testFoo = новий Foo.Builder ()
    .setBar (новий BarMock ())
    .setQux (новий Qux ())
    .build ();

Це те, що я використовую для залежностей. Я не використовую жодної рамки DI. Вони заважають.


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

@Snowman Це інший варіант замість DI. Мені шкода, що ти не бачиш це так. Дякуємо за прихильник голосування.
користувач3155341

1
запитуючий хотів зрозуміти, чи справді DI так просто, як передавати параметр, він не просить альтернативи DI.

1
Приклади ОП дуже хороші. Ваші приклади забезпечують більшу складність для чогось, що є простою ідеєю. Вони не ілюструють питання цього питання.
Адам Цукерман

Конструктор DI передає параметр. За винятком того, що ви передаєте об'єкт інтерфейсу, а не конкретний об'єкт класу. Саме ця "залежність" від конкретної реалізації вирішується через DI. Конструктор не знає, яким типом він передається, і це не хвилює, він просто знає, що отримує тип, який реалізує інтерфейс. Я пропоную PluralSight, він має чудовий курс для початківців DI / IOC.
Хардграф
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.