Was ist der Unterschied zwischen der Abhängigkeitsinjektion mit einem Container und der Verwendung eines Service-Locators?

107

Ich verstehe, dass das direkte Instanziieren von Abhängigkeiten innerhalb einer Klasse als schlechte Praxis angesehen wird. Dies ist sinnvoll, da alles so eng miteinander verbunden ist, was wiederum das Testen sehr schwierig macht.

Fast alle Frameworks, auf die ich gestoßen bin, scheinen die Abhängigkeitsinjektion mit einem Container der Verwendung von Service-Locators vorzuziehen. Beide scheinen dasselbe zu erreichen, indem sie es dem Programmierer ermöglichen, anzugeben, welches Objekt zurückgegeben werden soll, wenn eine Klasse eine Abhängigkeit erfordert.

Was ist der Unterschied zwischen den beiden? Warum sollte ich eine der anderen vorziehen?

tom6025222
quelle
3
Dies wurde auf StackOverflow
Kyralessa
2
Meiner Meinung nach ist es keineswegs ungewöhnlich, dass Entwickler andere in die Irre führen, dass ihr Service-Locator eine Abhängigkeitsinjektion ist. Sie tun dies, weil die Abhängigkeitsinjektion oft als weiter fortgeschritten angesehen wird.
Gherman
1
Ich bin überrascht, dass niemand erwähnt hat, dass es mit Service Locators schwieriger ist zu wissen, wann eine vorübergehende Ressource zerstört werden kann. Ich dachte immer, das sei der Hauptgrund.
Buh Buh

Antworten:

126

Wenn das Objekt selbst dafür verantwortlich ist, seine Abhängigkeiten anzufordern, anstatt sie über einen Konstruktor zu akzeptieren, werden einige wichtige Informationen ausgeblendet. Es ist nur geringfügig besser als der sehr eng gekoppelte Fall new, Abhängigkeiten mithilfe von zu instanziieren. Es reduziert die Kopplung, da Sie tatsächlich die Abhängigkeiten ändern können, die es erhält, aber es hat immer noch eine Abhängigkeit, die es nicht erschüttern kann: den Service-Locator. Das wird das, wovon alles abhängt.

Ein Container, der Abhängigkeiten über Konstruktorargumente bereitstellt, bietet die größte Übersichtlichkeit. Wir sehen von vornherein, dass ein Objekt sowohl ein AccountRepositoryals auch ein braucht PasswordStrengthEvaluator. Wenn Sie einen Service Locator verwenden, werden diese Informationen nicht sofort angezeigt. Sie würden sofort einen Fall sehen, in dem ein Objekt 17 Abhängigkeiten hat, und sich selbst sagen: "Hmm, das scheint viel zu sein. Was ist dort los?" Anrufe an einen Service Locator können sich auf die verschiedenen Methoden verteilen und sich hinter bedingter Logik verstecken, und Sie bemerken möglicherweise nicht, dass Sie eine "Gott-Klasse" erstellt haben - eine, die alles tut. Vielleicht könnte diese Klasse in 3 kleinere Klassen umgestaltet werden, die fokussierter und daher überprüfbarer sind.

Betrachten Sie nun das Testen. Wenn ein Objekt einen Service-Locator verwendet, um seine Abhängigkeiten zu ermitteln, benötigt Ihr Test-Framework auch einen Service-Locator. In einem Test konfigurieren Sie den Service-Locator so, dass die Abhängigkeiten für das zu testende Objekt bereitgestellt werden - möglicherweise ein FakeAccountRepositoryund ein VeryForgivingPasswordStrengthEvaluator, und führen Sie dann den Test aus. Dies ist jedoch mehr Arbeit als die Angabe von Abhängigkeiten im Konstruktor des Objekts. Und Ihr Test-Framework wird auch vom Service Locator abhängig. Es ist eine andere Sache, die Sie in jedem Test konfigurieren müssen, was das Schreiben von Tests weniger attraktiv macht.

Suchen Sie nach "Serivce Locator ist ein Anti-Pattern" für Mark Seemans Artikel darüber. Wenn Sie in der .Net-Welt sind, holen Sie sich sein Buch. Es ist sehr gut.

