Reader-Monade für Dependency Injection: mehrere Abhängigkeiten, geschachtelte Aufrufe
Wenn Sie gefragt werden über Dependency Injection in Scala eine ganze Menge von Antworten zeigen auf die mit der Reader-Monade, sei es der von Scalaz oder einfach nur Ihre eigenen Rollen. Es gibt eine Reihe von sehr klaren Artikel beschreibt die Grundlagen des Ansatzes (z.B. Runar sprechen, Jason ' s blog), aber ich schaffte es nicht zu finden, ein vollständigeres Beispiel, und ich kann nicht erkennen, die Vorteile der Herangehensweise über beispielsweise einen mehr traditionellen, "manuellen" DI (siehe die Anleitung, die ich schrieb). Die meisten wahrscheinlich ich bin fehlen einige wichtige Punkt, daher die Frage.
Nur als Beispiel, stellen wir uns 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 bin ich Modellierung Dinge Verwendung von Klassen und Konstruktor-Parameter, die spielt sehr schön mit den "traditionellen" DI Ansätze, aber das design hat ein paar gute Seiten:
- jede Funktion hat klar aufgezählt Abhängigkeiten. Wir vermuten, dass die Abhängigkeiten sind wirklich notwendig für die Funktionalität, um richtig zu arbeiten
- die Abhängigkeiten versteckt über Funktionalitäten, z.B. die
UserReminder
hat keine Ahnung, dassFindUsers
muss ein datastore. Die Funktionalitäten können auch in separaten kompilieren Einheiten - wir verwenden nur reines Scala; die Implementierungen nutzen können unveränderliche Klassen höherer Ordnung Funktionen, die "business-Logik" Methoden können Werte zurückgeben, eingehüllt in die
IO
Monade, wenn wir wollen, erfassen die Auswirkungen etc.
Wie konnte dies sein, modelliert mit der Reader-Monade? Es wäre gut, behalten die Eigenschaften oben, so dass klar ist, welche Art von Abhängigkeiten, die jede Funktion benötigt, und verstecken sich Abhängigkeiten von Funktionalität von einem anderen. Beachten Sie, dass die Verwendung class
es ist eher ein detail, vielleicht die "richtige" Lösung mit der Reader-Monade würde etwas anderes verwenden.
Habe ich einen etwas Verwandte Frage die vermuten lässt, entweder:
- mit einer einzelnen environment-Objekt, mit all den Abhängigkeiten
- mit lokalen Umgebungen
- "parfait" Muster
- Typ-indizierte Karten
Jedoch, abgesehen davon, dass (aber das ist subjektiv) ein wenig zu Komplex für eine so einfache Sache, in allen diesen Lösungen z.B. die retainUsers
- Methode (die Anrufe emailInactive
fordert inactive
zu finden die inaktiven Benutzer) müssten wissen über die Datastore
Abhängigkeit, in der Lage sein, um richtig zu nennen, der verschachtelte Funktionen - oder bin ich da falsch?
In welche Aspekte würden mit der Reader-Monade für so ein "business-Anwendung" besser sein als nur mit Konstruktor-Parameter?
- Die Reader-Monade ist keine silberne Kugel. Ich denke, wenn Sie eine Menge von Ebenen von Abhängigkeiten, Ihr design ist ziemlich gut.
- Es wird jedoch oft beschrieben als eine alternative zu Dependency Injection; vielleicht sollte es dann beschrieben werden als Ergänzung? Ich bekomme manchmal das Gefühl, dass DI entlassen durch "true funktionale Programmierer", daher wurde ich gefragt, "was statt dessen" 🙂 so oder so, ich denke, mit mehreren Ebenen von Abhängigkeiten, oder besser mehrere externe Dienste, die Sie benötigen, zu sprechen ist, wie jedes medium-large "business-Anwendung" aussieht (nicht der Fall für Bibliotheken sicher)
- Ich habe immer gedacht, über die Reader-Monade als etwas lokales. Zum Beispiel, wenn Sie haben einige module, die spricht, nur um eine DB, die Sie implementieren können dieses Modul im Reader-Monade Stil. Jedoch, wenn Ihre Anwendung erfordert viele verschiedene Datenquellen, die kombiniert werden sollen, zusammen, ich glaube nicht, dass die Reader-Monade ist gut für die.
- Ah, das könnte eine gute Richtschnur, wie eine Kombination der beiden Konzepte. Und dann in der Tat scheint es, dass DI-und RM-ergänzen einander. Ich denke, es ist in der Tat Recht Häufig auf Funktionen, die den Betrieb auf einer Abhängigkeit nur, und mit RM hier würde helfen zu klären, die Abhängigkeit/Daten Grenzen.
Du musst angemeldet sein, um einen Kommentar abzugeben.
, Wie das Modell in diesem Beispiel
Ich bin mir nicht sicher, ob dies sollte modelliert werden mit dem Reader, aber es kann sein durch:
Gerade Recht-vor dem start muss ich Ihnen sagen, über kleine Probe code-Anpassungen, fühlte ich mich von Vorteil für diese Antwort.
Erste änderung ist über
FindUsers.inactive
Methode. Ich ließ es zurückList[String]
also die Liste der Adressen, die verwendet werden könnenin
UserReminder.emailInactive
Methode. Außerdem habe ich einfache Implementierungen der Methoden. Schließlich wird die Probe verwendenfolgende hand-gerollte version der Reader-Monade:
Modellierung Schritt 1. Codierung Klassen als Funktionen
Vielleicht ist das optional, ich bin mir nicht sicher, aber später macht es die für das Verständnis besser Aussehen.
Beachten Sie, dass die resultierende Funktion ist Curry. Es dauert auch ehemalige Konstruktor-argument(s) als ersten parameter (parameter-Liste).
So
wird
Bedenken Sie, dass jeder von
Dep
,Arg
,Res
Arten werden können, völlig willkürlich: ein Tupel, eine Funktion oder einen einfachen Typ.Hier ist der Beispiel-code nach der ersten Anpassungen, verwandelt sich in Funktionen:
Eine Sache zu beachten ist hier, dass bestimmte Funktionen nicht hängt davon ab, die ganze Objekte, sondern nur auf die direkt verwendet Teile.
Wo in der OOP-version
UserReminder.emailInactive()
Instanz nennen würdeuserFinder.inactive()
hier, man ruft einfachinactive()
- eine Funktion übergeben Sie im ersten parameter.
Bitte beachten Sie, dass der code weist die drei wünschenswerte Eigenschaften, die aus der Frage:
retainUsers
Methode sollten Sie nicht wissen müssen über den Datastore-AbhängigkeitModellierung Schritt 2. Mit dem Reader zu Komponieren-Funktionen, und führen Sie
Reader-Monade können Sie nur verfassen, Funktionen, die hängen alle von dem gleichen Typ. Dies ist oft nicht der Fall. In unserem Beispiel
FindUsers.inactive
hängtDatastore
undUserReminder.emailInactive
aufEmailServer
. Um dieses problem zu lösenman könnte sich vorstellen, eine neue Art (oft bezeichnet als Config), die enthält alle Abhängigkeiten, dann ändern
die Funktionen, so dass Sie alle hängen und nur daraus die relevanten Daten.
Das ist offensichtlich falsch aus der Abhängigkeit-management-Perspektive, da Sie so machen diese Funktionen auch abhängig
auf Typen, die Sie gar nicht kennen in den ersten Platz.
Glücklicherweise stellt sich heraus, dass es gibt einen Weg, um die Funktion der Arbeit mit
Config
auch wenn es akzeptiert nur ein Teil davon als parameter.Es ist eine Methode namens
local
, definiert im Reader. Es muss mit einem Verfahren zum extrahieren der relevante Teil aus derConfig
.Dieses wissen angewendet, um die Beispiel bei der hand würde das so Aussehen, dass:
Vorteile gegenüber der Verwendung von Konstruktor-Parameter
Ich hoffe, dass durch die Vorbereitung dieser Antwort habe ich es leichter gemacht, für sich selbst beurteilen in welche Aspekte würde es schlagen plain-Konstruktoren.
Aber wenn ich aufzählen, diese hier ist meine Liste. Disclaimer: ich habe OOP-hintergrund, und ich kann nicht schätzen, Leser und Kleisli
voll, wie ich Sie nicht verwenden.
Beispiel, vielleicht nur die Einführung eine weitere Config-Typ und bedüsung einige
local
fordert es. Dieser Punkt ist IMOeher eine Frage des Geschmacks, da bei Verwendung von Konstruktoren niemand verhindert, dass Sie zu Komponieren, was auch immer Dinge, die Sie mögen,
es sei denn, jemand tut etwas dummes, wie die Arbeit im Konstruktor die sich als eine schlechte Praxis in der OOP.
sequence
,traverse
Methoden implementiert für Sie kostenlos.Mit Konstruktoren niemand hindert Sie, das zu tun, müssen Sie nur bauen die ganze objektgraphen neu für jede Config
eingehende. Ich habe zwar kein problem damit (ich selbst bevorzuge dabei, dass bei jeder Anforderung an die Anwendung), ist es nicht
eine naheliegende Idee, um viele Menschen für Gründe kann ich nur spekulieren.
Ich würde auch gerne sagen, was ich nicht mag, im Leser.
ein session-cookie oder in eine Datenbank. Für mich gibt es wenig Sinn, mit Reader für nahezu konstanter Objekte, wie E-Mail
server oder repository aus diesem Beispiel. Für solche Abhängigkeiten finde ich schlicht Konstruktoren und/oder teilweise angewandte Funktionen
die Art und Weise besser. Im wesentlichen Reader gibt Ihnen die Flexibilität, so können Sie die Abhängigkeiten bei jedem Anruf, aber wenn Sie
nicht wirklich brauchen, zahlen Sie nur die Steuer.
die lauten Teile, die mit implicits und machen einige Fehler, compiler geben Ihnen manchmal schwer zu entziffern, Nachrichten.
pure
,local
und das erstellen von eigenen Config-Klassen /mit Tupeln für, die. Leser zwingt Sie, fügen Sie einige codedas ist nicht über die problem domain, also die Einführung von etwas Rauschen in den code. Auf der anderen Seite, eine Anwendung
verwendet Konstruktoren oft verwendet, factory pattern, welches auch von außerhalb der problem-Domäne, so dass diese Schwäche ist das nicht, dass
ernst.
Was, wenn ich nicht konvertieren möchte meine Klassen von Objekten mit Funktionen?
Du willst. Sie technisch kann vermeiden, aber schau, was passieren würde, wenn ich nicht konvertieren
FindUsers
- Klasse-Objekt. Die entsprechende Zeile ist für das Verständnis Aussehen würde:was nicht lesbar ist, ist, dass? Der Punkt ist, dass Leser arbeitet auf Funktionen, so dass, wenn Sie diese nicht haben bereits, müssen Sie Sie halt inline, das ist oft nicht schön.
Datastore
undEmailServer
Links sind als Züge, und andere wurdenobject
s? Gibt es einen fundamentalen Unterschied in diesen Diensten/Abhängigkeiten/(aber Sie es nennen), die bewirkt, dass Sie anders behandelt werden?EmailSender
auf ein Objekt als gut, richtig? Würde ich das nicht Ausdrücken können, der Abhängigkeit, ohne die Art...EmailSender
Sie würde davon abhängen(String, String) => Unit
. Ob das überzeugt oder nicht ist ein anderes Thema 🙂 um sicher Zu sein, ist es mehr generische zumindest, da jeder bereits hängtFunction2
.(String, String) => Unit
so, dass es vermittelt eine gewisse Bedeutung, wenn auch nicht mit einer Art alias, sondern mit etwas, das überprüft zur compile-Zeit 😉(EmailAddress, EmailContent) => scalaz.concurrent.Task
dann vielleicht compiler würde prüfen, ob es ausreichend und zur gleichen Zeit meine Nutzer würde eine Funktion, die einfach zu Komponieren, mit anderen Dingen.object EmailServerFactory { def create: URL => EmailServer = ??? }
. Dann sind 3 Schritte notwendig: Schritt 1 in der config-ich bin Austausch der EmailServer nur mit der URL. Schritt 2 in derfor
ich legteserver <- EmailServerFactory.create.local[Config](_.emailServerURL)
Schritt 3 ich einstellenemailInactive
Linie:emailInactive <- pure(UserReminder.emailInactive(getAddresses)(server))
pure
Funktion? Was muss ich importieren, es zu haben?object Reader
definiert, die am Anfang des Beispiels. Froh, dass Sie finden meine Antwort Ihre Aufmerksamkeit Wert 🙂pure(a)
ist nur eine Abkürzung fürReader(_ => a)
so, auch wenn eine Bibliothek nicht haben, es ist einfach zu schreiben. Nicht sicher, ob Katzen oder scalaz haben einige äquivalent vonpure
ich denke, ich nahm es von speakerdeck.com/marakana/...Ich denke, der wesentliche Unterschied ist, dass in Ihrem Beispiel, Sie sind intravenös alle Abhängigkeiten, wenn Objekte instantiiert werden. Die Reader-Monade im Grunde baut sich eine mehr und mehr komplexe Funktionen zu nennen, angesichts der Abhängigkeiten, die dann wieder in den höchsten Schichten. In diesem Fall wird die Einspritzung geschieht, wenn die Funktion endlich aufgerufen.
Einen unmittelbaren Vorteil ist die Flexibilität, vor allem, wenn Sie erstellen können Sie Ihre Monade einmal und dann wollen Sie es mit verschiedenen injiziert Abhängigkeiten. Ein Nachteil ist, wie du sagst, möglicherweise weniger Klarheit. In beiden Fällen, die Zwischenschicht muss nur wissen, über Ihre unmittelbaren Abhängigkeiten, so dass Sie beide arbeiten, wie in der Werbung für DI.
Config
enthält einen Verweis aufUserRepository
. Also es stimmt, es ist nicht direkt sichtbar in der Signatur, aber ich würde sagen, das ist noch schlimmer, Sie haben wirklich keine Idee, welche Abhängigkeiten Ihr code wird mit auf den ersten Blick. Nicht angewiesen auf eineConfig
mit all den Abhängigkeiten bedeuten, jede Methode, Art, hängt von allen von Ihnen?config
, und was ist "nur eine Funktion". Wahrscheinlich würden Sie am Ende mit einer Menge von selbst-Abhängigkeiten als auch. Das ist sowieso mehr eine Frage der Präferenz Diskussion, als eine Q&A 🙂