Wstrzykiwanie zależności: wstrzykiwanie w terenie czy wstrzykiwanie konstruktora?

61

Wiem, że jest to gorąca debata, a opinie zmieniają się z czasem co do najlepszej praktyki podejścia.

Kiedyś korzystałem wyłącznie z iniekcji terenowych do moich zajęć, dopóki nie zacząłem czytać na różnych blogach (np. Petrikainulainen i schauderhaft i fowler ) o korzyściach z iniekcji konstruktora. Od tego czasu zmieniłem swoje metodologie, aby używać wstrzykiwania konstruktora dla wymaganych zależności i wstrzykiwania setera dla zależności opcjonalnych.

Jednak ja niedawno zdobyty w dyskusji z autorem JMockit - ramy kpiący - w którym widzi konstruktora & zastrzyk seter jako złych praktyk i wskazuje, że społeczność JEE zgadza się z nim.

Czy w dzisiejszym świecie istnieje preferowany sposób wykonywania zastrzyków? Czy preferowany jest zastrzyk polowy?

Po przejściu na iniekcję konstruktora z iniekcji polowej w ciągu ostatnich kilku lat uważam, że korzystanie z niej jest o wiele łatwiejsze, ale zastanawiam się, czy powinienem ponownie przeanalizować mój punkt widzenia. Autor JMockit (Rogério Liesenfeld) jest wyraźnie dobrze zorientowany w DI, więc czuję się zobowiązany do przeglądu mojego podejścia, biorąc pod uwagę, że tak mocno odczuwa sprzeciw wobec iniekcji konstruktora / setera.

Eric B.
źródło
2
Jeśli nie wolno ci używać iniekcji konstruktora lub iniekcji settera, to jak masz przekazać klasę zależności? Może lepiej link do debaty, chyba że rozmowa z Mockitem była prywatna.
Robert Harvey
2
@DavidArno: W niezmiennej klasie stan jest ustawiany raz ... w konstruktorze.
Robert Harvey
1
@Davkd to, co opisujesz, to anemiczny model domeny. Z pewnością ma zwolenników, ale tak naprawdę nie jest już zorientowany obiektowo; gdzie dane i funkcje działające na te dane żyją razem.
Richard Tingle
3
@David Nie ma nic złego w programowaniu funkcjonalnym. Ale java zasadniczo jest zorientowana obiektowo i będziesz ciągle z nią walczył, a jeśli nie będziesz bardzo ostrożny, skończysz na modelu domeny anemicznej . Nie ma nic złego w kodzie zorientowanym na funkcje, ale należy do niego użyć odpowiedniego narzędzia, czego java nie jest (chociaż lambda dodała elementy programowania opartego na funkcjach)
Richard Tingle

Odpowiedzi:

48

Jak na mój gust, iniekcja w polu to trochę zbyt „upiorna akcja na odległość” .

Rozważ przykład podany w poście w Grupach dyskusyjnych Google:

public class VeracodeServiceImplTest {


    @Tested(fullyInitialized=true)
    VeracodeServiceImpl veracodeService;
    @Tested(fullyInitialized=true, availableDuringSetup=true)
    VeracodeRepositoryImpl veracodeRepository;
    @Injectable private ResultsAPIWrapper resultsApiWrapper;
    @Injectable private AdminAPIWrapper adminApiWrapper;
    @Injectable private UploadAPIWrapper uploadApiWrapper;
    @Injectable private MitigationAPIWrapper mitigationApiWrapper;

    static { VeracodeRepositoryImpl.class.getName(); }
...
}

Więc w zasadzie mówisz, że „Mam tę klasę z państwem prywatnym, do której załączyłem @injectableadnotacje, co oznacza, że ​​państwo może zostać automatycznie zaludnione przez jakiegoś agenta z zewnątrz, mimo że mój stan został uznany za prywatny.