Carl Raymond
quelle
1
Als ich die Frage las, dachte ich über Adaptive Code via C # von Gary McLean Hall nach, was ziemlich gut zu dieser Antwort passt. Es gibt einige gute Analogien, zum Beispiel, dass der Service Locator der Schlüssel zu einem Safe ist. Wenn es einer Klasse übergeben wird, kann es nach Belieben zu Abhängigkeiten kommen, die möglicherweise nicht leicht zu erkennen sind.
Dennis
10
Eine Sache, die IMO zum constructor supplied dependenciesvs hinzufügen muss, service locatorist, dass das erstere zur Kompilierungszeit überprüft werden kann, während das letztere nur zur Laufzeit überprüft werden kann.
andras
2
Darüber hinaus hält ein Service-Locator eine Komponente nicht davon ab, viele Abhängigkeiten aufzuweisen. Wenn Sie 10 Parameter zu Ihrem Konstruktor hinzufügen müssen, haben Sie ein ziemlich gutes Gefühl, dass etwas mit Ihrem Code nicht stimmt. Wenn Sie jedoch zufällig 10 statische Anrufe an einen Service Locator tätigen, ist es möglicherweise nicht ohne weiteres ersichtlich, dass ein Problem vorliegt. Ich habe an einem großen Projekt mit einem Service Locator gearbeitet und dies war das größte Problem. Es war einfach zu einfach, einen neuen Weg kurzzuschließen, anstatt sich zurückzulehnen, zu reflektieren, neu zu gestalten und neu zu gestalten.
Pace
1
Über das Testen - But that's more work than specifying dependencies in the object's constructor.ich möchte Einwände erheben. Mit einem Service Locator müssen Sie nur die 3 Abhängigkeiten angeben, die Sie tatsächlich für Ihren Test benötigen. Bei einer konstruktorbasierten DI müssen Sie ALLE 10 angeben, auch wenn 7 nicht verwendet werden.
Vilx
4
@ Vilx- Wenn Sie mehrere nicht verwendete Konstruktorparameter haben, verstößt Ihre Klasse wahrscheinlich gegen das Prinzip der einfachen Verantwortung.
RB.
79

Stellen Sie sich vor, Sie sind Arbeiter in einer Fabrik, die Schuhe herstellt .

Sie sind verantwortlich für die Montage der Schuhe und so werden Sie eine Menge Dinge, um das zu tun.

  • Leder
  • Maßband
  • Kleben
  • Nägel
  • Hammer
  • Schere
  • Schnürsenkel

Und so weiter.

Sie arbeiten in der Fabrik und können loslegen. Sie haben eine Liste mit Anweisungen, wie Sie vorgehen sollen, aber Sie haben noch keine Materialien oder Werkzeuge.

Ein Service Locator ist wie ein Foreman, der Ihnen dabei hilft, das zu bekommen, was Sie brauchen.

Sie fragen den Service Locator jedes Mal, wenn Sie etwas benötigen, und er sucht es für Sie. Dem Service Locator wurde im Voraus mitgeteilt, wonach Sie wahrscheinlich fragen und wie Sie ihn finden.

Du solltest besser hoffen, dass du nicht nach etwas Unerwartetem fragst. Wenn der Locator nicht im Voraus über ein bestimmtes Werkzeug oder Material informiert wurde, kann er es nicht für Sie besorgen und zuckt nur mit den Schultern.

Service Locator

Ein Dependency Injection (DI) -Container ist wie eine große Kiste, die zu Beginn des Tages mit allem gefüllt wird, was jeder braucht.

Beim Start der Fabrik greift der Big Boss, der als Composition Root bekannt ist, nach dem Container und übergibt alles an die Linienmanager .

Die Linienvorgesetzten haben jetzt das, was sie für ihre täglichen Aufgaben benötigen. Sie nehmen das, was sie haben und geben das, was sie brauchen, an ihre Untergebenen weiter.

Dieser Prozess setzt sich fort, wobei Abhängigkeiten die Produktionslinie durchdringen. Schließlich erscheint ein Behälter mit Materialien und Werkzeugen für Ihren Vorarbeiter.

Ihr Vorarbeiter verteilt jetzt genau das, was Sie benötigen, an Sie und andere Mitarbeiter, ohne dass Sie danach fragen.

