Jaka jest różnica między używaniem wstrzykiwania zależności w kontenerze a używaniem lokalizatora usług?

107

Rozumiem, że bezpośrednie tworzenie instancji zależności w klasie jest uważane za złą praktykę. Ma to sens, ponieważ tak ścisłe łączenie wszystkiego, co z kolei sprawia, że ​​testowanie jest bardzo trudne.

Prawie wszystkie frameworki, które spotkałem, wydają się faworyzować wstrzykiwanie zależności za pomocą kontenera niż używanie lokalizatorów usług. Oba wydają się osiągać to samo, pozwalając programiście na określenie, który obiekt powinien zostać zwrócony, gdy klasa wymaga zależności.

Jaka jest różnica między nimi? Dlaczego miałbym wybierać jeden nad drugim?

tom6025222
źródło
3
To zostało zadane (i odpowiedź) na StackOverflow: stackoverflow.com/questions/8900710/…
Kyralessa
2
Moim zdaniem programiści wcale nie są w stanie oszukiwać innych, że ich lokalizator usług jest zastrzykiem zależności. Robią to, ponieważ zastrzyk zależności jest często uważany za bardziej zaawansowany.
Gherman
1
Dziwię się, że nikt nie wspominał, że dzięki Lokalizatorom usług trudniej jest ustalić, kiedy przejściowy zasób może zostać zniszczony. Zawsze myślałem, że to był główny powód.
Buh Buh

Odpowiedzi:

126

Kiedy sam obiekt jest odpowiedzialny za żądanie swoich zależności, w przeciwieństwie do akceptowania ich przez konstruktor, ukrywa pewne istotne informacje. Jest tylko nieznacznie lepszy niż bardzo ściśle powiązany przypadek użycia newdo utworzenia jego zależności. Zmniejsza to sprzężenie, ponieważ w rzeczywistości można zmienić zależności, które się dostaje, ale nadal istnieje zależność, której nie można wstrząsnąć: lokalizator usług. Staje się to tym, od czego wszystko zależy.

Kontener dostarczający zależności za pomocą argumentów konstruktora zapewnia największą przejrzystość. Widzimy od razu, że obiekt potrzebuje zarówno an AccountRepository, jak i a PasswordStrengthEvaluator. Podczas korzystania z lokalizatora usług informacje te są mniej widoczne od razu. Od razu zobaczysz przypadek, w którym obiekt ma 17 zależności, i powiesz sobie: „Hmm, to wydaje się dużo. Co się tam dzieje?” Połączenia z lokalizatorem usług mogą być rozłożone na różne metody i kryć się za logiką warunkową, i możesz nie zdawać sobie sprawy, że stworzyłeś „klasę Boga” - taką, która robi wszystko. Być może ta klasa mogłaby zostać przekształcona w 3 mniejsze klasy, które są bardziej skoncentrowane, a zatem bardziej testowalne.

Teraz rozważ testowanie. Jeśli obiekt korzysta z lokalizatora usług w celu uzyskania swoich zależności, środowisko testowe będzie również potrzebowało lokalizatora usług. W teście skonfigurujesz lokalizator usług, aby dostarczał zależności do testowanego obiektu - może a FakeAccountRepositoryi a VeryForgivingPasswordStrengthEvaluator, a następnie uruchom test. Ale to więcej pracy niż określenie zależności w konstruktorze obiektu. Twoja struktura testowa staje się również zależna od lokalizatora usług. Jest to kolejna rzecz, którą musisz skonfigurować w każdym teście, co sprawia, że ​​pisanie testów jest mniej atrakcyjne.

Poszukaj „Serivce Locator to antywzór” dla artykułu Marka Seemana na ten temat. Jeśli jesteś w świecie .Net, zdobądź jego książkę. To jest bardzo dobre.

