Потрібна ін'єкція залежності залежно від інкапсуляції?


128

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

Це виявляє залежність, яка вводиться, і порушує принцип ООП інкапсуляції.

Чи правильно я виявляю цей компроміс? Як ви вирішуєте це питання?

Будь ласка, дивіться також мою відповідь на моє власне запитання нижче.


7
це дуже розумне запитання imho
dfa

5
Відповідь на це питання спочатку вимагає аргументу про те, що означає інкапсуляція . ;)
Jeff Sternal

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

Відповіді:


62

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

Коли ми використовуємо ін'єкцію IoC / залежності, ми не використовуємо концепції OOP. Справді, ми використовуємо мову OO як "хоста", але ідеї, що стоять за IoC, походять від компонентно-орієнтованої програмної інженерії, а не від OO.

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

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

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


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

1
Аргументи, що надходять через конструктор, все ще підпадають під сферу очікуваних способів, коли об'єкт може бути "змінений": вони явно піддаються впливу, а інваріанти навколо них застосовуються. Приховування інформації є кращим терміном для типу конфіденційності, на яку ви посилаєтесь @RonInbar, і це не завжди завжди вигідно (це робить макарони складніше розплутати ;-)).
Ніколас Блюмхардт

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

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

1
Ось ще один спосіб вирішити цю проблему: уявіть, якби .NET збірки обрали "інкапсуляцію" і не заявили, на які інші збірки вони покладалися. Це було б шаленою ситуацією, читаючи документи та просто сподіваючись, що після завантаження щось працює. Декларування залежностей на цьому рівні дозволяє автоматизованому інструментуванню вирішувати масштабний склад програми. Ви повинні коситись, щоб побачити аналогію, але подібні сили діють на рівні компонентів. Є компроміси, і YMMV, як завжди :-)
Ніколас Блюмхардт

29

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

Класично цей останній випадок обробляється, як ви кажете, за допомогою аргументів конструктора або методів встановлення. Однак це не обов'язково вірно - я знаю, що останні версії фреймворку Spring DI в Java, наприклад, дозволяють вам анотувати приватні поля (наприклад, з @Autowired), і залежність буде встановлена ​​за допомогою відображення, не потребуючи викриття залежності через будь-який із класів публічних методів / конструкторів. Це може бути таке рішення, яке ви шукали.

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

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

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


2
Хороші бали. Я раджу не використовувати @Autowired на приватних полях; що робить клас важким для тестування; як тоді вводити макети або заглушки?
грудочка

4
Я не погоджуюсь. DI порушує інкапсуляцію, і цього можна уникнути. Наприклад, використовуючи ServiceLocator, якому, очевидно, не потрібно нічого знати про клієнтський клас; йому потрібно знати лише про реалізацію залежності Foo. Але найкраще, в більшості випадків, просто використовувати «нового» оператора.
Rogério

5
@Rogerio - імовірно, будь-яка рамка DI діє точно так само, як описаний вами ServiceLocator; клієнт не знає нічого конкретного щодо реалізації Foo, а інструмент DI не знає нічого конкретного про клієнта. А використання "нового" набагато гірше для порушення інкапсуляції, оскільки потрібно знати не тільки точний клас реалізації, але й точні класи та екземпляри всіх залежностей, які йому потрібні.
Анджей Дойл

4
Використання "нового" для створення класу помічників, який часто навіть не є загальнодоступним, сприяє інкапсуляції. Альтернативою DI було б оприлюднити клас помічника та додати публічний конструктор чи сетер у клієнтський клас; обидві зміни можуть порушити інкапсуляцію, яку надає початковий клас помічників.
Rogério

1
"Це гарне запитання - але в якийсь момент капсуляцію в найчистішому вигляді потрібно порушувати, якщо об'єкт коли-небудь виконуватиме свою залежність". Основа вашої відповіді просто не відповідає дійсності. Як @ Rogério стверджує, що внутрішнє налаштування залежності, і будь-який інший метод, коли об'єкт внутрішньо задовольняє власні залежності, не порушує інкапсуляції.
Нейтрино,

17

Це виявляє залежність, яка вводиться, і порушує принцип ООП інкапсуляції.

Ну, чесно кажучи, все порушує інкапсуляцію. :) Це свого роду тендерний принцип, до якого треба добре ставитися.

Отже, що порушує інкапсуляцію?

Спадщина робить .

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

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

