Bei der Frage nach der Abhängigkeitsinjektion in Scala deuten viele Antworten darauf hin, dass Sie die Reader-Monade verwenden, entweder die von Scalaz oder nur Ihre eigene. Es gibt eine Reihe sehr klarer Artikel, die die Grundlagen des Ansatzes beschreiben (z. B. Runars Vortrag , Jasons Blog ), aber ich habe es nicht geschafft, ein vollständigeres Beispiel zu finden, und ich sehe die Vorteile dieses Ansatzes gegenüber z traditionelles "manuelles" DI (siehe die Anleitung, die ich geschrieben habe ). Höchstwahrscheinlich fehlt mir ein wichtiger Punkt, daher die Frage.
Stellen wir uns als Beispiel vor, wir haben diese Klassen:
trait Datastore { def runQuery(query: String): List[String] }
trait EmailServer { def sendEmail(to: String, content: String): Unit }
class FindUsers(datastore: Datastore) {
def inactive(): Unit = ()
}
class UserReminder(findUser: FindUsers, emailServer: EmailServer) {
def emailInactive(): Unit = ()
}
class CustomerRelations(userReminder: UserReminder) {
def retainUsers(): Unit = {}
}
Hier modelliere ich Dinge mit Klassen und Konstruktorparametern, was sehr gut mit "traditionellen" DI-Ansätzen funktioniert, aber dieses Design hat ein paar gute Seiten:
- Jede Funktionalität hat klar aufgezählte Abhängigkeiten. Wir gehen davon aus, dass die Abhängigkeiten wirklich benötigt werden, damit die Funktionalität ordnungsgemäß funktioniert
- Die Abhängigkeiten sind über Funktionen hinweg verborgen, z. B.
UserReminder
hat der keine Ahnung, dassFindUsers
ein Datenspeicher benötigt wird. Die Funktionen können sogar in separaten Kompiliereinheiten vorliegen - Wir verwenden nur reine Scala. Die Implementierungen können unveränderliche Klassen, Funktionen höherer Ordnung nutzen, die "Geschäftslogik" -Methoden können Werte zurückgeben, die in die
IO
Monade eingeschlossen sind, wenn wir die Effekte erfassen möchten usw.
Wie könnte dies mit der Reader-Monade modelliert werden? Es wäre gut, die oben genannten Merkmale beizubehalten, damit klar ist, welche Art von Abhängigkeiten jede Funktionalität benötigt, und Abhängigkeiten einer Funktionalität von einer anderen zu verbergen. Beachten Sie, dass die Verwendung von class
es eher ein Implementierungsdetail ist. Vielleicht würde die "richtige" Lösung mit der Reader-Monade etwas anderes verwenden.
Ich habe eine etwas verwandte Frage gefunden, die entweder Folgendes nahe legt:
- Verwenden eines einzelnen Umgebungsobjekts mit allen Abhängigkeiten
- mit lokalen Umgebungen
- "Parfait" -Muster
- typindizierte Karten
Abgesehen davon, dass es für all diese Lösungen etwas zu komplex ist (aber das ist subjektiv), muss die retainUsers
Methode (die aufruft emailInactive
, die aufruft inactive
, um die inaktiven Benutzer zu finden) über die Datastore
Abhängigkeit Bescheid wissen die verschachtelten Funktionen richtig aufrufen können - oder irre ich mich?
In welchen Aspekten wäre die Verwendung der Reader-Monade für eine solche "Geschäftsanwendung" besser als nur die Verwendung von Konstruktorparametern?
Antworten:
So modellieren Sie dieses Beispiel
Ich bin mir nicht sicher, ob dies mit dem Reader modelliert werden soll, aber es kann sein durch:
Kurz vor dem Start muss ich Ihnen über kleine Anpassungen des Beispielcodes berichten, die ich für diese Antwort als vorteilhaft empfunden habe. Bei der ersten Änderung geht es um die
FindUsers.inactive
Methode. Ich lasse es zurückgeben,List[String]
damit die Liste der Adressen in derUserReminder.emailInactive
Methode verwendet werden kann. Ich habe auch einfache Implementierungen zu Methoden hinzugefügt. Schließlich wird im Beispiel eine folgende handgerollte Version der Reader-Monade verwendet:Modellierungsschritt 1. Codieren von Klassen als Funktionen
Vielleicht ist das optional, ich bin mir nicht sicher, aber später sieht das Verständnis dadurch besser aus. Beachten Sie, dass die resultierende Funktion Curry ist. Es werden auch frühere Konstruktorargumente als erster Parameter (Parameterliste) verwendet. Dieser Weg
wird
Beachten Sie, dass jeder
Dep
,Arg
,Res
Typen völlig willkürlich sein kann: ein Tupel, eine Funktion oder einen einfachen Typen.Hier ist der Beispielcode nach den ersten Anpassungen, der in Funktionen umgewandelt wurde:
Hierbei ist zu beachten, dass bestimmte Funktionen nicht von den gesamten Objekten abhängen, sondern nur von den direkt verwendeten Teilen. Wo in der OOP-Versionsinstanz hier nur
UserReminder.emailInactive()
aufgerufen wirduserFinder.inactive()
, wird nur aufgerufeninactive()
- eine Funktion, die im ersten Parameter an sie übergeben wird.Bitte beachten Sie, dass der Code die drei wünschenswerten Eigenschaften aus der Frage aufweist:
retainUsers
Methode sollte nicht über die Datenspeicherabhängigkeit wissen müssenModellierungsschritt 2. Verwenden des Readers zum Erstellen und Ausführen von Funktionen
Mit Reader Monad können Sie nur Funktionen erstellen, die alle vom selben Typ abhängen. Dies ist oft nicht der Fall. In unserem Beispiel ist
FindUsers.inactive
abhängig vonDatastore
undUserReminder.emailInactive
aufEmailServer
. Um dieses Problem zu lösen, könnte man einen neuen Typ (oft als Config bezeichnet) einführen, der alle Abhängigkeiten enthält, und dann die Funktionen so ändern, dass sie alle davon abhängen und nur die relevanten Daten daraus entnehmen. Dies ist aus Sicht des Abhängigkeitsmanagements offensichtlich falsch, da Sie auf diese Weise diese Funktionen auch von Typen abhängig machen, über die sie überhaupt nichts wissen sollten.Glücklicherweise stellt sich heraus, dass es eine Möglichkeit gibt, die Funktion zum Arbeiten zu bringen,
Config
auch wenn nur ein Teil davon als Parameter akzeptiert wird. Es ist eine Methode namenslocal
, definiert in Reader. Es muss eine Möglichkeit bereitgestellt werden, das relevante Teil aus dem zu extrahierenConfig
.Dieses Wissen, das auf das vorliegende Beispiel angewendet wird, würde folgendermaßen aussehen:
Vorteile gegenüber der Verwendung von Konstruktorparametern
Ich hoffe, dass ich es durch die Vorbereitung dieser Antwort leichter gemacht habe, selbst zu beurteilen, in welchen Aspekten es einfache Konstrukteure schlagen würde. Wenn ich diese jedoch aufzählen würde, wäre hier meine Liste. Haftungsausschluss: Ich habe OOP-Hintergrund und kann Reader und Kleisli möglicherweise nicht vollständig schätzen, da ich sie nicht verwende.
local
Aufrufe darüber streuen . Dieser Punkt ist IMO eher Geschmackssache, denn wenn Sie Konstruktoren verwenden, hindert Sie niemand daran, alles zu komponieren, was Sie möchten, es sei denn, jemand tut etwas Dummes, wie die Arbeit im Konstruktor, was in OOP als schlechte Praxis angesehen wird.sequence
,traverse
Methoden kostenlos umgesetzt.Ich möchte auch sagen, was ich in Reader nicht mag.
pure
,local
und die Schaffung von eigenen Config - Klassen / Tupel für die Verwendung. Der Reader zwingt Sie, Code hinzuzufügen, bei dem es nicht um Problemdomänen geht, und führt daher zu Rauschen im Code. Andererseits verwendet eine Anwendung, die Konstruktoren verwendet, häufig ein Factory-Muster, das auch außerhalb des Problembereichs liegt, sodass diese Schwachstelle nicht so schwerwiegend ist.Was ist, wenn ich meine Klassen nicht in Objekte mit Funktionen konvertieren möchte?
Sie wollen. Sie können das technisch vermeiden, aber schauen Sie, was passieren würde, wenn ich die
FindUsers
Klasse nicht in ein Objekt konvertieren würde . Die jeweilige Zeile zum Verständnis würde folgendermaßen aussehen:was ist nicht so lesbar, oder? Der Punkt ist, dass Reader Funktionen ausführt. Wenn Sie sie also noch nicht haben, müssen Sie sie inline erstellen, was oft nicht so schön ist.
quelle
Datastore
undEmailServer
bleiben als Merkmale übrig, und andere wurdenobject
s? Gibt es einen grundlegenden Unterschied zwischen diesen Diensten / Abhängigkeiten / (wie auch immer Sie sie nennen), der dazu führt, dass sie unterschiedlich behandelt werden?EmailSender
in ein Objekt konvertieren , oder? Ich wäre dann nicht in der Lage, die Abhängigkeit auszudrücken, ohne den Typ zu haben ...EmailSender
, würden Sie sich darauf verlassen(String, String) => Unit
. Ob das überzeugt oder nicht, ist ein anderes Problem :) Natürlich ist es zumindest allgemeiner, da jeder bereits davon abhängtFunction2
.(String, String) => Unit
damit er eine Bedeutung vermittelt, allerdings nicht mit einemIch denke, der Hauptunterschied besteht darin, dass Sie in Ihrem Beispiel alle Abhängigkeiten einfügen, wenn Objekte instanziiert werden. Die Reader-Monade erstellt im Grunde genommen immer komplexere Funktionen, die angesichts der Abhängigkeiten aufgerufen werden können und die dann an die höchsten Ebenen zurückgegeben werden. In diesem Fall erfolgt die Injektion, wenn die Funktion endgültig aufgerufen wird.
Ein unmittelbarer Vorteil ist die Flexibilität, insbesondere wenn Sie Ihre Monade einmal erstellen können und sie dann mit verschiedenen injizierten Abhängigkeiten verwenden möchten. Ein Nachteil ist, wie Sie sagen, möglicherweise weniger Klarheit. In beiden Fällen muss die Zwischenschicht nur über ihre unmittelbaren Abhängigkeiten Bescheid wissen, sodass beide wie für DI angekündigt funktionieren.
quelle
Config
der ein Verweis auf enthältUserRepository
. So wahr, es ist nicht direkt in der Signatur sichtbar, aber ich würde sagen, das ist noch schlimmer, Sie haben auf den ersten Blick keine Ahnung, welche Abhängigkeiten Ihr Code wirklich verwendet. Bedeutet die Abhängigkeit von aConfig
mit all den Abhängigkeiten nicht, dass jede Methode von allen abhängt ?config
und was "nur eine Funktion" ist. Wahrscheinlich würden Sie auch viele Selbstabhängigkeiten haben. Wie auch immer, das ist eher eine Frage der Präferenz als eine Frage und