wake-up-neo.com

Ist ServiceLocator ein Anti-Pattern?

Vor kurzem habe ich Mark Seemanns Artikel über Service Locator Anti-Pattern gelesen.

Der Autor weist auf zwei Hauptgründe hin, warum ServiceLocator ein Anti-Pattern ist:

  1. API-Nutzungsproblem (was mir sehr gut geht)
    Wenn eine Klasse einen Service Locator verwendet, sind ihre Abhängigkeiten sehr schwer zu erkennen, da die Klasse in den meisten Fällen nur einen PARAMETERLESS-Konstruktor aufweist .. Im Gegensatz zum ServiceLocator legt der DI-Ansatz Abhängigkeiten explizit über Konstruktorparameter frei, sodass Abhängigkeiten einfach sind gesehen in IntelliSense.

  2. Wartungsproblem (was mich verwirrt)
    .__ Betrachten Sie das folgende Beispiel 

Wir haben eine Klasse 'MyType', die einen Service Locator-Ansatz verwendet: 

public class MyType
{
    public void MyMethod()
    {
        var dep1 = Locator.Resolve<IDep1>();
        dep1.DoSomething();
    }
}

Jetzt möchten wir der Klasse 'MyType' eine weitere Abhängigkeit hinzufügen.

public class MyType
{
    public void MyMethod()
    {
        var dep1 = Locator.Resolve<IDep1>();
        dep1.DoSomething();

        // new dependency
        var dep2 = Locator.Resolve<IDep2>();
        dep2.DoSomething();
    }
}

Und hier beginnt mein Missverständnis. Der Autor sagt: 

Es wird viel schwieriger zu sagen, ob Sie eine bahnbrechende Änderung einführen oder nicht. Sie müssen die gesamte Anwendung kennen, in der der Service Locator verwendet wird, und der Compiler wird Ihnen nicht helfen.

Aber warten Sie bitte eine Sekunde. Wenn Sie den DI-Ansatz verwenden, würden wir im Konstruktor eine Abhängigkeit mit einem anderen Parameter einführen (im Falle einer Konstruktorinjektion). Und das Problem wird immer noch da sein. Wenn wir vergessen, ServiceLocator einzurichten, vergessen wir möglicherweise, eine neue Zuordnung in unseren IoC-Container hinzuzufügen, und der DI-Ansatz hätte das gleiche Laufzeitproblem.

Der Autor erwähnte auch die Schwierigkeiten bei der Prüfung von Einheiten. Aber haben wir keine Probleme mit dem DI-Ansatz? Müssen wir nicht alle Tests aktualisieren, die diese Klasse instanziiert haben? Wir werden sie aktualisieren, um eine neue mutmaßliche Abhängigkeit zu bestehen, um unseren Test kompilierbar zu machen. Und ich sehe keine Vorteile von diesem Update und Zeitaufwand.

Ich versuche nicht, den Service Locator-Ansatz zu verteidigen. Aber dieses Missverständnis lässt mich denken, dass ich etwas sehr Wichtiges verliere. Könnte jemand meine Zweifel zerstreuen?

UPDATE (ZUSAMMENFASSUNG):

Die Antwort auf meine Frage "Ist Service Locator ein Anti-Muster" hängt wirklich von den Umständen ab. Und ich würde definitiv nicht vorschlagen, es aus Ihrer Werkzeugliste zu streichen. Es kann sehr praktisch sein, wenn Sie sich mit altem Code beschäftigen. Wenn Sie das Glück haben, ganz am Anfang Ihres Projekts zu sein, ist der DI-Ansatz möglicherweise die bessere Wahl, da er einige Vorteile gegenüber dem Service Locator bietet.

Und hier sind die wichtigsten Unterschiede, die mich überzeugt haben, Service Locator nicht für meine neuen Projekte zu verwenden:

  • Am offensichtlichsten und am wichtigsten: Service Locator verbirgt Klassenabhängigkeiten
  • Wenn Sie einen IoC-Container verwenden, wird der Konstruktor beim Start wahrscheinlich durchsucht, um alle Abhängigkeiten zu überprüfen, und Sie erhalten umgehend Feedback zu fehlenden Zuordnungen (oder einer falschen Konfiguration). Dies ist nicht möglich, wenn Sie Ihren IoC-Container als Service Locator verwenden