Carl Raymond
źródło
1
Czytając pytanie, pomyślałem o Adaptive Code za pośrednictwem C # autorstwa Gary'ego McLeana Halla, co całkiem dobrze pasuje do tej odpowiedzi. Ma kilka dobrych analogii, takich jak lokalizator usług, który jest kluczem do sejfu; przekazany klasie może dowolnie tworzyć zależności, które mogą nie być łatwe do wykrycia.
Dennis
10
Jedną z rzeczy, którą IMO należy dodać do constructor supplied dependenciesvs, service locatorjest to, że pierwszą można zweryfikować w czasie kompilacji, a drugą można zweryfikować tylko w czasie wykonywania.
andras
2
Ponadto lokalizator usług nie zniechęca komponentu do posiadania wielu wielu zależności. Jeśli musisz dodać 10 parametrów do swojego konstruktora, masz całkiem dobre przeczucie, że coś jest nie tak z twoim kodem. Jeśli jednak zdarzy ci się wykonać 10 połączeń statycznych do lokalizatora usług, może nie być oczywiste, że masz jakiś problem. Pracowałem nad dużym projektem z lokalizatorem usług i był to największy problem. Zbyt łatwo było zewrzeć nową ścieżkę zamiast siedzieć, zastanawiać się, przeprojektowywać i refaktoryzować.
Tempo
1
O testowaniu - But that's more work than specifying dependencies in the object's constructor.chciałbym się sprzeciwić. W przypadku lokalizatora usług wystarczy tylko określić 3 zależności, które są rzeczywiście potrzebne do testu. W przypadku DI opartego na konstruktorze musisz podać WSZYSTKIE 10 z nich, nawet jeśli 7 jest nieużywanych.
Vilx-
4
@ Vilx - Jeśli masz wiele nieużywanych parametrów konstruktora, prawdopodobnie Twoja klasa narusza zasadę pojedynczej odpowiedzialności.
RB.
79

Wyobraź sobie, że jesteś pracownikiem fabryki produkującej buty .

Jesteś odpowiedzialny za składanie butów, więc będziesz potrzebować wielu rzeczy, aby to zrobić.

  • Skórzany
  • Miarka
  • Klej
  • Paznokcie
  • Młot
  • Nożyczki
  • Sznurówki

I tak dalej.

Jesteś w pracy w fabryce i możesz zacząć. Masz listę instrukcji, jak postępować, ale nie masz jeszcze żadnych materiałów ani narzędzi.

Lokalizator usług jest jak brygadzista, który pomoże ci zdobyć to, czego potrzebujesz.

Pytasz Lokalizator usług za każdym razem, gdy czegoś potrzebujesz, a oni idą znaleźć to dla Ciebie. Lokalizator usług został wcześniej poinformowany o tym, o co prawdopodobnie poprosisz i jak go znaleźć.

Miej jednak nadzieję, że nie poprosisz o coś nieoczekiwanego. Jeśli Lokalizator nie został wcześniej poinformowany o określonym narzędziu lub materiale, nie będzie w stanie go dla ciebie zdobyć i po prostu wzrusza ramionami.

lokalizator usług

Pojemnik do wstrzykiwania zależności (DI) jest jak duże pudełko, które wypełnia się wszystkim, czego wszyscy potrzebują na początku dnia.

Gdy fabryka zostaje uruchomiona, Wielki Szef znany jako Korzeń Kompozycji chwyta pojemnik i rozdaje wszystko Kierownikom Linii .

Kierownicy liniowi mają teraz to, czego potrzebują, aby wykonywać swoje obowiązki na dany dzień. Biorą to, co mają i przekazują to, co jest potrzebne, swoim podwładnym.

Proces ten trwa, a zależności spadają na linię produkcyjną. Ostatecznie dla majstra pojawia się pojemnik z materiałami i narzędziami.

Twój brygadzista rozdaje teraz dokładnie to, czego potrzebujesz tobie i innym pracownikom, nawet o to nie pytając.

Zasadniczo, jak tylko pojawi się w pracy, wszystko, czego potrzebujesz, jest już w pudełku i czeka na Ciebie. Nie musiałeś nic wiedzieć o tym, jak je zdobyć.

pojemnik do wstrzykiwań zależności

