Ін'єкційна залежність; передовий досвід зменшення кодового коду


25

У мене просте запитання, і я навіть не впевнений, що він має відповідь, але спробуємо. Я кодую C ++ і використовую ін'єкцію залежностей, щоб уникнути глобального стану. Це працює досить добре, і я не часто зустрічаюсь з несподіваною / невизначеною поведінкою.

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

Ніщо не є добрим прикладом, тому давайте:

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

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

У моєму додатку потрібно багато класів, щоб створити об’єкти часу. Іноді ці заняття глибоко вкладені.

Скажімо, у мене є клас A, який містить екземпляри класу B, і так далі до класу D. Класу D потрібно створити об'єкти часу.

Під час наївної реалізації я передаю TimeFactory конструктору класу A, який передає його конструктору класу В і так далі до класу D.

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

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

Як ти гадаєш ?


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

2
аспектно-орієнтоване програмування, також добре підходить для виконання цих завдань - en.wikipedia.org/wiki/Aspect-oriented_programming
Е.Л. Юсубов

Чи є завод класу A для класів B, C і D? Якщо ні, то чому він створює їх екземпляри? Якщо тільки класу D потрібні часові об'єкти, то чому б ви ввели будь-який інший клас за допомогою TimeFactory? Чи дійсно потрібен клас D TimeFactory, чи може до нього бути введений просто примірник часу?
simoraman

D потрібно створювати об'єкти часу, з інформацією, яку повинен мати лише D. Я повинен подумати над рештою вашого коментаря. Можливо, у моєму коді
виникне

"Хто створює кого" вирішується контейнерами IoC, детально дивіться мою відповідь. "Хто створює хто" - одне з найкращих питань, які ви можете задати, використовуючи Dependency Injection.
GameDeveloper

Відповіді:


23

У моєму додатку потрібно багато класів, щоб створити об’єкти часу

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

Тому моя пропозиція: не використовуйте DI для таких типів даних, як Time. Напишіть тестові одиниці для самого Timeкласу, і коли це буде зроблено, використовуйте його скрізь у своїй програмі, як і stringклас, або vectorклас чи будь-який інший клас стандартної lib. Використовуйте DI для компонентів, які повинні бути дійсно відокремлені один від одного.


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

3
@Dinaiz: ідея полягає в тому, щоб прийняти тісний зв'язок між Timeіншими частинами вашої програми. Таким чином, ви можете прийняти також прийняти жорстку зв'язок TimeFactory. Однак я б уникнув, коли є єдиний глобальний TimeFactoryоб'єкт зі станом (наприклад, інформація про локаль або щось подібне) - це може спричинити неприємні побічні ефекти для вашої програми та зробити загальне тестування та повторне використання дуже важким. Або зробіть це без громадянства, або не використовуйте його як одиноку.
Док Браун

Насправді він повинен мати стан, тому, коли ви говорите "Не використовуйте його як синглтон", ви маєте на увазі "продовжуйте використовувати ін'єкцію залежності, тобто передайте екземпляр кожному об'єкту, який йому потрібен", правда?
Дінаїз

1
@Dinaiz: чесно, це залежить - від типу штату, від виду та кількості частин вашої програми, що використовуються, Timeта TimeFactoryвід ступеня "еволюційності", яка вам потрібна для майбутніх розширень, пов’язаних із TimeFactoryтощо.
Док Браун

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

4

Що ви маєте на увазі під "я втрачаю всю гнучкість і читабельність, яку я, мабуть, використовую для введення залежності" - DI не стосується читабельності. Йдеться про розв'язку залежності між об'єктами.

Це здається, що у вас клас A створює клас B, клас B створює клас C і клас C створює клас D.

Що ви повинні мати - це клас B, який вводять до класу A. Клас C, який вводять до класу B. Клас D, який вводять до класу C.


DI може стосуватися читабельності. Якщо у вас є глобальна змінна, що "магічно" з'являється в середині методу, це не допоможе зрозуміти код (з точки зору обслуговування). Звідки ця змінна? Кому належить? Хто його ініціалізує? І так далі ... Що стосується вашого другого пункту, ви, мабуть, праві, але я не думаю, що це стосується мого випадку. Дякуємо, що
знайшли

DI збільшує читальність головним чином через те, що "new / delete" буде магічно викреслено з вашого коду. Якщо вам не доведеться попередити про динамічний код розподілу, це легше читати.
GameDeveloper

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

3

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

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

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


Весна - для Java, і я використовую C ++. Але чому 2 людини зголосилися на вас? У вашому дописі є моменти, з якими я не згоден, але все ж ...
Дінаїз