Für Details lesen Sie die ausgezeichneten Antworten, die unten angegeben sind.

120
davidoff

Wenn Sie Muster als Anti-Muster definieren, nur weil es einige Situationen gibt, in denen es nicht passt, ist JA ein Anti-Muster. Aber mit dieser Begründung wären alle Muster auch Anti-Muster.

Stattdessen müssen wir prüfen, ob es gültige Verwendungen der Muster gibt, und für Service Locator gibt es mehrere Anwendungsfälle. Aber schauen wir uns zunächst die Beispiele an, die Sie gegeben haben.

public class MyType
{
    public void MyMethod()
    {
        var dep1 = Locator.Resolve<IDep1>();
        dep1.DoSomething();

        // new dependency
        var dep2 = Locator.Resolve<IDep2>();
        dep2.DoSomething();
    }
}

Der Wartungs-Albtraum mit dieser Klasse besteht darin, dass die Abhängigkeiten verborgen sind. Wenn Sie diese Klasse erstellen und verwenden:

var myType = new MyType();
myType.MyMethod();

Sie verstehen nicht, dass Abhängigkeiten bestehen, wenn sie mithilfe des Servicestandorts ausgeblendet werden. Wenn wir stattdessen Abhängigkeitsinjektion verwenden:

public class MyType
{
    public MyType(IDep1 dep1, IDep2 dep2)
    {
    }

    public void MyMethod()
    {
        dep1.DoSomething();

        // new dependency
        dep2.DoSomething();
    }
}

Sie können die Abhängigkeiten direkt erkennen und die Klassen nicht verwenden, bevor Sie sie befriedigen.

In einer typischen Geschäftsanwendung sollten Sie die Verwendung des Service-Standorts aus genau diesem Grund vermeiden. Es sollte das Muster sein, das zu verwenden ist, wenn keine anderen Optionen vorhanden sind.

Ist das Muster ein Anti-Muster?

Nein. 

Zum Beispiel würde das Umkehren von Kontrollcontainern ohne Service-Standort nicht funktionieren. So lösen sie die Dienste intern auf.

Ein viel besseres Beispiel ist jedoch ASP.NET MVC und WebApi. Was macht Ihrer Meinung nach die Abhängigkeitsinjektion in die Steuerungen möglich? Richtig - Servicestandort.

Deine Fragen

Aber warte mal, wenn wir DI-Ansatz verwenden würden, würden wir ein .__ einführen. Abhängigkeit von einem anderen Parameter im Konstruktor (im Fall von Konstruktorinjektion). Und das Problem wird immer noch da sein. 

Es gibt zwei weitere schwerwiegende Probleme:

  1. Mit dem Servicestandort fügen Sie auch eine weitere Abhängigkeit hinzu: Den Service Locator. 
  2. Wie können Sie feststellen, welche Lebensdauer die Abhängigkeiten haben sollten und wie und wann sie aufgeräumt werden sollten?

Mit der Konstruktorinjektion über einen Container erhalten Sie das kostenlos.

Wenn wir dürfen. Vergessen Sie nicht, ServiceLocator einzurichten. Dann vergessen wir möglicherweise, ein neues .__ hinzuzufügen. Mapping in unserem IoC-Container und DI-Ansatz hätte das gleiche Laufzeitproblem.

Das ist richtig. Bei der Konstruktorinjektion müssen Sie jedoch nicht die gesamte Klasse scannen, um herauszufinden, welche Abhängigkeiten fehlen. 

Und einige bessere Container validieren beim Start alle Abhängigkeiten (durch Scannen aller Konstruktoren). Bei diesen Containern erhalten Sie den Laufzeitfehler direkt und nicht zu einem späteren Zeitpunkt.

Der Autor erwähnte auch die Schwierigkeiten bei der Prüfung von Einheiten. Aber haben wir keine Probleme mit dem DI-Ansatz? 

Nein, da Sie nicht von einem statischen Service Locator abhängig sind. Haben Sie versucht, parallele Tests mit statischen Abhängigkeiten arbeiten zu lassen? Das ist kein Spaß.

110
jgauffin