Декларація про друзів у C ++ робить .

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

Ну, багато чого робить .

Інкапсуляція - це хороший і важливий принцип. Але не єдиний.

switch (principle)
{
      case encapsulation:
           if (there_is_a_reason)
      break!
}

3
"Це мої принципи, і якщо вони вам не подобаються ... ну, я маю інших". (Грушо Маркс)
Рон Інбар

2
Я думаю, що це свого роду суть. Це залежна від ін'єкції проти інкапсуляції. Тож використовуйте залежну ін'єкцію лише там, де це дає значні переваги. Саме ДІ скрізь дає ДІ погане ім’я
Річард Тінгл

Не впевнений, що ця відповідь намагається сказати ... Що нормально порушувати інкапсуляцію при виконанні DI, що це "завжди добре", оскільки це все одно було б порушено, або просто, що DI може бути причиною порушення інкапсуляції? З іншого боку, в ці дні більше не потрібно покладатися на громадські конструктори чи властивості, щоб ввести залежності; натомість ми можемо вводити в приватні анотовані поля , що простіше (менше коду) і зберігає інкапсуляцію. Отже, ми можемо скористатися обома принципами одночасно.
Rogério

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

13

Так, DI порушує інкапсуляцію (також відомий як "приховування інформації").

Але справжня проблема виникає тоді, коли розробники використовують це як привід для порушення принципів KISS (Keep It Short and Simple) та YAGNI (You Ant't Gonna Need It).

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


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

5

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

Тепер я припускаю, що ви порівнюєте проти випадку, коли створений об’єкт створює власні інкапсульовані об'єкти, швидше за все, у своєму конструкторі. Моє розуміння ДП полягає в тому, що ми хочемо зняти цю відповідальність з об'єкта і передати її комусь іншому. З цією метою "хтось інший", який є контейнером DP в даному випадку, має інтимні знання, які "порушують" інкапсуляцію; користь полягає в тому, що вона витягує це знання з об'єкта, сама. Хтось повинен це мати. Інша частина вашої заяви не відповідає.

Я б подумав про це так: Контейнер / система для ін'єкційних залежностей порушує інкапсуляцію, але ваш код цього не робить. Насправді ваш код більше "капсульований", ніж будь-коли.


3
Якщо у вас виникає ситуація, коли клієнтський об’єкт МОЖЕ безпосередньо запровадити свої залежності, то чому б цього не зробити? Це, безумовно, найпростіша річ, і це не обов'язково знижує заповітність. Окрім простоти та кращої інкапсуляції, це також полегшує наявність стаціонарних об'єктів замість одиниць без громадянства.
Rogério

1
Додавши до того, що @ Rogério сказав, що це також може бути значно ефективнішим. Не кожному класу, створеному за всю історію світу, потрібно було встановити кожну його залежність протягом усього життя об'єкта, що володіє ним. Об'єкт, що використовує DI, втрачає найосновніший контроль над власними залежностями, а саме своєю тривалістю життя.
Нейтрино

5

Як зазначив Джефф Стернал у коментарі до питання, відповідь повністю залежить від того, як ви визначаєте інкапсуляцію .

Здається, є два основні табори того, що означає інкапсуляція:

  1. Все, що стосується об'єкта, - це метод на об’єкті. Таким чином, Fileоб'єкт може мати методи для Save, Print, Display, ModifyTextі т.д.
  2. Об'єкт - це власний маленький світ, і не залежить від поведінки зовні.

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

Отже, ін'єкція залежності призведе до порушення інкапсуляції, якщо використовувати перше визначення. Але, чесно кажучи, я не знаю, чи мені подобається перше визначення - воно явно не масштабує (якби це було, MS Word був би одним великим класом).

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


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

4

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


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

3
Я думаю, ви неправильно розумієте інкапсуляцію. Наприклад, візьміть наївний клас побачень. Всередині нього можуть бути змінні екземпляри день, місяць та рік. Якби їх виставляли як прості сетери без логіки, це може порушити інкапсуляцію, оскільки я міг би зробити щось на зразок місяця до 2 та дня до 31. З іншого боку, якщо сетери розумні і перевіряють інваріанти, то все добре . Також зауважте, що в останній версії я міг змінити сховище на дні з 1.01.1970 року, і нічого, що використовувало інтерфейс, не слід знати про це, якщо я належним чином переписав методи день / місяць / рік.
Джейсон Уоткінс

2
DI безумовно порушує приховування / приховування інформації. Якщо ви перетворите приватну внутрішню залежність на щось, відкрите в публічному інтерфейсі класу, то за визначенням ви порушили інкапсуляцію цієї залежності.
Rogério

2
Я маю конкретний приклад, коли я думаю, що інкапсуляція є компрометованою DI. У мене є FooProvider, який отримує "дані foo" від БД, і FooManager, який кешує його і обчислює речі на верхній частині постачальника. У мене споживачі свого коду помилково переходили до FooProvider для даних, де я вважав за краще інкапсульований, щоб вони знали лише FooManager. Це, в основному, тригер для мого оригінального питання.
уріг

1
@Rogerio: Я б заперечував, що конструктор не є частиною загальнодоступного інтерфейсу, оскільки він використовується лише в корені композиції. Таким чином, залежність "бачить" лише корінь складу. Корінь відповідальності єдиного складу полягає в тому, щоб з'єднати ці залежності разом. Тож використання конструкторської ін'єкції не порушує жодної інкапсуляції.
Джей Салліван

4

Це схоже на схвалену відповідь, але я хочу подумати вголос - можливо, інші бачать і так.

  • Класичний OO використовує конструктори для визначення публічного договору про "ініціалізацію" для споживачів класу (приховуючи ВСІ деталі реалізації; також інкапсуляція). Цей договір може гарантувати, що після створення екземпляра у вас є готовий до використання об'єкт (тобто немає додаткових кроків ініціалізації, які користувач повинен запам'ятати (ер, забув)).

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