Rowan Freeman
źródło
45
Jest to doskonały opis tego, czym są te dwie rzeczy, również bardzo dobrze wyglądające diagramy! Ale to nie odpowiada na pytanie „Dlaczego wybrać jedną z drugiej”?
Matthieu M.
21
„Lepiej mieć jednak nadzieję, że nie poprosisz o coś nieoczekiwanego. Jeśli Lokalizator nie został wcześniej poinformowany o konkretnym narzędziu lub materiale”, ale może to również dotyczyć pojemnika DI.
Kenneth K.
5
@FrankHopkins Jeśli pominiesz rejestrację interfejsu / klasy w kontenerze DI, kod nadal będzie się kompilował i natychmiast zawiedzie w czasie wykonywania.
Kenneth K.
3
Oba są równie prawdopodobne, że zawiodą, w zależności od ich konfiguracji. Ale w przypadku kontenera DI bardziej prawdopodobne jest, że wpadniesz na problem wcześniej, gdy zbudowana zostanie klasa X, niż później, gdy klasa X faktycznie będzie potrzebować tej jednej zależności. Jest prawdopodobnie lepszy, ponieważ łatwiej jest napotkać problem, znaleźć go i rozwiązać. Zgadzam się jednak z Kennethem, że odpowiedź sugeruje, że ten problem występuje tylko w lokalizatorach usług, co zdecydowanie nie jest prawdą.
GolezTrol
3
Świetna odpowiedź, ale myślę, że brakuje jej jeszcze jednej rzeczy; to znaczy, jeśli zapytać majster na każdym etapie na słoniu, a on nie wie, jak dostać jeden, otrzyma jedną dla ciebie żadnych pytań asked- nawet jeśli nie jest to potrzebne. A ktoś, kto próbuje debugować problem na podłodze (programista, jeśli wolisz), będzie się zastanawiał, że wtf jest słoniem robiącym tutaj na tym biurku, co utrudnia im rozumowanie, co faktycznie poszło nie tak. Podczas gdy w DI, ty, klasa robotnicza, z góry deklarujesz każdą zależność, której potrzebujesz, zanim jeszcze zaczniesz pracę, ułatwiając w ten sposób rozumowanie.
Stephen Byrne
10

Kilka dodatkowych punktów, które znalazłem podczas przeszukiwania sieci:

  • Wstrzyknięcie zależności do konstruktora ułatwia zrozumienie, czego potrzebuje klasa. Nowoczesne środowiska IDE podpowiedzą, jakie argumenty przyjmuje konstruktor i jakie są ich typy. Jeśli używasz lokalizatora usług, musisz przeczytać klasę, zanim będziesz wiedział, jakie zależności są wymagane.
  • Zastrzyki zależności wydają się być bardziej zgodne z zasadą „nie pytaj” niż lokalizatory usług. Nakazując, aby zależność była określonego typu, „informujesz”, które zależności są wymagane. Nie można utworzyć instancji klasy bez przekazania wymaganych zależności. W lokalizatorze usług „pytasz” o usługę, a jeśli lokalizator usług nie jest poprawnie skonfigurowany, możesz nie uzyskać wymaganej usługi.
tom6025222
źródło
4

Spóźniam się na to przyjęcie, ale nie mogę się oprzeć.

Jaka jest różnica między używaniem wstrzykiwania zależności w kontenerze a używaniem lokalizatora usług?

Czasami wcale. Różnica polega na tym, co wie o czym.

Wiesz, że używasz lokalizatora usług, gdy klient szukający zależności wie o kontenerze. Klient umiejący znaleźć swoje zależności, nawet przechodząc przez kontener w celu ich uzyskania, jest wzorcem lokalizatora usług.

Czy to oznacza, że ​​jeśli chcesz uniknąć lokalizatora usług, nie możesz użyć kontenera? Nie. Musisz tylko powstrzymać klientów przed wiedzą o kontenerze. Kluczową różnicą jest to, gdzie używasz kontenera.

Pozwala powiedzieć Clientpotrzeby Dependency. Kontener ma Dependency.

class Client { 
    Client() { 
        BeanFactory beanfactory = new ClassPathXmlApplicationContext("Beans.xml");
        this.dependency = (Dependency) beanfactory.getBean("dependency");        
    }
    Dependency dependency;
}

Właśnie zastosowaliśmy się do wzorca lokalizatora usług, ponieważ Clientwie, jak znaleźć Dependency. Jasne, że używa twardego kodu, ClassPathXmlApplicationContextale nawet jeśli wstrzykujesz, że nadal masz lokalizator usług, ponieważ Clientpołączenia beanfactory.getBean().