Ich möchte auch darauf hinweisen, dass, wenn Sie den alten Code umgestalten, dass das Service Locator-Muster nicht nur kein Anti-Muster ist, sondern auch eine praktische Notwendigkeit ist. Niemand wird jemals einen Zauberstab über Millionen von Codezeilen winken und plötzlich ist alles Code bereit für DI. Wenn Sie also anfangen möchten, DI in eine vorhandene Codebasis einzuführen, ändern Sie häufig Dinge, um zu DI-Diensten zu werden, und der Code, der auf diese Dienste verweist, ist häufig KEIN DI-Dienst. Daher müssen DIESE Dienste den Service Locator verwenden, um Instanzen dieser Dienste zu erhalten, die HAVE zur Verwendung von DI konvertiert haben.

Beim Umgestalten großer Legacy-Anwendungen für die Verwendung von DI-Konzepten würde ich sagen, dass Service Locator NICHT nur kein Anti-Pattern ist, sondern dass dies die einzige Möglichkeit ist, DI-Konzepte schrittweise auf die Codebasis anzuwenden.

34
jwells131313

Aus Testsicht ist der Service Locator fehlerhaft. Siehe Misko Heverys Google Tech Talk. Nizza Erklärung mit Codebeispielen http://youtu.be/RlfLCWKxHJ0 ab Minute 8:45. Ich mochte seine Analogie: Wenn Sie $ 25 brauchen, fragen Sie direkt nach Geld, anstatt die Geldbörse zu geben, von der das Geld genommen wird. Er vergleicht auch Service Locator mit einem Heuhaufen, der über die Nadel verfügt, die Sie benötigen, und weiß, wie er abgerufen werden muss. Klassen, die Service Locator verwenden, sind aufgrund dessen schwer wiederzuverwenden.

7
user3511397

Wartungsproblem (was mich verwirrt)

Es gibt zwei verschiedene Gründe, warum die Verwendung von Service Locator in dieser Hinsicht schlecht ist.

  1. In Ihrem Beispiel kodieren Sie einen statischen Verweis auf den Service Locator in Ihrer Klasse. Dies koppelt eng Ihre Klasse direkt an den Service Locator, was wiederum bedeutet, dass es funktioniert nicht ohne den Service Locator. Außerdem sind Ihre Unit-Tests (und alle anderen Benutzer, die die Klasse verwenden) auch implizit vom Service Locator abhängig. Eine Sache, die hier scheinbar unbemerkt schien, ist bei der Verwendung der Konstruktorinjektion benötigen Sie beim Komponententest keinen DI-Container, wodurch Ihre Komponententests (und die Fähigkeit der Entwickler, sie zu verstehen) erheblich vereinfacht werden. Dies ist der realisierte Unit-Test-Vorteil, den Sie durch die Verwendung der Konstruktor-Injektion erhalten.
  2. Warum Konstruktor Intellisense so wichtig ist, scheinen die Menschen hier den Punkt völlig verfehlt zu haben. Eine Klasse wird einmal geschrieben, aber kann in mehreren Anwendungen verwendet werden (d. H. In mehreren DI-Konfigurationen). Im Laufe der Zeit zahlt es sich aus, wenn Sie sich die Konstruktordefinition ansehen können, um die Abhängigkeiten einer Klasse zu verstehen, anstatt sich die (hoffentlich aktuelle) Dokumentation anzusehen oder, falls dies nicht der Fall ist, auf den ursprünglichen Quellcode zurückzugreifen (was möglicherweise nicht der Fall ist praktisch sein), um die Abhängigkeiten einer Klasse zu bestimmen. Die Klasse mit dem Service Locator ist in der Regel einfacher zu schreiben, aber Sie müssen die Kosten für diesen Komfort bei der laufenden Wartung des Projekts mehr als bezahlen.

Einfach und einfach: Eine Klasse mit einem Service Locator ist schwieriger wiederzuverwenden als eine Klasse, die ihre Abhängigkeiten durch ihren Konstruktor akzeptiert.