Sobald Sie zur Arbeit erscheinen, ist praktisch alles, was Sie brauchen, bereits in einer Kiste für Sie bereit. Du musstest nichts darüber wissen, wie du sie bekommst.

Abhängigkeit Injektionsbehälter

Rowan Freeman
quelle
45
Dies ist eine exzellente Beschreibung der beiden Dinge, super gut aussehende Diagramme auch! Dies beantwortet jedoch nicht die Frage "Warum eine über die andere wählen"?
Matthieu M.
21
"Sie sollten jedoch hoffen, dass Sie nicht nach etwas Unerwartetem fragen. Wenn der Locator nicht im Voraus über ein bestimmtes Werkzeug oder Material informiert wurde." Dies kann jedoch auch für einen DI-Container zutreffen.
Kenneth K.
5
@FrankHopkins Wenn Sie die Registrierung einer Schnittstelle / Klasse in Ihrem DI-Container auslassen, wird der Code weiterhin kompiliert und schlägt zur Laufzeit umgehend fehl.
Kenneth K.
3
Beide schlagen wahrscheinlich gleichermaßen fehl, je nachdem, wie sie eingerichtet sind. Aber mit einem DI-Container stoßen Sie eher auf ein Problem, wenn Klasse X erstellt wird, als später, wenn Klasse X tatsächlich diese eine Abhängigkeit benötigt. Es ist wohl besser, weil es einfacher ist, auf das Problem zu stoßen, es zu finden und es zu lösen. Ich stimme jedoch Kenneth zu, dass die Antwort besagt, dass dieses Problem nur für Servicelokalisierer besteht, was definitiv nicht der Fall ist.
GolezTrol
3
Gute Antwort, aber ich denke, es fehlt eine andere Sache; Das heißt, wenn Sie den Vorarbeiter zu irgendeinem Zeitpunkt nach einem Elefanten fragen und er weiß , wie er einen bekommt, bekommt er einen für Sie, ohne dass Fragen gestellt werden - auch wenn Sie ihn nicht benötigen. Und jemand, der versucht, ein Problem auf dem Boden zu beheben (ein Entwickler, wenn Sie so wollen), wird sich fragen, ob wtf ein Elefant ist, der hier auf diesem Schreibtisch arbeitet, wodurch es für ihn schwieriger wird, zu überlegen, was tatsächlich schief gelaufen ist. Während Sie, die Arbeiterklasse, mit DI jede Abhängigkeit, die Sie benötigen , im Voraus deklarieren, bevor Sie überhaupt mit der Arbeit beginnen, wodurch es einfacher wird, darüber nachzudenken.
Stephen Byrne
10

Ein paar zusätzliche Punkte, die ich beim Durchsuchen des Webs gefunden habe:

  • Das Einfügen von Abhängigkeiten in den Konstruktor erleichtert das Verstehen der Anforderungen einer Klasse. Moderne IDEs geben Hinweise darauf, welche Argumente der Konstruktor akzeptiert und welche Typen sie haben. Wenn Sie einen Service Locator verwenden, müssen Sie die Klasse durchlesen, bevor Sie wissen, welche Abhängigkeiten erforderlich sind.
  • Die Abhängigkeitsinjektion scheint eher dem Prinzip "Tell Don't Ask" zu entsprechen als Service Locators. Indem Sie festlegen, dass eine Abhängigkeit von einem bestimmten Typ sein soll, "sagen" Sie, welche Abhängigkeiten erforderlich sind. Es ist unmöglich, die Klasse zu instanziieren, ohne die erforderlichen Abhängigkeiten zu übergeben. Mit einem Service Locator "fragen" Sie nach einem Service und wenn der Service Locator nicht richtig konfiguriert ist, erhalten Sie möglicherweise nicht das, was erforderlich ist.
tom6025222
quelle
4

Ich komme zu spät zu dieser Party, aber ich kann nicht widerstehen.

Was ist der Unterschied zwischen der Abhängigkeitsinjektion mit einem Container und der Verwendung eines Service-Locators?

Manchmal überhaupt keine. Was den Unterschied ausmacht, ist, was über was weiß.

