Ich habe eine Schnittstelle aufgerufen IContext
. Zu diesem Zweck spielt es keine Rolle, was es tut, mit Ausnahme der folgenden:
T GetService<T>();
Diese Methode überprüft den aktuellen DI-Container der Anwendung und versucht, die Abhängigkeit aufzulösen. Ziemlich normal, denke ich.
In meiner ASP.NET MVC-Anwendung sieht mein Konstruktor folgendermaßen aus.
protected MyControllerBase(IContext ctx)
{
TheContext = ctx;
SomeService = ctx.GetService<ISomeService>();
AnotherService = ctx.GetService<IAnotherService>();
}
So anstatt für jeden Dienst mehr Parameter im Konstruktor hinzugefügt (weil das ist wirklich ärgerlich wird und zeitraubend für die Entwickler zur Verlängerung der Anwendung) ich diese Methode bin mit Dienstleistungen zu erhalten.
Jetzt fühlt es sich falsch an . Die Art und Weise, wie ich es derzeit in meinem Kopf rechtfertige, ist jedoch folgende: Ich kann mich darüber lustig machen .
Ich kann. Es wäre nicht schwierig, sich zu verspotten IContext
, um den Controller zu testen. Ich müsste sowieso:
public class MyMockContext : IContext
{
public T GetService<T>()
{
if (typeof(T) == typeof(ISomeService))
{
// return another mock, or concrete etc etc
}
// etc etc
}
}
Aber wie gesagt, es fühlt sich falsch an. Irgendwelche Gedanken / Missbrauch willkommen.
quelle
public SomeClass(Context c)
. Dieser Code ist ziemlich klar, nicht wahr? Es heißt, esthat SomeClass
kommt auf a anContext
. Ähm, aber warte, das tut es nicht! Es hängt nur von der Abhängigkeit ab, dieX
es vom Kontext erhält. Das heißt, macht jedes Mal , wenn eine ÄnderungContext
es könnte brechenSomeObject
, auch wenn Sie nur geändertContext
sY
. Aber ja, du weißt, dass du dich nurY
nicht verändert hastX
, alsoSomeClass
ist es in Ordnung. Beim Schreiben eines guten Codes geht es jedoch nicht darum, was Sie wissen, sondern darum , was der neue Mitarbeiter weiß, wenn er sich Ihren Code zum ersten Mal ansieht.Antworten:
Ein statt vieler Parameter im Konstruktor zu haben, ist nicht der problematische Teil dieses Entwurfs . Solange Ihre
IContext
Klasse nichts anderes als eine Servicefassade ist , insbesondere zur Bereitstellung der in verwendeten AbhängigkeitenMyControllerBase
, und kein allgemeiner Service-Locator, der im gesamten Code verwendet wird, ist dieser Teil Ihres Codes meiner Meinung nach in Ordnung.Ihr erstes Beispiel könnte in geändert werden
das wäre keine wesentliche konstruktionsänderung von
MyControllerBase
. Ob dieser Entwurf gut oder schlecht ist, hängt nur davon ab, ob Sie möchtenTheContext
,SomeService
undAnotherService
sind immer alle mit Mock - Objekte initialisiert, oder alle von ihnen mit realen ObjektenDie Verwendung von nur einem Parameter anstelle von 3 im Konstruktor kann daher durchaus sinnvoll sein.
Das Problem ist
IContext
, dieGetService
Methode in der Öffentlichkeit bekannt zu machen. IMHO sollten Sie dies vermeiden, stattdessen die "Factory-Methoden" explizit beibehalten. Ist es also in Ordnung, die MethodenGetSomeService
undGetAnotherService
aus meinem Beispiel mithilfe eines Service-Locators zu implementieren ? IMHO das hängt davon ab. Solange dieIContext
Klasse nur eine einfache abstrakte Factory für den speziellen Zweck der Bereitstellung einer expliziten Liste von Dienstobjekten ist, ist dies meiner Meinung nach akzeptabel. Abstrakte Fabriken sind in der Regel nur "Kleber" -Code, der nicht selbst einer Prüfung unterzogen werden muss. Trotzdem sollten Sie sich fragen, ob es im Kontext von Methoden wieGetSomeService
, ob Sie wirklich einen Service Locator benötigen oder ob ein expliziter Konstruktoraufruf nicht einfacher wäre.Wenn Sie sich also an ein Design halten, bei dem die
IContext
Implementierung nur eine Hülle um eine öffentliche, generischeGetService
Methode ist, mit der beliebige Abhängigkeiten durch beliebige Klassen aufgelöst werden können, gilt alles, was @BenjaminHodgson in seiner Antwort schrieb.quelle
GetService
Methode. Das Umgestalten auf explizit benannte und typisierte Methoden ist besser. Besser wäre es noch, die Abhängigkeiten derIContext
Implementierung explizit im Konstruktor zu formulieren.IValidatableObject
?Dieses Design ist als Service Locator * bekannt und gefällt mir nicht. Es gibt viele Argumente dagegen:
Service Locator koppelt Sie an Ihren Container . Mit der regulären Abhängigkeitsinjektion (bei der der Konstruktor die Abhängigkeiten explizit angibt) können Sie Ihren Container direkt durch einen anderen ersetzen oder zu
new
-expressions zurückkehren. Mit deinem istIContext
das nicht wirklich möglich.Service Locator verbirgt Abhängigkeiten . Als Client ist es sehr schwierig zu sagen, was Sie zum Erstellen einer Instanz einer Klasse benötigen. Sie benötigen eine Art von
IContext
, aber Sie müssen auch den Kontext einrichten, um die richtigen Objekte zurückzugeben, damit dieMyControllerBase
Arbeit ausgeführt werden kann. Dies ist aus der Unterschrift des Konstrukteurs gar nicht ersichtlich. Mit dem regulären DI sagt Ihnen der Compiler genau, was Sie brauchen. Wenn Ihre Klasse viele Abhängigkeiten hat, sollten Sie diesen Schmerz spüren, weil er Sie zur Umgestaltung anspornt. Service Locator verbirgt die Probleme mit schlechten Designs.Service Locator verursacht Laufzeitfehler . Wenn Sie
GetService
mit einem schlechten Typparameter aufrufen, erhalten Sie eine Ausnahme. Mit anderen Worten, IhreGetService
Funktion ist keine Gesamtfunktion. (Total Functions sind eine Idee aus der FP-Welt, aber es bedeutet im Grunde, dass Funktionen immer einen Wert zurückgeben sollten.) Lassen Sie sich besser vom Compiler helfen und sagen, wenn Sie die falschen Abhängigkeiten haben.Service Locator verstößt gegen das Liskov-Substitutionsprinzip . Da sein Verhalten vom Typ des Arguments abhängt, kann der Service Locator so betrachtet werden, als ob er eine unendliche Anzahl von Methoden auf der Oberfläche hat! Dieses Argument wird im Detail dargelegt hier .
Service Locator ist schwer zu testen . Sie haben ein Beispiel für eine Fälschung
IContext
für Tests angegeben, was in Ordnung ist, aber es ist sicherlich besser, diesen Code nicht erst schreiben zu müssen. Fügen Sie einfach Ihre gefälschten Abhängigkeiten direkt ein, ohne über Ihren Service Locator zu gehen.Kurz gesagt, tu es einfach nicht . Es scheint eine verführerische Lösung für das Problem der Klassen mit vielen Abhängigkeiten zu sein, aber auf lange Sicht werden Sie Ihr Leben nur unglücklich machen.
* Ich definiere Service Locator als Objekt mit einer generischen
Resolve<T>
Methode, die beliebige Abhängigkeiten auflösen kann und in der gesamten Codebasis verwendet wird (nicht nur im Kompositionsstamm). Dies ist nicht dasselbe wie Service Facade (ein Objekt, das einige bekannte Abhängigkeiten bündelt) oder Abstract Factory (ein Objekt, das Instanzen eines einzelnen Typs erstellt - der Typ einer Abstract Factory ist möglicherweise generisch, die Methode jedoch nicht). .quelle
MyControllerBase
jedoch weder an einen bestimmten DI-Container gekoppelt, noch ist dies "wirklich" ein Beispiel für das Anti-Pattern "Service Locator".GetService<T>
Methode. Das Auflösen beliebiger Abhängigkeiten ist der eigentliche Geruch, der im Beispiel des OP vorhanden und korrekt ist.GetService<T>()
Methode können beliebige Klassen angefordert werden: "Diese Methode überprüft den aktuellen DI-Container der Anwendung und versucht, die Abhängigkeit aufzulösen. Ich denke, ein angemessener Standard." . Ich habe auf Ihren Kommentar oben in dieser Antwort geantwortet. Dies ist 100% ein Service LocatorDie besten Argumente gegen das Anti-Pattern von Service Locator werden von Mark Seemann klar formuliert, sodass ich nicht zu sehr darauf eingehen werde, warum dies eine schlechte Idee ist auch Marks Buch empfehlen ).
Um die Frage zu beantworten, lassen Sie uns Ihr aktuelles Problem erneut darlegen :
Es gibt eine Frage, die dieses Problem in StackOverflow behebt . Wie in einem der Kommentare dort erwähnt:
Sie suchen an der falschen Stelle nach einer Lösung für Ihr Problem. Es ist wichtig zu wissen, wann eine Klasse zu viel tut. Ich vermute in Ihrem Fall stark, dass kein "Base Controller" benötigt wird. Tatsächlich ist in OOP fast immer überhaupt keine Vererbung erforderlich . Variationen im Verhalten und in der gemeinsamen Funktionalität können vollständig durch geeignete Verwendung von Schnittstellen erzielt werden, was normalerweise zu besser faktorisiertem und gekapseltem Code führt - und keine Notwendigkeit, Abhängigkeiten an übergeordnete Konstruktoren weiterzugeben.
In allen Projekten, an denen ich gearbeitet habe, gab es einen Basis-Controller, der ausschließlich dazu diente, praktische Eigenschaften und Methoden wie
IsUserLoggedIn()
und mit anderen zu teilenGetCurrentUserId()
. STOP . Dies ist ein schrecklicher Missbrauch der Erbschaft. Erstellen Sie stattdessen eine Komponente, die diese Methoden verfügbar macht, und nehmen Sie eine Abhängigkeit davon, wo Sie sie benötigen. Auf diese Weise bleiben Ihre Komponenten testbar und ihre Abhängigkeiten werden offensichtlich.Abgesehen von allem anderen würde ich bei Verwendung des MVC-Musters immer dünne Controller empfehlen . Sie können hier mehr darüber lesen , aber der Kern des Musters ist einfach: Controller in MVC sollten nur eines tun: Argumente verarbeiten, die vom MVC-Framework übergeben werden, und andere Belange an eine andere Komponente delegieren. Dies ist wiederum das Prinzip der Einzelverantwortung bei der Arbeit.
Es wäre wirklich hilfreich, Ihren Anwendungsfall zu kennen, um ein genaueres Urteil abzugeben, aber ich kann mir ehrlich gesagt kein Szenario vorstellen, in dem eine Basisklasse gut faktorisierten Abhängigkeiten vorzuziehen ist.
quelle
protected
Delegierungen an die neuen Komponenten. Dann können Sie BaseController verlieren und dieseprotected
Methoden durch direkte Aufrufe von Abhängigkeiten ersetzen - dies vereinfacht Ihr Modell und macht all Ihre Abhängigkeiten explizit (was in einem Projekt mit Hunderten von Controllern eine sehr gute Sache ist!)Ich füge eine Antwort hinzu, die auf den Beiträgen aller anderen basiert. Vielen Dank an alle. Hier ist zunächst meine Antwort: "Nein, daran ist nichts auszusetzen."
Doc Browns "Service Facade" -Antwort Ich habe diese Antwort akzeptiert, weil ich nach einigen Beispielen oder einer Erweiterung meiner Arbeit gesucht habe (wenn die Antwort "Nein" war). Er gab dies an, indem er vorschlug, dass A) es einen Namen hat und B) es wahrscheinlich bessere Möglichkeiten gibt, dies zu tun.
Benjamin Hodgsons "Service Locator" -Antwort So sehr ich das Wissen schätze, das ich hier gewonnen habe, ist das, was ich habe, kein "Service Locator". Es ist eine "Servicefassade". Alles in dieser Antwort ist richtig, aber nicht für meine Umstände.
Antworten der USR
Ich werde das genauer angehen:
Ich verliere kein Werkzeug und keine "statische" Eingabe. Die Servicefassade gibt das zurück, was ich in meinem DI-Container konfiguriert habe, oder
default(T)
. Und was es zurückgibt, ist "getippt". Die Reflexion ist eingekapselt.Es ist sicherlich nicht "selten". Als ich eine bin mit Basis - Controller, ich jedes Mal müssen sie Konstruktor ändern, kann ich 10 ändern müssen, 100, 1000 anderen Controllern.
Ich benutze Abhängigkeitsinjektion. Das ist der Punkt.
Und schließlich, Jules 'Kommentar zu Benjamins Antwort, verliere ich keine Flexibilität. Es ist meine Servicefassade. Ich kann so viele Parameter hinzufügen,
GetService<T>
wie ich zwischen verschiedenen Implementierungen unterscheiden möchte, wie dies bei der Konfiguration eines DI-Containers der Fall wäre. So zum Beispiel könnte ich ändern ,GetService<T>()
umGetService<T>(string extraInfo = null)
dieses „potenzielles Problem“ zu umgehen.Trotzdem nochmals vielen Dank an alle. Es war wirklich nützlich. Prost.
quelle
GetService<T>()
Methode ist in der Lage (zu versuchen), beliebige Abhängigkeiten aufzulösen . Dies macht es zu einem Service Locator und nicht zu einer Service Facade, wie ich in der Fußnote meiner Antwort erklärt habe. Wenn Sie es durch eine kleine Menge vonGetServiceX()
/GetServiceY()
Methoden ersetzen würden, wie @DocBrown vorschlägt, wäre es eine Fassade.IContext
ausfüllen und das einspeisen, und entscheidend: Wenn Sie eine neue Abhängigkeit hinzufügen, wird der Code immer noch kompiliert - obwohl die Tests zur Laufzeit fehlschlagen