Aby uniknąć lokalizatora usług, nie musisz porzucać tego kontenera. Musisz go po prostu usunąć, Clientwięc Clientnie wiem o tym.

class EntryPoint { 
    public static void main(String[] args) {
        BeanFactory beanfactory = new ClassPathXmlApplicationContext("Beans.xml");
        Client client = (Client) beanfactory.getBean("client");

        client.start();
    }
}

<?xml version="1.0" encoding="UTF-8"?>
 <beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
  http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

    <bean id="dependency" class="Dependency">
    </bean>

    <bean id="client" class="Client">
        <constructor-arg value="dependency" />        
    </bean>
</beans>

Zauważ, że Clientteraz nie ma pojęcia, że ​​kontener istnieje:

class Client { 
    Client(Dependency dependency) { 

        this.dependency = dependency;        
    }
    Dependency dependency;
}

Przenieś pojemnik ze wszystkich klientów i umieść go w głównym miejscu, w którym może zbudować wykres obiektów wszystkich twoich długo żyjących obiektów. Wybierz jeden z tych obiektów do wyodrębnienia i wywołaj na nim metodę, a ty zaczniesz zaznaczanie całego wykresu.

To przenosi całą statyczną konstrukcję do XML-a kontenerów, a jednocześnie sprawia, że ​​wszyscy klienci są błogo nieświadomi, jak znaleźć ich zależności.

Ale główny nadal wie, jak zlokalizować zależności! Tak. Ale nie rozpowszechniając tej wiedzy, uniknąłeś podstawowego problemu lokalizatora usług. Decyzja o użyciu kontenera jest teraz podejmowana w jednym miejscu i można ją zmienić bez przepisywania setek klientów.

candied_orange
źródło
1

Myślę, że najłatwiejszym sposobem na zrozumienie różnicy między tymi dwoma i dlaczego kontener DI jest o wiele lepszy niż lokalizator usług, to zastanowienie się, dlaczego w pierwszej kolejności dokonujemy inwersji zależności.

Dokonujemy inwersji zależności, aby każda klasa wyraźnie określała dokładnie, od czego zależy działanie. Robimy to, ponieważ tworzy to najsłabsze sprzęgło, jakie możemy osiągnąć. Im luźniejsze sprzężenie, tym łatwiej coś przetestować i zrefaktoryzować (i generalnie wymaga to najmniej refaktoryzacji w przyszłości, ponieważ kod jest czystszy).

Spójrzmy na następującą klasę:

public class MySpecialStringWriter
{
  private readonly IOutputProvider outputProvider;
  public MySpecialFormatter(IOutputProvider outputProvider)
  {
    this.outputProvider = outputProvider;
  }

  public void OutputString(string source)
  {
    this.outputProvider.Output("This is the string that was passed: " + source);
  }
}

W tej klasie wyraźnie stwierdzamy, że potrzebujemy IOutputProvider i nic więcej, aby ta klasa działała. Jest to w pełni testowalne i zależy od jednego interfejsu. Mogę przenieść tę klasę do dowolnego miejsca w mojej aplikacji, w tym do innego projektu, a wszystko czego potrzebuje to dostęp do interfejsu IOutputProvider. Jeśli inni programiści chcą dodać coś nowego do tej klasy, co wymaga drugiej zależności, muszą wyraźnie powiedzieć, czego potrzebują w konstruktorze.

Spójrz na tę samą klasę z lokalizatorem usług:

public class MySpecialStringWriter
{
  private readonly ServiceLocator serviceLocator;
  public MySpecialFormatter(ServiceLocator serviceLocator)
  {
    this.serviceLocator = serviceLocator;
  }

  public void OutputString(string source)
  {
    this.serviceLocator.OutputProvider.Output("This is the string that was passed: " + source);
  }
}