Sie wissen, dass Sie einen Service-Locator verwenden, wenn der Client, der die Abhängigkeit sucht, über den Container Bescheid weiß. Ein Client, der weiß, wie er seine Abhängigkeiten findet, auch wenn er einen Container durchsucht, um sie abzurufen, ist das Service-Locator-Muster.

Heißt das, wenn Sie den Service Locator umgehen möchten, können Sie keinen Container verwenden? Nein. Sie müssen nur Kunden davon abhalten, über den Container Bescheid zu wissen. Der Hauptunterschied besteht darin, wo Sie den Container verwenden.

Sagen wir ClientBedürfnisse Dependency. Der Behälter hat eine Dependency.

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

Wir haben uns gerade an das Suchmuster gehalten, weil wir Clientwissen, wie man es findet Dependency. Sicher , es wird ein hart codiert , ClassPathXmlApplicationContextaber auch wenn Sie injizieren , dass Sie noch einen Service Locator da haben ClientAnrufe beanfactory.getBean().

Um den Service Locator zu umgehen, müssen Sie diesen Container nicht verlassen. Sie müssen es nur herausziehen, Clientdamit Sie Clientnichts davon wissen.

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>

Beachten Sie, wie Clientjetzt keine Ahnung hat, dass der Container existiert:

class Client { 
    Client(Dependency dependency) { 

        this.dependency = dependency;        
    }
    Dependency dependency;
}

Verschieben Sie den Container aus allen Clients und platzieren Sie ihn in main, um einen Objektgraphen aller Ihrer langlebigen Objekte zu erstellen. Wählen Sie eines dieser Objekte aus, um es zu extrahieren und eine Methode aufzurufen, und Sie beginnen mit dem Ticking des gesamten Diagramms.

Dadurch wird die gesamte statische Konstruktion in die XML-Container verschoben, und Ihre Kunden wissen nicht, wie sie ihre Abhängigkeiten finden können.

Aber main weiß immer noch, wie man Abhängigkeiten findet! Ja tut es. Indem Sie dieses Wissen jedoch nicht verbreiten, vermeiden Sie das Kernproblem des Service Locator. Die Entscheidung, einen Container zu verwenden, wird nun an einem Ort getroffen und kann geändert werden, ohne dass Hunderte von Clients neu geschrieben werden müssen.

kandierte_orange
quelle
1

Ich denke, der einfachste Weg, um den Unterschied zwischen den beiden zu verstehen und warum ein DI-Container so viel besser ist als ein Service-Locator, ist zu überlegen, warum wir überhaupt eine Abhängigkeitsinversion durchführen.

Wir führen eine Abhängigkeitsinversion durch, damit jede Klasse explizit angibt, wovon sie für die Operation abhängig ist . Wir tun dies, weil dies die lockerste Kopplung schafft, die wir erreichen können. Je lockerer die Kopplung ist, desto einfacher ist es, etwas zu testen und umzugestalten (und in der Regel ist das geringste Refactoring in der Zukunft erforderlich, da der Code sauberer ist).

Schauen wir uns die folgende Klasse an:

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

In dieser Klasse geben wir ausdrücklich an, dass wir einen IOutputProvider und nichts anderes benötigen, damit diese Klasse funktioniert. Dies ist vollständig testbar und hängt von einer einzelnen Schnittstelle ab. Ich kann diese Klasse an eine beliebige Stelle in meiner Anwendung verschieben, einschließlich eines anderen Projekts. Sie benötigt lediglich Zugriff auf die IOutputProvider-Schnittstelle. Wenn andere Entwickler dieser Klasse etwas Neues hinzufügen möchten, was eine zweite Abhängigkeit erfordert, müssen sie explizit angeben, was sie im Konstruktor benötigen.

Schauen Sie sich dieselbe Klasse mit einem Service-Locator an:

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