Теоретичний приклад:

Клас Foo має 4 способи та потребує цілого числа для ініціалізації, тому його конструктор виглядає як Foo (int size), і користувачам класу Foo відразу зрозуміло, що вони повинні надати розмір при створенні, щоб Foo працював.

Скажімо, саме ця реалізація Foo також може знадобитися IWidget для виконання своєї роботи. Введення конструктором цієї залежності дозволило б нам створити такий конструктор, як Foo (int size, віджет IWidget)

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

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

Ще гірше, що, якщо віджет необхідний лише для двох із 4-х методів цього класу; Можливо, у мене відзначаються накладні витрати на віджет, навіть якщо він може не використовуватися!

Як це скомпрометувати / примирити?

Один із підходів полягає у переключенні виключно на інтерфейси для визначення контракту на експлуатацію; і скасувати використання конструкторів користувачами. Щоб бути послідовною, до всіх об'єктів потрібно було б отримати доступ лише через інтерфейси та інстанціювати лише через якусь форму роздільної здатності (наприклад, контейнер IOC / DI). Лише контейнер потрапляє на інстанціювання речей.

Це піклується про залежність віджетів, але як ми ініціалізувати "розмір", не вдаючись до окремого методу ініціалізації в інтерфейсі Foo? Використовуючи це рішення, ми втратили можливість гарантувати, що екземпляр Foo буде повністю ініціалізований до моменту отримання екземпляра. Баммер, тому що мені дуже подобається ідея та простота введення конструктора.

Як досягти гарантованої ініціалізації в цьому світі DI, коли ініціалізація БІЛЬШЕ, ніж ТІЛЬКИ зовнішні залежності?


Оновлення: Я щойно помітив, що Unity 2.0 підтримує надання параметрів конструктора (таких як ініціалізатори стану), використовуючи при цьому звичайний механізм для IoC залежностей під час вирішення (). Можливо, інші контейнери також підтримують це? Це вирішує технічні труднощі змішування стану init та DI в одному конструкторі, але це все ще порушує інкапсуляцію!
shawnT

Я чую тебе. Я задав це запитання, тому що я теж вважаю, що дві добрі речі (DI & інкапсуляція) приходять одна за рахунок іншої. BTW, у вашому прикладі, коли лише 2 з 4 методів потребують IWidget, це вказувало б на те, що інші 2 належать до іншого компонента IMHO.
уріг

3

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

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

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


3

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

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

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

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


2

Я вірю в простоту. Застосування IOC / Dependecy Injection в класах Доменів не сприяє покращенню, за винятком того, що код набагато складніше основний, маючи зовнішні файли xml, що описують відношення. Багато технологій, таких як EJB 1.0 / 2.0 та struts 1.1, повертаються назад, зменшуючи вкладені файли в XML і намагаються ввести їх у код як анотацію тощо. Тому застосування IOC для всіх розроблених вами класів зробить код безглуздим.

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

