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


15

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

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

public class SomeBusinessOperation
{
    private readonly IDataRepository _repository;

    public SomeBusinessOperation(IDataRespository repository = null)
    {
        _repository = repository ?? new ConcreteRepository();
    }

    public SomeType Run(SomeRequestType request)
    {
        // do work...
        var results = _repository.GetThings(request);

        return results;
    }
}

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

Що стосується мого теперішнього розуміння контейнера IoC, то все, що потрібно, це вирішення IDataRepository. Але якщо це все, що це робиться, то я не бачу в ній тонни значення, оскільки мої операційні класи вже визначають резервний запас, коли не проходила залежність. Тож єдиною іншою перевагою, про яку я можу придумати, є те, що якщо у мене є кілька операцій, таких як використовуючи ту саму резервну репо, я можу змінити це репо в одному місці, яке є реєстром / фабрикою / контейнером. І це чудово, але це так?


1
Часто мати версію резервної версії залежності не має сенсу.
Бен Аронсон

Як ви маєте на увазі? "Резервний" - це конкретний клас, до якого майже весь час використовуються, за винятком одиничних тестів. Фактично, це був би той самий клас, який зареєстрований у контейнері.
Сінестетик

Так, але з контейнером: (1) всі інші об'єкти контейнера отримують один і той же екземпляр, ConcreteRepositoryі (2) ви можете надати додаткові залежності ConcreteRepository(наприклад, з'єднання з базою даних).
Жуль

@Sinaesthetic Я не маю на увазі, що це завжди погана ідея, але часто це не доречно. Наприклад, це не дозволить вам слідувати архітектурі цибулі із посиланнями на ваш проект. Також не може бути чіткої реалізації за замовчуванням. І як каже Жуль, контейнери МОК управляють не просто вибираючи тип залежності, а роблять такі речі, як обмін екземплярами та управління життєвим циклом
Бен Ааронсон,

Я збираюсь зробити футболку, на якій написано "Параметри функції - ОРИГІНАЛЬНА ВВЕДЕННЯ залежності"! "
Грем

Відповіді:


2

Контейнер IoC не стосується випадку, коли у вас є одна залежність. Йдеться про випадок, коли у вас є 3 залежності, а в них є кілька залежностей, які мають залежності тощо.

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


10

Існує ряд причин, чому ви можете використовувати контейнер IoC.

Нереференційовані дл

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

Уникайте використання new

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

Пишіть проти абстракцій

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

Уникайте крихкого коду

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

Довічне управління та некероване очищення відновлюють

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

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


22
Це не переваги контейнерів IoC, це переваги ін'єкцій залежностей, які можна легко здійснити з ІР бідолаха
Бен Аронсон,

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

Що ж, останні два причини, які ви додали, оскільки мій коментар мають більш конкретний контейнер і є дуже сильними аргументами на мою думку
Бен Ааронсон,

"Уникайте використання нових" - також перешкоджає аналізу статичного коду, тому вам доведеться почати використовувати щось подібне: hmemcpy.github.io/AgentMulder . Інші переваги, які ви описуєте в цьому пункті, стосуються DI, а не IoC. Також ваші класи все ще будуть поєднані, якщо ви не використовуєте нові, але будете використовувати конкретні типи замість інтерфейсів для параметрів.
Den

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

2

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

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

Поєднання цих принципів на прикладі (вибачте за низьку якість коду, я використовував деякі спеціальні мови замість C #, оскільки я цього не знаю):

  1. Ні SRP, ні IoC

    class SomeHighLevelService
    {
        public doFooBar(){
            Crap crap = doFoo();
            doBar(crap);
        }
    
        public Crap doFoo(){
            //...
            return crap;
        }
    
        public doBar(Crap crap){
            //...
        }
    }
    
    SomeHighLevelService service = new SomeHighLevelService();
    service.doFooBar();
  2. Ближче до SRP, без IoC

    class SomeHighLevelService
    {
        public SomeHighLevelService(){
            Foo foo = new Foo();
            Bar bar = new Bar();
        }
    
        public doFooBar(){
            Crap crap = foo.doFoo();
            bar.doBar(crap);
        }
    }
    
    class Foo {
        public Crap doFoo(){
            //...
            return crap;
        }
    }
    
    class Bar {
        public doBar(Crap crap){
            //...
        }
    }
    
    SomeHighLevelService service = new SomeHighLevelService();
    service.doFooBar();
  3. Так, SRP, немає IoC

    class HighLevelServiceProvider {
        public SomeHighLevelService getSomeHighLevelService(){
            SomeHighLevelService service = new SomeHighLevelService();
            service.setFoo(this.getFoo());
            service.getBar(this.getBar());
            return service;
        }
    
        private Foo getFoo(){
            return new Foo();
        }
    
        private Bar getBar(){
            return new Bar();
        }
    }
    
    class SomeHighLevelService
    {           
        public setFoo(Foo foo){
            this.foo = foo;
        }
    
        public setBar(Bar bar){
            this.bar = bar;
        }
    
        public doFooBar(){
            Crap crap = foo.doFoo();
            bar.doBar(crap);
        }
    
    }
    
    class Foo {
        public Crap doFoo(){
            //...
            return crap;
        }
    }
    
    class Bar {
        public doBar(Crap crap){
            //...
        }
    }
    
    HighLevelServiceProvider provider = new HighLevelServiceProvider();
    SomeHighLevelService service = provider.getSomeHighLevelService();
    service.doFooBar();
  4. Так SRP, так IoC

    interface HighLevelServiceProvider {
        SomeHighLevelService getSomeHighLevelService();
    }
    
    interface SomeHighLevelService {
        doFooBar();
    }
    
    interface Foo {
        Crap doFoo();
    }
    
    interface Bar {
        doBar(Crap crap);
    }
    
    
    class ConcreteHighLevelServiceContainer implements HighLevelServiceProvider {
        public SomeHighLevelService getSomeHighLevelService(){
            SomeHighLevelService service = new ConcreteHighLevelService();
            service.setFoo(this.getFoo());
            service.getBar(this.getBar());
            return service;
        }
    
        private Foo getFoo(){
            return new ConcreteFoo();
        }
    
        private Bar getBar(){
            return new ConcreteBar();
        }
    }
    
    class ConcreteHighLevelService implements SomeHighLevelService
    {           
        public setFoo(Foo foo){
            this.foo = foo;
        }
    
        public setBar(Bar bar){
            this.bar = bar;
        }
    
        public doFooBar(){
            Crap crap = foo.doFoo();
            bar.doBar(crap);
        }
    
    }
    
    class ConcreteFoo implements Foo {
        public Crap doFoo(){
            //...
            return crap;
        }
    }
    
    class ConcreteBar implements Bar {
        public doBar(Crap crap){
            //...
        }
    }
    
    
    HighLevelServiceProvider provider = new ConcreteHighLevelServiceContainer();
    SomeHighLevelService service = provider.getSomeHighLevelService();
    service.doFooBar();

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

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