Betrachten Sie den Fall, in dem Sie einen Dienst von LibraryA verwenden müssen, für den sich der Autor entschieden hat, ServiceLocatorA und einen Dienst von LibraryB zu verwenden, dessen Autor sich für ServiceLocatorB entschieden hat. Wir haben keine andere Wahl, als zwei verschiedene Service Locators in unserem Projekt einzusetzen. Wie viele Abhängigkeiten konfiguriert werden müssen, ist ein Ratespiel, wenn wir keine gute Dokumentation, keinen Quellcode oder keinen Autor für die Kurzwahl haben. Wenn diese Optionen fehlschlagen, müssen wir möglicherweise einen Dekompilierer nur verwenden, um die Abhängigkeiten herauszufinden. Möglicherweise müssen zwei völlig unterschiedliche Service Locator-APIs konfiguriert werden. Je nach Design ist es möglicherweise nicht möglich, den vorhandenen DI-Container einfach einzuwickeln. Es ist möglicherweise nicht möglich, eine Instanz einer Abhängigkeit zwischen den beiden Bibliotheken gemeinsam zu nutzen. Die Komplexität des Projekts könnte sich sogar noch verschärfen, wenn sich die Service Locators nicht tatsächlich in denselben Bibliotheken befinden wie die von uns benötigten Services - wir ziehen implizit zusätzliche Bibliotheksreferenzen in unser Projekt.

Betrachten Sie nun die gleichen zwei Dienste, die mit der Konstruktorinjektion gemacht wurden. Fügen Sie einen Verweis auf LibraryA hinzu. Fügen Sie einen Verweis auf LibraryB hinzu. Geben Sie die Abhängigkeiten in Ihrer DI-Konfiguration an (indem Sie analysieren, was über Intellisense erforderlich ist). Erledigt.

Mark Seemann verfügt über eine StackOverflow-Antwort, die diesen Vorteil anschaulich veranschaulicht . Dies gilt nicht nur für die Verwendung eines Service Locators aus einer anderen Bibliothek, sondern auch für die Verwendung von Fremdstandards in Services.

3
NightOwl888

Mein Wissen ist nicht gut genug, um das beurteilen zu können, aber im Allgemeinen denke ich, wenn etwas in einer bestimmten Situation von Nutzen ist, bedeutet das nicht unbedingt, dass es kein Antimuster sein kann. Insbesondere wenn Sie sich mit Bibliotheken von Drittanbietern beschäftigen, haben Sie nicht die volle Kontrolle über alle Aspekte und verwenden möglicherweise nicht die beste Lösung. 

Hier ist ein Absatz aus Adaptiver Code über C # :

"Leider ist der Service Locator manchmal ein unvermeidlicher Anti-Pattern. Bei einigen Anwendungstypen - insbesondere Windows Workflow Foundation - eignet sich die Infrastruktur nicht für Konstruktorinjektionen. In diesen Fällen besteht die einzige Alternative in der Verwendung eines Service Locator Es ist besser als gar keine Abhängigkeiten zu injizieren. Für alle meine Vitriol gegen das (Anti-) Muster ist es unendlich viel besser als das manuelle Erstellen von Abhängigkeiten. Schließlich ermöglicht es immer noch alle wichtigen Erweiterungspunkte, die von Schnittstellen bereitgestellt werden, und ähnliche Vorteile. "

- Hall, Gary McLean. Adaptiver Code über C #: Agile Codierung mit Entwurfsmustern und SOLID - Prinzipien (Developer Reference) (S. 309). Pearson Ausbildung.

1

Ja, Service Locator ist ein Anti-Pattern, das gegen die Kapselung und solid verstößt.

Der Autor begründet, dass "der Compiler Ihnen nicht helfen wird" - und das stimmt. Wenn Sie eine Klasse entwerfen, sollten Sie ihre Schnittstelle sorgfältig auswählen - neben anderen Zielen, um sie so unabhängig wie ... zu machen es macht Sinn.

Indem der Client den Verweis auf einen Dienst (auf eine Abhängigkeit) über eine explizite Schnittstelle akzeptiert, müssen Sie

  • implizit prüfen, so "hilft" der Compiler.
  • Sie machen es dem Kunden auch überflüssig, etwas über den "Locator" oder ähnliche Mechanismen zu wissen, so dass der Client tatsächlich unabhängiger ist.

Du hast recht, dass DI seine Probleme/Nachteile hat, aber die genannten Vorteile überwiegen bei weitem ... IMO ..__ Sie haben Recht, dass mit DI eine Abhängigkeit in der Schnittstelle (Konstruktor) eingeführt wird - aber das hier ist hoffentlich genau die Abhängigkeit, die Sie brauchen und sichtbar und überprüfbar machen wollen.

0
Zrin