Kontext: Ich verwende C #
Ich habe eine Klasse entworfen, und um sie zu isolieren und das Testen von Einheiten zu vereinfachen, übergebe ich alle Abhängigkeiten. Intern wird keine Objektinstanziierung durchgeführt. Anstatt auf Schnittstellen zu verweisen, um die benötigten Daten zu erhalten, verweise ich auf allgemeine Funktionen, die die erforderlichen Daten / das erforderliche Verhalten zurückgeben. Wenn ich seine Abhängigkeiten einspeise, kann ich dies einfach mit Lambda-Ausdrücken tun.
Für mich scheint dies ein besserer Ansatz zu sein, da ich während des Unit-Tests keine langweiligen Verspottungen anstellen muss. Wenn die umgebende Implementierung grundlegende Änderungen aufweist, muss ich nur die Factory-Klasse ändern. Es sind keine Änderungen in der Klasse mit der Logik erforderlich.
Ich habe jedoch noch nie zuvor gesehen, dass IoC auf diese Weise durchgeführt wird, was mich zu der Annahme veranlasst, dass es einige potenzielle Fallstricke gibt, die mir möglicherweise fehlen. Das Einzige, an das ich denken kann, ist die geringfügige Inkompatibilität mit früheren Versionen von C #, die Func nicht definieren, und dies ist in meinem Fall kein Problem.
Gibt es irgendwelche Probleme bei der Verwendung von allgemeinen Delegaten / Funktionen höherer Ordnung wie Func für IoC im Gegensatz zu spezifischeren Schnittstellen?
quelle
Func
da Sie die Parameter benennen können, in denen Sie ihre Absicht angeben können.Antworten:
Wenn eine Schnittstelle nur eine Funktion enthält, nicht mehr, und es keinen zwingenden Grund gibt, zwei Namen (den Schnittstellennamen und den Funktionsnamen innerhalb der Schnittstelle)
Func
einzufügen , kann die Verwendung von a unnötigen Kesselcode vermeiden und ist in den meisten Fällen vorzuziehen. Genau wie wenn Sie damit beginnen, ein DTO zu entwerfen und zu erkennen, dass es nur ein Mitgliedsattribut benötigt.Ich denke, viele Leute sind es gewohnter zu verwenden
interfaces
, weil es zu der Zeit, als Abhängigkeitsinjektion und IoC populär wurden, kein wirkliches Äquivalent zu derFunc
Klasse in Java oder C ++ gab (ich bin nicht einmal sicher, obFunc
es zu dieser Zeit in C # verfügbar war). ). Daher bevorzugen viele Tutorials, Beispiele oder Lehrbücher immer noch dieinterface
Form, auch wenn die VerwendungFunc
eleganter wäre.Sie könnten in meine frühere Antwort zum Prinzip der Schnittstellentrennung und zum Flow-Design-Ansatz von Ralf Westphal schauen . Dieses Paradigma implementiert DI ausschließlich mit
Func
Parametern , aus genau demselben Grund, den Sie bereits selbst (und einige weitere) erwähnt haben. Wie Sie sehen, ist Ihre Idee nicht wirklich neu, ganz im Gegenteil.Ja, ich habe diesen Ansatz für den Produktionscode für Programme verwendet, die Daten in Form einer Pipeline mit mehreren Zwischenschritten verarbeiten mussten, einschließlich Unit-Tests für jeden der Schritte. So kann ich Ihnen die Erfahrung aus erster Hand geben, dass es sehr gut funktionieren kann.
quelle
Einer der Hauptvorteile von IoC besteht darin, dass ich alle meine Abhängigkeiten über die Benennung ihrer Schnittstelle benennen kann und der Container weiß, welche dem Konstruktor zur Verfügung gestellt werden muss, indem er die Typnamen angleicht. Dies ist praktisch und erlaubt viel aussagekräftigere Namen von Abhängigkeiten als
Func<string, string>
.Ich stelle auch oft fest, dass selbst bei einer einfachen Abhängigkeit manchmal mehr als eine Funktion erforderlich ist - eine Schnittstelle ermöglicht es Ihnen, diese Funktionen auf eine Art und Weise zu gruppieren, die sich selbst dokumentiert, statt mehrere Parameter zu haben, die alle wie folgt lauten
Func<string, string>
:Func<string, int>
.Es gibt definitiv Zeiten, in denen es nützlich ist, einfach einen Delegaten zu haben, der als Abhängigkeit übergeben wird. Es ist ein Urteilsspruch darüber, wann Sie einen Delegierten einsetzen, anstatt eine Schnittstelle mit sehr wenigen Mitgliedern zu haben. Sofern nicht wirklich klar ist, wozu das Argument dient, werde ich mich normalerweise auf die Seite der Erzeugung von selbstdokumentierendem Code begeben. dh eine Schnittstelle schreiben.
quelle
Nicht wirklich. Ein Func ist eine eigene Art von Schnittstelle (in der englischen Bedeutung, nicht in der C # -Bedeutung). Msgstr "Dieser Parameter liefert X, wenn gefragt wird." Der Func hat sogar den Vorteil, die Informationen nur bei Bedarf faul bereitzustellen. Ich mache das ein bisschen und empfehle es in Maßen.
Was die Nachteile angeht:
T
und andere Dinge sindFunc<T>
.quelle
Nehmen wir ein einfaches Beispiel - vielleicht injizieren Sie ein Protokollierungsmittel.
Injizieren einer Klasse
Ich denke, das ist ziemlich klar, was los ist. Wenn Sie einen IoC-Container verwenden, müssen Sie nicht einmal explizit etwas einfügen. Fügen Sie dies einfach zu Ihrem Kompositionsstamm hinzu:
Beim Debuggen
Worker
muss ein Entwickler lediglich den Kompositionsstamm konsultieren, um festzustellen, welche konkrete Klasse verwendet wird.Wenn ein Entwickler eine kompliziertere Logik benötigt, kann er mit der gesamten Oberfläche arbeiten:
Injizieren einer Methode
Beachten Sie zunächst, dass der Konstruktorparameter jetzt einen längeren Namen hat
methodThatLogs
. Dies ist notwendig, weil Sie nicht sagen können, was eineAction<string>
tun soll. Bei der Schnittstelle war es völlig klar, aber hier müssen wir uns auf die Benennung der Parameter verlassen. Dies scheint inhärent weniger zuverlässig und schwieriger während eines Builds durchzusetzen zu sein.Wie injizieren wir diese Methode? Nun, der IoC-Container erledigt das nicht für Sie. Sie müssen es also beim Instanziieren explizit injizieren
Worker
. Dies wirft einige Probleme auf:Worker
Worker
werden feststellen, dass es schwieriger ist, herauszufinden, welche konkrete Instanz aufgerufen wird. Sie können nicht nur die Kompositionswurzel konsultieren; Sie müssen den Code nachverfolgen.Wie wäre es, wenn wir eine kompliziertere Logik brauchen? Ihre Technik macht nur eine Methode verfügbar. Nun, ich nehme an, Sie könnten das komplizierte Zeug in das Lambda backen:
aber wenn Sie Ihre Komponententests schreiben, wie testen Sie diesen Lambda-Ausdruck? Es ist anonym, sodass Ihr Unit-Test-Framework es nicht direkt instanziieren kann. Vielleicht können Sie eine clevere Möglichkeit finden, dies zu tun, aber es wird wahrscheinlich eine größere PITA sein als die Verwendung einer Schnittstelle.
Zusammenfassung der Unterschiede:
Wenn Sie mit all dem Obenstehenden einverstanden sind, ist es in Ordnung, nur die Methode zu injizieren. Ansonsten würde ich vorschlagen, dass Sie bei der Tradition bleiben und eine Schnittstelle einfügen.
quelle
Worker
Abhängigkeit unterstützen, sodass jedes Lambda unabhängig injiziert werden kann, bis alle Abhängigkeiten erfüllt sind. Dies ähnelt der Teilanwendung in FP. Darüber hinaus hilft die Verwendung von Lambdas dabei, den impliziten Zustand und die versteckte Kopplung zwischen Abhängigkeiten zu beseitigen.Betrachten Sie den folgenden Code, den ich vor langer Zeit geschrieben habe:
Es nimmt eine relative Position zu einer Vorlagendatei ein, lädt sie in den Speicher, rendert den Nachrichtentext und stellt das E-Mail-Objekt zusammen.
Sie könnten sehen
IPhysicalPathMapper
und denken: "Es gibt nur eine Funktion. Das könnte eine seinFunc
." Das Problem hierbei ist jedoch, dassIPhysicalPathMapper
es nicht einmal existieren sollte. Eine viel bessere Lösung besteht darin, nur den Pfad zu parametrisieren :Dies wirft eine ganze Reihe weiterer Fragen zur Verbesserung dieses Codes auf. ZB sollte es nur eine akzeptieren
EmailTemplate
, und dann sollte es vielleicht eine vorgerenderte Vorlage akzeptieren, und dann sollte es vielleicht nur ausgekleidet sein.Aus diesem Grund mag ich es nicht, die Kontrolle als ein allgegenwärtiges Muster umzukehren. Es wird im Allgemeinen als diese gottähnliche Lösung zum Verfassen Ihres gesamten Codes angesehen. Aber in Wirklichkeit wird Ihr Code erstellt, wenn Sie ihn überall verwenden (im Gegensatz zu sparsam) viel schlimmer indem Sie dazu ermutigen, viele unnötige Schnittstellen einzuführen, die vollständig rückwärts verwendet werden. (Rückwärts in dem Sinne, dass der Aufrufer wirklich dafür verantwortlich sein sollte, diese Abhängigkeiten zu bewerten und das Ergebnis weiterzuleiten, anstatt dass die Klasse selbst den Aufruf aufruft.)
Schnittstellen sollten sparsam verwendet werden, und Inversion von Kontrolle und Abhängigkeitsinjektion sollten auch sparsam verwendet werden. Wenn Sie große Mengen davon haben, wird es viel schwieriger, Ihren Code zu entschlüsseln.
quelle