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 .