Teraz dodałem lokalizator usług jako zależność. Oto problemy, które są od razu oczywiste:

  • Pierwszym problemem jest to, że potrzeba więcej kodu, aby osiągnąć ten sam wynik. Więcej kodu jest złe. To niewiele więcej kodu, ale wciąż więcej.
  • Drugi problem polega na tym, że moja zależność nie jest już wyraźna . Nadal muszę wstrzyknąć coś do klasy. Z wyjątkiem teraz rzecz, której chcę, nie jest wyraźna. Jest ukryty we właściwości rzeczy, o którą prosiłem. Teraz potrzebuję dostępu zarówno do ServiceLocator, jak i IOutputProvider, jeśli chcę przenieść klasę do innego zestawu.
  • Trzeci problem polega na tym, że dodatkowa zależność może zostać przejęta przez innego programistę, który nawet nie zdaje sobie sprawy, że bierze ją, gdy dodaje kod do klasy.
  • W końcu ten kod jest trudniejszy do przetestowania (nawet jeśli ServiceLocator jest interfejsem), ponieważ musimy wyśmiewać ServiceLocator i IOutputProvider zamiast tylko IOutputProvider

Dlaczego więc nie uczynić lokalizatora usług klasą statyczną? Spójrzmy:

public class MySpecialStringWriter
{
  public void OutputString(string source)
  {
    ServiceLocator.OutputProvider.Output("This is the string that was passed: " + source);
  }
}

To jest o wiele prostsze, prawda?

Źle.

Powiedzmy, że IOutputProvider jest implementowany przez bardzo długo działającą usługę internetową, która zapisuje ciąg znaków w piętnastu różnych bazach danych na całym świecie i zajmuje bardzo dużo czasu.

Spróbujmy przetestować tę klasę. Do testu potrzebujemy innej implementacji IOutputProvider. Jak piszemy test?

Aby to zrobić, musimy wykonać fantazyjną konfigurację w statycznej klasie ServiceLocator, aby użyć innej implementacji IOutputProvider, gdy jest ona wywoływana przez test. Nawet napisanie tego zdania było bolesne. Wdrożenie tego byłoby torturujące i byłoby koszmarem konserwacyjnym . Nigdy nie powinniśmy musieć modyfikować klasy specjalnie do testowania, szczególnie jeśli ta klasa nie jest klasą, którą faktycznie próbujemy przetestować.

Pozostaje Ci albo a) test, który powoduje natrętne zmiany kodu w niepowiązanej klasie ServiceLocator; lub b) brak testu. I pozostaje Ci również mniej elastyczne rozwiązanie.

Zatem klasa lokalizatora usług musi zostać wstrzyknięta do konstruktora. Co oznacza, że ​​pozostały nam konkretne problemy wspomniane wcześniej. Lokalizator usług wymaga więcej kodu, mówi innym programistom, że potrzebuje rzeczy, których nie potrzebuje, zachęca innych programistów do pisania gorszego kodu i daje nam mniejszą elastyczność w posuwaniu się naprzód.

Mówiąc prosto , lokalizatory usług zwiększają sprzężenie w aplikacji i zachęcają innych programistów do pisania wysoce sprzężonego kodu .

Stephen
źródło
Lokalizatory usług (SL) i wstrzykiwanie zależności (DI) rozwiązują ten sam problem w podobny sposób. Jedyna różnica, jeśli „mówisz” DI lub „pytasz” SL o zależności.
Matthew Whited
@MatthewWhited Główną różnicą jest liczba niejawnych zależności, które przyjmujesz za pomocą lokalizatora usług. To ogromnie wpływa na długoterminową łatwość konserwacji i stabilność kodu.
Stephen
Naprawdę nie. Tak czy inaczej, masz taką samą liczbę zależności.
Matthew Whited
Początkowo tak, ale możesz wziąć zależności za pomocą lokalizatora usług, nie zdając sobie sprawy, że bierzesz zależności. Który pokonuje dużą część powodu, dla którego dokonujemy inwersji zależności. Jedna klasa zależy od właściwości A i B, inna zależy od B i C. Nagle Lokalizator usług staje się klasą bogów. Kontener DI nie, a obie klasy są właściwie zależne tylko odpowiednio od A i B oraz B i C. To sprawia, że ​​refaktoryzacja ich sto razy łatwiej. Lokalizatory usług są podstępne, ponieważ wyglądają tak samo jak DI, ale nie są.
Stephen