Як і всі інші технології, і в цьому є PROs & CONs. Я хвилююся, ми впроваджуємо новітні технології в усіх місцях, незалежно від їх найкращого використання в контексті.


2

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

Коли я їхав зі свого старого комбі 1971 року, я міг натиснути на прискорювач, і він пішов (трохи) швидше. Мені не потрібно було знати, чому, але хлопці, які будували комбі на заводі, точно знали, чому.

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

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

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

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

Це я за кермом мого Комбі на пляж.

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

Я також можу підвезти свою нову машину до пляжу. Інкапсуляція не порушена.

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


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

2

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

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

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

"заборона користувачам встановлювати внутрішні дані компонента в недійсний або непослідовний стан".

У той же час цього не можна сказати

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

Обидві цитати з вікіпедії .

Щоб навести конкретний приклад: мені потрібно надати клієнтську DLL, яка спрощує та приховує зв’язок до послуги WCF (фактично віддаленого фасаду). Оскільки це залежить від 3-х різних проксі-класів WCF, якщо я скористаюсь підходом DI, я змушений виставити їх через конструктор. З цим я розкриваю внутрішню частину свого комунікаційного рівня, який я намагаюся приховати.

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


2

DI порушує інкапсуляцію для об'єктів, що не поділяються, - період. Спільні об'єкти мають тривалість життя поза створюваним об'єктом, і тому вони повинні бути АГРЕГРИРОВАНО в об'єкт, що створюється. Об'єкти, які є приватними для створюваного об'єкта, повинні бути СКЛАДЕНО в створений об'єкт - коли створений об’єкт знищений, він бере з собою складений об’єкт. Візьмемо людський організм як приклад. Що складається і що зводиться. Якби ми використовували DI, конструктор людського тіла мав би 100 об'єктів. Наприклад, багато органів є (потенційно) замінними. Але вони все ще складаються в організм. Клітини крові створюються в організмі (і руйнуються) щодня, не потребуючи зовнішніх впливів (крім білка). Таким чином, клітини крові створюються внутрішньо організмом - новим BloodCell ().

Прихильники DI стверджують, що об’єкт НІКОЛИ не повинен використовувати нового оператора. Цей "пуристичний" підхід не тільки порушує інкапсуляцію, але й Принцип заміщення Ліскова для тих, хто створює об'єкт.


1

Я також боровся з цим поняттям. Спочатку «вимога» використовувати контейнер DI (наприклад, Spring) для створення об'єкта відчувається як стрибки через обручі. Але насправді це справді не обруч - це просто ще один "опублікований" спосіб створення потрібних мені об'єктів. Звичайно, інкапсуляція "зламана", тому що хтось "поза класом" знає, що їй потрібно, але насправді це не решта системи, яка це знає - це контейнер DI. Нічого магічного не буває інакше, оскільки DI "знає", що один об'єкт потребує іншого.

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


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

1

PS. Надаючи Dependency Injection ви не обов'язково порушувати інкапсуляцію . Приклад:

obj.inject_dependency(  factory.get_instance_of_unknown_class(x)  );

Код клієнта ще не знає деталей реалізації.


Як у вашому прикладі щось вводиться? (За винятком найменування вашої функції сетера)
foo

1

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

