Dostaję zastrzyk zależności, ale czy ktoś może mi pomóc w zrozumieniu potrzeby posiadania kontenera IoC?

15

Przepraszam, jeśli wydaje się to kolejną powtórką pytania, ale za każdym razem, gdy znajduję artykuł na ten temat, głównie mówi on o tym, czym jest DI. Tak, dostaję DI, ale staram się zrozumieć potrzebę kontenera IoC, do którego wszyscy chyba się pakują. Czy celem kontenera IoC jest po prostu „automatyczne rozwiązywanie” konkretnej implementacji zależności? Może moje klasy zwykle nie mają kilku zależności i może dlatego nie widzę wielkiego problemu, ale chcę się upewnić, że dobrze rozumiem użyteczność kontenera.

Zazwyczaj dzielę logikę biznesową na klasy, które mogą wyglądać mniej więcej tak:

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;
    }
}

Więc ma tylko jedną zależność, aw niektórych przypadkach może mieć drugą lub trzecią, ale nie tak często. Tak więc wszystko, co to wywołuje, może przejść własne repozytorium lub pozwolić na użycie domyślnego repozytorium.

O ile obecnie rozumiem kontener IoC, wszystko, co robi kontener, to IDataRepository. Ale jeśli to wszystko, co robi, to nie widzę w tym mnóstwa wartości, ponieważ moje klasy operacyjne już definiują awarię, gdy nie przechodzi żadna zależność. Więc jedyną inną korzyścią, o której mogę myśleć, jest to, że jeśli mam kilka operacji, takich jak używając tego samego repozytorium rezerwowego, mogę zmienić to repozytorium w jednym miejscu, którym jest rejestr / fabryka / kontener. I to świetnie, ale czy o to chodzi?

Sinaesthetic
źródło
1
Często posiadanie domyślnej rezerwowej wersji zależności tak naprawdę nie ma sensu.
Ben Aaronson
Co masz na myśli „Awaria” to konkretna klasa, która jest wykorzystywana prawie cały czas, z wyjątkiem testów jednostkowych. W efekcie byłaby to ta sama klasa zarejestrowana w kontenerze.
Sinaesthetic
Tak, ale z kontenerem: (1) wszystkie inne obiekty w kontenerze otrzymują to samo wystąpienie ConcreteRepositoryi (2) można podać dodatkowe zależności ConcreteRepository(na przykład połączenie z bazą danych byłoby powszechne).
Jules
@Sestestetyczne Nie mam na myśli, że to zawsze zły pomysł, ale często nie jest odpowiedni. Na przykład zapobiegnie to podążaniu za architekturą cebuli z referencjami do projektu. Ponadto może nie być jasnej domyślnej implementacji. I jak mówi Jules, kontenery MKOl nie tylko wybierają typ zależności, ale także robią takie rzeczy, jak udostępnianie instancji i zarządzanie cyklem życia
Ben Aaronson
Mam zamiar zrobić koszulkę z napisem „Parametry funkcji - ORYGINALNY wstrzyknięcie zależności!”
Graham,

Odpowiedzi:

2

Kontener IoC nie dotyczy przypadku, w którym masz jedną zależność. Chodzi o przypadek, w którym masz 3 zależności i mają one kilka zależności, które mają zależności itp.

Pomaga także scentralizować rozwiązywanie zależności i zarządzanie nimi w cyklu życia.

Znak
źródło
10

Istnieje wiele powodów, dla których warto użyć kontenera IoC.

Niepowiązane biblioteki dll

Możesz użyć kontenera IoC, aby rozwiązać konkretną klasę z dll bez odwołania. Oznacza to, że możesz wziąć zależności całkowicie od abstrakcji - tj. Interfejsu.

Unikaj używania new

Kontener IoC oznacza, że ​​możesz całkowicie usunąć użycie newsłowa kluczowego do tworzenia klasy. Ma to dwa efekty. Pierwszym jest to, że oddziela twoje zajęcia. Druga (związana z tym kwestia) polega na tym, że można poddawać się próbnym testom jednostkowym. Jest to niezwykle przydatne, szczególnie w przypadku długotrwałego procesu.

Napisz przeciw abstrakcjom

Użycie kontenera IoC do rozwiązania konkretnych zależności pozwala na pisanie kodu przeciw abstrakcjom, zamiast implementowania każdej konkretnej klasy, jakiej potrzebujesz. Na przykład może być potrzebny kod do odczytu danych z bazy danych. Zamiast pisać klasę interakcji z bazą danych, po prostu piszesz dla niej interfejs i kodujesz przeciw temu. Możesz użyć próbnego narzędzia do przetestowania funkcjonalności tworzonego kodu podczas jego opracowywania, zamiast polegać na opracowaniu konkretnej klasy interakcji bazy danych przed przetestowaniem drugiego kodu.