Jetzt habe ich den Service Locator als Abhängigkeit hinzugefügt. Hier sind die Probleme, die sofort offensichtlich sind:

  • Das allererste Problem dabei ist, dass mehr Code erforderlich ist , um das gleiche Ergebnis zu erzielen. Mehr Code ist schlecht. Es ist nicht viel mehr Code, aber es ist immer noch mehr.
  • Das zweite Problem ist, dass meine Abhängigkeit nicht mehr explizit ist . Ich muss noch etwas in die Klasse spritzen. Außer jetzt ist das, was ich will, nicht explizit. Es ist in einer Eigenschaft der Sache versteckt, die ich angefordert habe. Jetzt muss ich sowohl auf den ServiceLocator als auch auf den IOutputProvider zugreifen können, um die Klasse in eine andere Assembly zu verschieben.
  • Das dritte Problem ist, dass eine zusätzliche Abhängigkeit von einem anderen Entwickler eingenommen werden kann, der nicht einmal merkt, dass er sie einnimmt, wenn er der Klasse Code hinzufügt.
  • Schließlich ist dieser Code schwieriger zu testen (auch wenn ServiceLocator eine Schnittstelle ist), da wir ServiceLocator und IOutputProvider anstelle von IOutputProvider verspotten müssen

Warum machen wir den Service Locator nicht zu einer statischen Klasse? Lass uns mal sehen:

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

Das ist doch viel einfacher, oder?

Falsch.

Angenommen, IOutputProvider wird von einem sehr lang laufenden Webdienst implementiert, der die Zeichenfolge in fünfzehn verschiedenen Datenbanken auf der ganzen Welt schreibt und dessen Fertigstellung sehr lange dauert.

Lassen Sie uns versuchen, diese Klasse zu testen. Für den Test benötigen wir eine andere Implementierung von IOutputProvider. Wie schreiben wir den Test?

Dazu müssen wir einige ausgefallene Konfigurationen in der statischen ServiceLocator-Klasse vornehmen, um eine andere Implementierung von IOutputProvider zu verwenden, wenn dieser vom Test aufgerufen wird. Sogar das Schreiben dieses Satzes war schmerzhaft. Die Umsetzung wäre mühsam und ein Alptraum für die Instandhaltung . Wir sollten niemals eine Klasse speziell zum Testen modifizieren müssen, insbesondere wenn diese Klasse nicht die Klasse ist, die wir tatsächlich testen möchten.

Jetzt haben Sie entweder a) einen Test, der auffällige Codeänderungen in der nicht verwandten ServiceLocator-Klasse verursacht; oder b) überhaupt kein Test. Und Ihnen bleibt auch eine weniger flexible Lösung.

So ist die Service - Locator - Klasse hat in den Konstruktor injiziert werden. Was bedeutet, dass wir mit den zuvor erwähnten spezifischen Problemen zurückbleiben. Der Service Locator benötigt mehr Code, teilt anderen Entwicklern mit, dass er Dinge benötigt, die er nicht benötigt, ermutigt andere Entwickler, schlechteren Code zu schreiben, und gibt uns weniger Flexibilität bei der Weitergabe.

Einfach ausgedrückt: Service Locators erhöhen die Kopplung in einer Anwendung und ermutigen andere Entwickler, stark gekoppelten Code zu schreiben .

Stephen
quelle
Service Locators (SL) und Dependency Injection (DI) lösen dasselbe Problem auf ähnliche Weise. Der einzige Unterschied, wenn Sie DI "erzählen" oder SL nach Abhängigkeiten "fragen".
Matthew Whited
@MatthewWhited Der Hauptunterschied ist die Anzahl der impliziten Abhängigkeiten, die Sie mit einem Service Locator eingehen. Dies wirkt sich massiv auf die langfristige Wartbarkeit und Stabilität des Codes aus.
Stephen
Wirklich nicht. Sie haben in beiden Fällen die gleiche Anzahl von Abhängigkeiten.
Matthew Whited
Anfangs ja, aber Sie können Abhängigkeiten mit einem Service-Locator erfassen, ohne zu bemerken, dass Sie Abhängigkeiten erfassen. Was einen großen Teil des Grundes besiegt, warum wir überhaupt eine Abhängigkeitsinversion durchführen. Eine Klasse hängt von den Eigenschaften A und B ab, eine andere von B und C. Plötzlich wird der Service Locator zu einer Gottklasse. Der DI-Container funktioniert nicht und die beiden Klassen sind nur von A und B bzw. B und C abhängig. Dies erleichtert die hundertfache Umgestaltung. Service Locators sind insofern heimtückisch, als sie genauso aussehen wie DI, aber sie sind es nicht.
Stephen