Rozumiem motywacje do tego. Jest to próba uniknięcia dużej części ceremonii, która jest nieodłącznie związana z prawidłowym zorganizowaniem klasy. Zasadniczo chodzi o to, że „jestem zmęczony pisaniem całego tego bojlera, więc po prostu opiszę cały mój stan i pozwól pojemnikowi DI zająć się ustawieniem go dla mnie”.

To całkowicie poprawny punkt widzenia. Ale jest to również obejście funkcji językowych, które prawdopodobnie nie powinny być omijane. A po co się tu zatrzymywać? Tradycyjnie DI opierał się na każdej klasie posiadającej interfejs towarzyszący. Dlaczego nie wyeliminować również wszystkich tych interfejsów z adnotacjami?

Rozważ alternatywną opcję (będzie to C #, ponieważ znam ją lepiej, ale prawdopodobnie jest to dokładny odpowiednik w Javie):

public class VeracodeService
{        
    private readonly IResultsAPIWrapper _resultsApiWrapper;
    private readonly IAdminAPIWrapper _adminApiWrapper;
    private readonly IUploadAPIWrapper _uploadApiWrapper;
    private readonly IMitigationAPIWrapper _mitigationApiWrapper;

    // Constructor
    public VeracodeService(IResultsAPIWrapper resultsApiWrapper, IAdminAPIWrapper adminApiWrapper, IUploadAPIWrapper uploadApiWrapper,         IMitigationAPIWrapper mitigationApiWrapper)
    {
         _resultsAPIWrapper = resultsAPIWrapper;
         _adminAPIWrapper = adminAPIWrapper;
         _uploadAPIWrapper = uploadAPIWrapper;
         _mitigationAPIWrapper = mitigationAPIWrapper;
    }
}

Już wiem kilka rzeczy na temat tej klasy. To niezmienna klasa; stan można ustawić tylko w konstruktorze (w tym konkretnym przypadku odwołania). A ponieważ wszystko wywodzi się z interfejsu, mogę wymieniać implementacje w konstruktorze, czyli tam, gdzie przychodzą twoje symulacje.

Teraz wszystko, co musi zrobić mój kontener DI, to zastanowienie się nad konstruktorem, aby określić, jakie obiekty będą potrzebne do nowego. Ale ta refleksja odbywa się na członku publicznym, w pierwszej kolejności; tzn. metadane są już częścią klasy, ponieważ zostały zadeklarowane w konstruktorze, metodzie, której wyraźnym celem jest zapewnienie klasie potrzebnych zależności.

Przyznaję, że jest to duży bojler, ale tak właśnie zaprojektowano język. Adnotacje wyglądają jak brudny hack do czegoś, co powinno być wbudowane w sam język.

Robert Harvey
źródło
Chyba że źle przeczytałem twój post, tak naprawdę nie patrzysz na przykład poprawnie. Moja implementacja VeracodeServicejest prawie identyczna z tą, którą napisałeś (choć w Javie vs C #). W VeracodeServiceImplTestrzeczywistości jest to klasa testu jednostkowego. Te @Injectablepola są zasadniczo wyśmiewali obiekty są umieszczone w kontekście. @TestedPole jest przedmiotem / klasa z DI konstruktor zdefiniowane. Zgadzam się z twoim punktem widzenia, który woli wstrzykiwanie konstruktora niż wstrzykiwanie w terenie. Jednak, jak wspomniałem, autor JMockit uważa, że ​​jest odwrotnie i staram się zrozumieć, dlaczego
Eric B.,
Jeśli spojrzysz na pierwszy post w tej dyskusji, zobaczysz, że mam prawie taki sam defn w mojej klasie Repo.
Eric B.
Jeśli mówisz tylko o klasach testowych, może to nie mieć znaczenia. Ponieważ klasy testowe.
Robert Harvey
Nie - nie mówię o klasach testowych. Mówię o klasach produkcyjnych. Zdarza się tak, że przykładem w grupie dyskusyjnej jest test jednostkowy, ponieważ struktura jest strukturą próbną / próbną. (Cała dyskusja obraca się wokół tego, czy kpiąca struktura powinna wspierać wstrzykiwanie konstruktora)
Eric B.
3
+1: Tak, brak magii w wersji C #. Jest to klasa standardowa, ma zależności, wszystkie są wstrzykiwane w jednym punkcie, zanim klasa zostanie użyta. Klasa może być używana z kontenerem DI lub nie. Nic w tej klasie nie mówi, że jest to IOC lub framework „Automatic Dependency Injection”.
Binary Worrier,
27

Argument mniejszej próby inicjalizacji testu jest poprawny, ale istnieją inne obawy, które należy wziąć pod uwagę. Przede wszystkim musisz odpowiedzieć na pytanie:

Czy chcę, aby moja klasa była natychmiastowa tylko dzięki refleksji?

Stosowanie iniekcji w polu oznacza zawężenie kompatybilności klasy do środowisk iniekcji zależnych, które tworzą obiekty za pomocą odbicia i wspierają te adnotacje dotyczące iniekcji. Niektóre platformy oparte na języku Java nie obsługują nawet odbicia ( GWT ), więc klasa wstrzykiwana w pole nie będzie z nimi kompatybilna.

Drugi problem to wydajność . Wywołanie konstruktora (bezpośrednie lub przez odbicie) jest zawsze szybsze niż kilka przypisań pola odbicia. Struktury wstrzykiwania zależności muszą używać analizy odbić, aby budować drzewo zależności i tworzyć konstruktory odbić. Ten proces powoduje dodatkowe pogorszenie wydajności.

Wydajność wpływa na łatwość konserwacji . Jeśli każdy zestaw testowy musi zostać uruchomiony w jakimś pojemniku do wstrzykiwania zależności, uruchomienie testowe składające się z kilku tysięcy testów jednostkowych może potrwać kilkadziesiąt minut. W zależności od rozmiaru podstawy kodu może to stanowić problem.

To wszystko rodzi wiele nowych pytań:

  1. Jakie jest prawdopodobieństwo wykorzystania części kodu na innej platformie?
  2. Ile testów jednostkowych zostanie napisanych?
  3. Jak szybko muszę przygotować każde nowe wydanie?
  4. ...

Ogólnie rzecz biorąc, im większy i ważniejszy jest projekt, tym bardziej znaczące są te czynniki. Ponadto, jeśli chodzi o jakość, ogólnie chcielibyśmy utrzymać kod na wysokim poziomie pod względem kompatybilności, testowalności i łatwości konserwacji. Z filozoficznego punktu widzenia wstrzykiwanie w pole przerywa enkapsulację , która jest jedną z czterech podstaw programowania obiektowego , która jest głównym paradygmatem Javy.

Wiele, wiele argumentów przeciwko iniekcji w terenie.

Maciej Chałapuk
źródło
3
+1 Trafiłeś w nerw. Nie lubię potrzeby refleksji ani struktury DI / IoC, aby utworzyć instancję klasy. To jak jazda do Rzymu, aby dostać się do Paryża z Amsterdamu. Pamiętaj, że kocham DI. Po prostu nie jestem pewien co do ram.
Marjan Venema
1
Świetne argumenty. Werbalizuje wiele myśli, które miałem, ale cieszę się, że inni czują to samo. Choć tak niesamowite jak kiedyś znajdowałem zastrzyk polowy, myślę, że tak bardzo mi się podobało, ponieważ było to „łatwiejsze”. Uważam, że zastrzyk konstruktora jest teraz o wiele wyraźniejszy.
Eric B.,
19

Iniekcja otrzymuje ode mnie zdecydowane „nie” głosowanie.

Podobnie jak Robert Harvey, jest to zbyt automagiczne jak na mój gust. Wolę kod jawny niż niejawny i toleruję pośredniość tylko wtedy, gdy zapewnia on wyraźne korzyści, ponieważ utrudnia zrozumienie i uzasadnienie kodu.

Podobnie jak Maciej Chałapuk, nie lubię potrzeby refleksji ani struktury DI / IoC, aby utworzyć instancję klasy. To jak jazda do Rzymu, aby dostać się do Paryża z Amsterdamu.

Pamiętaj, że kocham DI i wszystkie zalety, które to przynosi. Po prostu nie jestem pewien, czy potrzebne są ramy do tworzenia instancji klasy. Szczególnie nie wpływ, jaki mogą mieć pojemniki IoC lub inne frameworki DI na kod testowy.

Lubię, żeby moje testy były bardzo proste. Nie lubię przechodzić przez pośrednie konfigurowanie kontenerów IoC lub innych struktur DI, aby następnie przetestować moją klasę. To po prostu dziwne.

I to uzależnia moje testy od globalnego stanu frameworka. To znaczy, że nie mogę już uruchamiać równolegle dwóch testów, które wymagają skonfigurowania instancji klasy X z różnymi zależnościami.

Przynajmniej w przypadku wstrzykiwania konstruktora i setera masz możliwość nie używania ram i / lub odbicia w swoich testach.

Marjan Venema
źródło
Jeśli używasz na przykład Mockito mockito.org , nie potrzebujesz frameworka DI / IoC, nawet jeśli korzystasz z iniekcji w terenie, i możesz uruchomić test równolegle i tak dalej.
Hans-Peter Störr
1
@hstoerr Nice. Musi to jednak oznaczać, że Mockito używa refleksji (lub jak to się nazywa odpowiednik Java) ...
Marjan Venema
5

Cóż, to wydaje się polemiczna dyskusja. Pierwszą rzeczą, którą należy rozwiązać, jest to, że iniekcja polowa różni się od iniekcji Settera . Pracuję już z niektórymi ludźmi, którzy myślą, że zastrzyk z pola i setera to to samo.

Tak więc, dla jasności:

Iniekcja w terenie:

@Autowired
private SomeService someService;

Zastrzyk setera:

@Autowired
public void setSomeService(SomeService someService) {
    this.someService = someService;
}

Ale dla mnie mają te same obawy. I, mój ulubiony, zastrzyk konstruktora:

@AutoWired
public MyService(SomeService someService) {
    this.someService = someService;
}

Mam następujące powody, by sądzić, że wstrzyknięcie konstruktora jest lepsze niż wstrzyknięcie setera / pola (podam kilka linków z wiosny, aby zademonstrować swój punkt):

  1. Zapobiegaj zależnościom kołowym : Wiosna jest w stanie wykryć zależności kołowe między ziarnami, jak wyjaśnił @Tomasz. Zobacz także: Zespół wiosenny, który to mówi .
  2. Zależności klasy są oczywiste : Zależności klasy są oczywiste w konstruktorze, ale ukrywane przez wstrzyknięcie pola / setera. Czuję, że moje zajęcia są jak „czarna skrzynka” z zastrzykiem z pola / setera. Z mojego doświadczenia wynika, że ​​programiści nie zawracają sobie głowy klasą z wieloma zależnościami, co jest oczywiste, gdy próbujesz kpić z klasy za pomocą wstrzykiwania konstruktora (patrz następny element). Problem, który niepokoi programistów, najprawdopodobniej zostanie rozwiązany. Ponadto klasa ze zbyt wieloma zależnościami prawdopodobnie narusza zasadę pojedynczej odpowiedzialności (SRP).
  3. Łatwy i niezawodny do kpienia : w porównaniu z iniekcją polową łatwiej jest kpić w teście jednostkowym, ponieważ nie potrzebujesz żadnego kontekstowego kpina lub techniki refleksyjnej, aby dotrzeć do zadeklarowanej prywatnej fasoli w klasie i nie da się oszukać . Po prostu stwórz klasę i przekaż szyderczą fasolę. Prosty do zrozumienia i łatwy.
  4. Narzędzia jakości kodu mogą ci pomóc : narzędzia takie jak Sonar mogą ostrzec cię, że klasa staje się zbyt złożona, ponieważ klasa z wieloma parametrami jest prawdopodobnie zbyt złożoną klasą i potrzebuje pewnego refaktora. To narzędzie nie może zidentyfikować tego samego problemu, gdy klasa używa iniekcji Field / Setter.
  5. Lepszy i niezależny kod : przy mniejszym @AutoWiredrozproszeniu kodu, twój kod jest mniej zależny od frameworka. Wiem, że możliwość prostej zmiany frameworka DI w projekcie nie uzasadnia tego (kto to robi, prawda?). Ale to pokazuje, dla mnie, lepszy kod (nauczyłem się tego na poważnie za pomocą EJB i wyszukiwań). Za pomocą Springa możesz nawet usunąć @AutoWiredadnotację za pomocą iniekcji konstruktora.
  6. Więcej adnotacji nie zawsze jest właściwą odpowiedzią : Z niektórych powodów naprawdę staram się używać adnotacji tylko wtedy, gdy są naprawdę przydatne.
  7. Możesz sprawić, by zastrzyki były niezmienne . Niezmienność jest bardzo pożądaną cechą.

Jeśli szukasz więcej informacji, polecam ten stary (ale nadal aktualny) artykuł z Spring Blog, który mówi nam, dlaczego używają tak dużo zastrzyku setera i zalecenie użycia zastrzyku konstruktora:

zastrzyk setera jest używany znacznie częściej, niż można się spodziewać, jest fakt, że frameworki, takie jak Spring, są znacznie bardziej odpowiednie do skonfigurowania przez zastrzyk setera niż zastrzyk konstruktora

I ta uwaga o tym, dlaczego uważają, że ten konstruktor jest bardziej odpowiedni dla kodu aplikacji:

Myślę, że zastrzyk konstruktora jest znacznie bardziej użyteczny dla kodu aplikacji niż dla kodu frameworka. W kodzie aplikacji z natury rzeczy mniejsze jest zapotrzebowanie na opcjonalne wartości, które należy skonfigurować

Końcowe przemyślenia

Jeśli nie masz pewności co do zalet konstruktora, być może pomysł mieszania setera (nie pola) z wstrzykiwaniem konstruktora jest wyjaśniony przez zespół Spring :

Ponieważ można łączyć zarówno DI oparte na konstruktorze, jak i Setterze, dobrą zasadą jest używanie argumentów konstruktorów dla zależności obowiązkowych i ustawiaczy dla zależności opcjonalnych

Pamiętaj jednak, że wstrzyknięcie pola jest prawdopodobnie tym, którego należy unikać wśród tych trzech.

Dherik
źródło
-5

Czy Twoja zależność prawdopodobnie zmieni się w trakcie życia klasy? Jeśli tak, użyj zastrzyku pola / nieruchomości. W przeciwnym razie użyj iniekcji konstruktora.

Darren Young
źródło
2
To tak naprawdę nic nie wyjaśnia. Po co wprowadzać prywatne pola, skoro można to zrobić we właściwy sposób?
Robert Harvey
Gdzie cokolwiek mówi o prywatnych polach? Mówię o publicznym interfejsie klasy /
Darren Young
W pytaniu nie ma argumentów o wstrzyknięciu metody vs. wstrzyknięciu konstruktora. Autor JMockit uważa oba za złe. Spójrz na tytuł pytania.
Robert Harvey
Czy pytanie nie mówi o iniekcji w terenie a iniekcji konstruktora?
Darren Young,
1
Iniekcja w terenie to zupełnie inne zwierzę. Narzucasz wartości prywatnym członkom.
Robert Harvey