Я припускаю, що голосування було пов'язане з тим, що я не відповів на це запитання як такий, можливо, як і згадка про Весну. Однак пропозиція використовувати Singleton була моєю на думку, і може не відповісти на запитання, але пропонує вагому альтернативу і викликає цікаве дизайнерське питання. З цієї причини я поставив +1.
Fergus In London

1

Це має на меті бути доповнюючою відповіддю Дока Брауна, а також відповісти на коментарі Дінаїза без відповіді, які все ще пов'язані з Питанням.

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

Одинокий? Ні, дякую. Якщо вам потрібен лише один екземпляр, щоб він поділився у вашому контексті програми (Використання контейнера IoC для DI, як Infector ++, потрібно просто прив’язати TimeFactory як єдиний екземпляр), ось приклад (C ++ 11, до речі, але так C ++. Можливо, перемістіть до C ++ 11? Ви безкоштовно отримуєте заявку на герметичність):

Infector::Container ioc; //your app's context

ioc.bindSingleAsNothing<TimeFactory>(); //declare TimeFactory to be shared
ioc.wire<TimeFactory>(); //wire its constructor 

// if you want to be sure TimeFactory is created at startup just request it
// (else it will be created lazily only when needed)
auto myTimeFactory = ioc.buildSingle<TimeFactory>();

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

ioc.bindAsNothing<A>(); //declare class A
ioc.bindAsNothing<B>(); //declare class B
ioc.bindAsNothing<D>(); //declare class D

//constructors setup
ioc.wire<D, TimeFactory>(); //time factory injected to class D
ioc.wire<B, D>(); //class D injected to class B
ioc.wire<A, B>(); //class B injected to class A

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

auto myA1 = ioc.build<A>(); //A is not "single" so many different istances
auto myA2 = ioc.build<A>(); //can live at same time

щоразу, коли ви створюєте клас A, він буде автоматично (лінива старість) вводитися всіма залежностями до D і D, буде вводитися TimeFactory, тому, викликаючи лише 1 метод, ви готові вашу повну ієрархію (і навіть складні ієрархії вирішуються таким чином видалення ВЕЛИКОГО коду пластини котла): Вам не потрібно викликати "new / delete", і це дуже важливо, оскільки ви можете відокремити логіку програми від коду клею.

D може створювати об'єкти часу з інформацією, яку може мати тільки D

Це просто, ваш TimeFactory має метод "create", тоді просто використовуйте інший підпис "create (params)" і все закінчено. Параметри, які не мають залежності, часто вирішуються таким чином. Це також знімає обов'язок введення речей, таких як "рядки" або "цілі числа", оскільки це просто додає додаткову плиту котла.

Хто створює кого? Контейнер IoC створює інстанції та фабрики, фабрики створюють решту (фабрики можуть створювати різні об'єкти з довільними параметрами, тож вам не потрібно штат для заводів). Ви все ще можете використовувати фабрики як обгортки для контейнера IoC: Взагалі Injectin IoC Container дуже поганий і такий же, як і для використання сервісного локатора. Деякі люди вирішили проблему, обернувши контейнер IoC з фабрикою (це не обов'язково, але це перевага, що ієрархія вирішується контейнером, і всі ваші фабрики стають ще простішими в обслуговуванні).

//factory method
std::unique_ptr<myType> create(params){
    auto istance = ioc->build<myType>(); //this code's agnostic to "myType" hierarchy
    istance->setParams(params); //the customization you needed
    return std::move(istance); 
}

Також не зловживайте ін'єкційними залежностями, прості типи можуть бути просто членами класу або локальними змінними. Це здається очевидним, але я бачив, як люди вводять "std :: vector" лише тому, що існувала рамка DI, яка це дозволяла. Завжди пам’ятайте про закон Деметра: «Впорскуйте лише те, що вам справді потрібно ввести»


Нічого раніше, я не бачив вашої відповіді, вибачте! Дуже інформативний пане! Upvoted
Дінаїз

0

Чи потрібно також вашим класам A, B і C створювати екземпляри часу або лише клас D? Якщо це лише клас D, то A і B нічого не повинні знати про TimeFactory. Створіть екземпляр TimeFactory всередині класу C та передайте його до класу D. Зауважте, що під "створенням екземпляра" я не обов'язково маю на увазі, що клас C повинен нести відповідальність за інстанціювання TimeFactory. Він може отримувати DClassFactory з класу B, а DClassFactory вміє створювати екземпляр Time.

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


-1

Я реалізував ще одну структуру введення залежності C ++, яку нещодавно було запропоновано для підвищення - https://github.com/krzysztof-jusiak/di - бібліотека макро менше (безкоштовно), лише заголовок, C ++ 03 / C ++ 11 / C ++ 14 бібліотека, що забезпечує безпечний тип, час компіляції, введення залежності макро-вільного конструктора.


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