Крім того, використовуючи якусь функцію автоматичного підключення (Autofac для C #, наприклад,), він робить код надзвичайно чистим. Створюючи методи розширення для конструктора Autofac, я зміг вирізати багато конфігураційного коду DI, який мені довелося б підтримувати з часом, коли список залежностей зростав.


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

1

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

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

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

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

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

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

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

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

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


0

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

Ви починаєте з тесту CreditCard і пишете простий тест на одиницю.

@Test
public void creditCard_Charge()
{
    CreditCard c = new CreditCard("1234 5678 9012 3456", 5, 2008);
    c.charge(100);
}

Наступного місяця ви отримуєте рахунок на 100 доларів. Чому за вас стягували плату? Підроздільний тест вплинув на виробничу базу даних. Всередині телефонує CreditCard Database.getInstance(). Рефакторинг CreditCard таким чином, що він займає DatabaseInterfaceконструктор, виявляє залежність. Але я заперечую, що залежність ніколи не була інкапсульована для початку, оскільки клас CreditCard викликає зовнішні видимі побічні ефекти. Якщо ви хочете протестувати CreditCard без рефакторингу, ви, звичайно, можете спостерігати залежність.

@Before
public void setUp()
{
    Database.setInstance(new MockDatabase());
}

@After
public void tearDown()
{
    Database.resetInstance();
}

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


1
Одиничні тести, як правило, записуються автором класу, тому нормально викладати залежності в тестовому випадку з технічної точки зору. Коли пізніше клас кредитної картки змінюється для використання веб-API, такого як PayPal, користувачеві потрібно буде змінити все, якби це було DIED. Тестування одиниць, як правило, проводиться з інтимних знань про тестувану тему (чи не в цьому все?), Тому я вважаю, що тести є скоріше винятком, ніж типовим прикладом.
kizzx2

1
Суть DI полягає в тому, щоб уникнути описаних вами змін. Якщо ви перейшли з веб-API на PayPal, ви не змінили б більшість тестів, оскільки вони використовуватимуть MockPaymentService та CreditCard, будучи створеними разом із PaymentService. У вас буде лише декілька тестів, які вивчають реальну взаємодію між кредитною карткою та реальним PaymentService, тому майбутні зміни дуже поодинокі. Переваги ще більше для графіків глибшої залежності (наприклад, класу, який залежить від CreditCard).
Крейг П. Мотлін

@Craig p. Motlin Як можна CreditCardзмінити об'єкт з WebAPI на PayPal, не маючи змін нічого зовнішнього класу?
Ян Бойд

@Я я згадав, що CreditCard має бути відновлений, щоб взяти DatabaseInterface у своєму конструкторі, що захищає його від змін у впровадженні DatabaseInterfaces. Можливо, це має бути ще більш загальним, і взяти інтерфейс StorageInterface, який WebAPI може бути ще однією реалізацією. PayPal знаходиться на неправильному рівні, оскільки це альтернатива CreditCard. І PayPal, і CreditCard могли реалізувати PaymentInterface для захисту інших частин програми поза цим прикладом.
Крейг П. Мотлін

@ kizzx2 Іншими словами, дурно говорити, що кредитна картка повинна використовувати PayPal. Вони є альтернативами в реальному житті.
Крейг П. Мотлін

0

Я думаю, це справа сфери. Коли ви визначаєте інкапсуляцію (не даючи знати, як), ви повинні визначити, що таке функція інкапсуляції.

  1. Клас такий : те, що ви інкапсулюєте, є єдиною відповідальністю класу. Що це вміє робити. За прикладом сортування. Якщо ви вводите якийсь компаратор для замовлення, скажімо, клієнтів, це не є частиною вкладеної речі: quicksort.

  2. Налаштована функціональність : якщо ви хочете надати функціонал, готовий до використання, ви надаєте не клас QuickSort, а екземпляр класу QuickSort, налаштований із компаратором. У цьому випадку код, відповідальний за створення та налаштування, повинен бути прихованим від коду користувача. І це інкапсуляція.

Коли ви програмуєте заняття, це, реалізуючи окремі обов'язки у класах, ви використовуєте варіант 1.

Коли ви програмуєте програми, саме ви робите щось, що виконує якусь корисну конкретну роботу, тоді ви неодноразово використовуєте варіант 2.

Це реалізація налаштованого примірника:

<bean id="clientSorter" class="QuickSort">
   <property name="comparator">
      <bean class="ClientComparator"/>
   </property>
</bean>

Ось як його використовують деякі інші клієнтські коди:

<bean id="clientService" class"...">
   <property name="sorter" ref="clientSorter"/>
</bean>

Він інкапсульований, оскільки якщо ви зміните реалізацію (ви зміните clientSorterвизначення bean), це не порушує використання клієнта. Можливо, під час використання файлів xml із усіма написаними разом ви бачите всі деталі. Але повірте, клієнтський код ( ClientService) нічого не знає про його сортувальник.


0

Мабуть, варто згадати, що Encapsulationдещо залежить перспектива.

public class A { 
    private B b;

    public A() {
        this.b = new B();
    }
}


public class A { 
    private B b;

    public A(B b) {
        this.b = b;
    }
}

З точки зору того, хто працює над Aкласом, у другому прикладі Aвідомо набагато менше про природуthis.b

Тоді як без DI

new A()

проти

new A(new B())

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

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

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