Unikaj łamliwego kodu

Innym powodem korzystania z kontenera IoC jest to, że polegając na kontenerze IoC w celu rozwiązania zależności, można uniknąć konieczności zmiany każdego wywołania do konstruktora klasy podczas dodawania lub usuwania zależności. Kontener IoC automatycznie rozwiąże twoje zależności. Nie jest to ogromny problem, gdy tworzysz klasę raz, ale jest to gigantyczny problem, gdy tworzysz klasę w stu miejscach.

Dożywotnie zarządzanie i niezarządzane czyszczenie zasobów

Ostatnim powodem, o którym wspomnę, jest zarządzanie czasem życia obiektów. Kontenery IoC często umożliwiają określenie czasu życia obiektu. Sensowne jest określenie czasu życia obiektu w kontenerze IoC zamiast próby ręcznego zarządzania nim w kodzie. Ręczne zarządzanie cyklem życia może być bardzo trudne. Może to być przydatne w przypadku obiektów wymagających utylizacji. Zamiast ręcznie zarządzać usuwaniem obiektów, niektóre kontenery IoC zarządzają usuwaniem dla Ciebie, co może pomóc w zapobieganiu wyciekom pamięci i uprościć bazę kodu.

Problem z podanym przez ciebie przykładowym kodem polega na tym, że pisana klasa ma konkretną zależność od klasy ConcreteRepository. Kontener IoC usunąłby tę zależność.

Stephen
źródło
22
To nie są zalety kontenerów IoC, to zalety wstrzykiwania zależności, co można łatwo zrobić z DI biedaka
Ben Aaronson
Pisanie dobrego kodu DI bez kontenera IoC może być raczej trudne. Tak, zalety w pewnym stopniu się pokrywają, ale wszystkie te zalety najlepiej wykorzystać w kontenerze IoC.
Stephen
Cóż, dwa ostatnie powody, które dodałeś od czasu mojego komentarza, są bardziej specyficzne dla kontenera i oba są moim zdaniem bardzo silnymi argumentami
Ben Aaronson
„Unikaj używania nowego” - utrudnia również analizę kodu statycznego, więc musisz zacząć używać czegoś takiego: hmemcpy.github.io/AgentMulder . Inne korzyści opisane w tym punkcie dotyczą DI, a nie IoC. Również twoje klasy będą nadal połączone, jeśli unikniesz używania nowych, ale użyjesz konkretnych typów zamiast interfejsów dla parametrów.
Den
1
Ogólnie rzecz biorąc, IoC jest postrzegane jako coś bez wad, na przykład nie ma wzmianki o dużym minusie: wprowadzanie klasy błędów w środowisko wykonawcze zamiast czasu kompilacji.
Den
2

Zgodnie z zasadą jednej odpowiedzialności każda klasa musi ponosić tylko jedną odpowiedzialność. Tworzenie nowych instancji klas to po prostu kolejna odpowiedzialność, więc musisz obudować ten rodzaj kodu w jednej lub kilku klasach. Możesz to zrobić przy użyciu dowolnych wzorców kreacyjnych, na przykład fabryk, konstruktorów, kontenerów DI itp.

Istnieją inne zasady, takie jak odwrócenie kontroli i odwrócenie zależności. W tym kontekście są one związane z wystąpieniem zależności. Stwierdzają, że klasy wysokiego poziomu muszą być oddzielone od klas niskiego poziomu (zależności), których używają. Możemy oddzielić rzeczy, tworząc interfejsy. Tak więc klasy niskiego poziomu muszą implementować określone interfejsy, a klasy wysokiego poziomu muszą wykorzystywać instancje klas, które implementują te interfejsy. (uwaga: jednolite ograniczenie interfejsu REST stosuje to samo podejście na poziomie systemu).

Kombinacja tych zasad na przykładzie (przepraszam za kod niskiej jakości, użyłem jakiegoś języka ad-hoc zamiast C #, ponieważ nie wiem tego):

  1. Bez SRP, bez 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. Bliżej SRP, bez 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. Tak SRP, nie 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. Tak SRP, tak 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();

W rezultacie otrzymaliśmy kod, w którym możesz zastąpić każdą konkretną implementację inną, która implementuje ten sam interfejs ofc. Jest to dobre, ponieważ uczestniczące klasy są od siebie oddzielone, znają tylko interfejsy. Kolejną zaletą jest to, że kod instancji jest wielokrotnego użytku.

inf3rno
źródło