Wie passt Beharrlichkeit in eine rein funktionale Sprache?

18

Wie passt das Muster der Verwendung von Befehlshandlern für den Umgang mit Persistenz in eine rein funktionale Sprache, in der IO-Code so dünn wie möglich gestaltet werden soll?


Bei der Implementierung von Domain-Driven Design in einer objektorientierten Sprache wird häufig das Command / Handler-Muster verwendet , um Statusänderungen auszuführen. In diesem Entwurf befinden sich Befehlshandler über Ihren Domänenobjekten und sind für die langweilige persistenzbezogene Logik wie die Verwendung von Repositorys und das Veröffentlichen von Domänenereignissen verantwortlich. Die Handler sind das öffentliche Gesicht Ihres Domain-Modells. Anwendungscode wie die Benutzeroberfläche ruft die Handler auf, wenn der Status von Domänenobjekten geändert werden muss.

Eine Skizze in C #:

public class DiscardDraftDocumentCommandHandler : CommandHandler<DiscardDraftDocument>
{
    IDraftDocumentRepository _repo;
    IEventPublisher _publisher;

    public DiscardDraftCommandHandler(IDraftDocumentRepository repo, IEventPublisher publisher)
    {
        _repo = repo;
        _publisher = publisher;
    }

    public override void Handle(DiscardDraftDocument command)
    {
        var document = _repo.Get(command.DocumentId);
        document.Discard(command.UserId);
        _publisher.Publish(document.NewEvents);
    }
}

Das documentDomänenobjekt ist verantwortlich für die Implementierung der Geschäftsregeln (z. B. "Der Benutzer sollte die Berechtigung zum Verwerfen des Dokuments haben" oder "Sie können ein bereits verworfenes Dokument nicht verwerfen") und für die Generierung der Domänenereignisse, die wir veröffentlichen müssen ( document.NewEventswürden) ein sein IEnumerable<Event>und wahrscheinlich ein DocumentDiscardedEreignis enthalten würde ).

Dies ist ein ansprechendes Design - es ist einfach zu erweitern (Sie können neue Anwendungsfälle hinzufügen, ohne das Domänenmodell zu ändern, indem Sie neue Befehlshandler hinzufügen) und es ist unabhängig davon, wie Objekte beibehalten werden (Sie können ein NHibernate-Repository einfach gegen ein Mongo austauschen) Repository oder tauschen Sie einen RabbitMQ-Publisher gegen einen EventStore-Publisher aus. Dies erleichtert das Testen mit Fakes und Mocks. Es wird auch die Trennung von Modell und Ansicht beachtet - der Befehlshandler weiß nicht, ob er von einem Stapeljob, einer GUI oder einer REST-API verwendet wird.


In einer rein funktionalen Sprache wie Haskell könnten Sie den Befehlshandler ungefähr so ​​modellieren:

newtype CommandHandler = CommandHandler {handleCommand :: Command -> IO Result)
data Result a = Success a | Failure Reason
type Reason = String

discardDraftDocumentCommandHandler = CommandHandler handle
    where handle (DiscardDraftDocument documentID userID) = do
              document <- loadDocument documentID
              let result = discard document userID :: Result [Event]
              case result of
                   Success events -> publishEvents events >> return result
                   -- in an event-sourced model, there's no extra step to save the document
                   Failure _ -> return result
          handle _ = return $ Failure "I expected a DiscardDraftDocument command"

Hier ist der Teil, den ich nur schwer verstehen kann. Typischerweise wird es eine Art 'Präsentations'-Code geben, der den Befehlshandler aufruft, wie z. B. eine GUI oder eine REST-API. Jetzt haben wir zwei Ebenen in unserem Programm, die IO ausführen müssen - den Befehlshandler und die Ansicht -, was in Haskell ein großes No-No ist.

Soweit ich das beurteilen kann, gibt es hier zwei gegensätzliche Kräfte: Eine ist die Trennung von Modell und Ansicht und die andere ist die Notwendigkeit, das Modell beizubehalten. Es muss einen E / A-Code geben, um das Modell irgendwo zu erhalten , aber die Trennung von Modell und Ansicht besagt, dass wir es nicht mit allen anderen E / A-Codes in die Präsentationsschicht einfügen können.

Natürlich kann (und tut) IO in einer "normalen" Sprache überall vorkommen. Gutes Design schreibt vor, dass die verschiedenen Arten von E / A getrennt bleiben müssen, der Compiler dies jedoch nicht erzwingt.

Also: Wie bringen wir die Trennung von Modell und Ansicht mit dem Wunsch in Einklang, den E / A-Code an den äußersten Rand des Programms zu bringen, wenn das Modell beibehalten werden muss? Wie können wir die beiden verschiedenen Arten von E / A-Vorgängen voneinander trennen , ohne den reinen Code zu verwenden?


Update : Das Kopfgeld läuft in weniger als 24 Stunden ab. Ich habe nicht das Gefühl, dass eine der aktuellen Antworten meine Frage überhaupt beantwortet hat. @ Pthariens Flame's Kommentar über acid-statescheint vielversprechend, aber es ist keine Antwort und es fehlt im Detail. Ich würde es hassen, wenn diese Punkte verschwendet würden!

Benjamin Hodgson
quelle
1
Vielleicht wäre es hilfreich, sich das Design verschiedener Persistenzbibliotheken in Haskell anzuschauen. acid-statescheint insbesondere nah an dem zu sein, was Sie beschreiben .
Pthariens Flamme
1
acid-statesieht ziemlich gut aus, danke für diesen Link. In Bezug auf API-Design scheint es immer noch gebunden zu sein IO; Meine Frage ist, wie ein Persistenz-Framework in eine größere Architektur passt. Kennen Sie Open-Source-Anwendungen, die acid-stateneben einer Präsentationsebene verwendet werden, und schaffen Sie es, die beiden voneinander zu trennen?
Benjamin Hodgson
Die Queryund UpdateMonaden sind eigentlich ziemlich weit entfernt IO. Ich werde versuchen, ein einfaches Beispiel in einer Antwort zu geben.
Pthariens Flamme
Bei Lesern, die das Command / Handler-Muster auf diese Weise verwenden, empfehle ich dringend, Akka.NET zu besuchen. Das Darstellermodell fühlt sich hier gut an. Bei Pluralsight gibt es einen großartigen Kurs dafür. (Ich schwöre, ich bin nur ein Fan, kein Werbebot.)
RJB

Antworten:

6

Die allgemeine Methode zum Trennen von Komponenten in Haskell besteht in der Verwendung von Monadentransformatorstapeln. Ich erkläre dies im Folgenden ausführlicher.

Stellen Sie sich vor, wir bauen ein System mit mehreren großen Komponenten:

  • eine Komponente, die mit dem Datenträger oder der Datenbank (Submodell) kommuniziert
  • eine Komponente, die Transformationen auf unserer Domain durchführt (Modell)
  • eine Komponente, die mit dem Benutzer interagiert (Ansicht)
  • eine Komponente, die die Verbindung zwischen Ansicht, Modell und Untermodell (Controller) beschreibt
  • eine Komponente, die das gesamte System (den Treiber) startet

Wir beschließen, diese Komponenten lose miteinander zu verbinden, um einen guten Codestil beizubehalten.

Daher codieren wir jede unserer Komponenten polymorph, wobei wir die verschiedenen MTL-Klassen verwenden, um uns zu leiten:

  • Jede Funktion im Untermodell ist vom Typ MonadState DataState m => Foo -> Bar -> ... -> m Baz
    • DataState ist eine reine Darstellung eines Schnappschusses des Status unserer Datenbank oder unseres Speichers
  • Jede Funktion im Modell ist rein
  • Jede Funktion in der Ansicht ist vom Typ MonadState UIState m => Foo -> Bar -> ... -> m Baz
    • UIState ist eine reine Darstellung eines Schnappschusses des Zustands unserer Benutzeroberfläche
  • Jede Funktion in der Steuerung ist vom Typ MonadState (DataState, UIState) m => Foo -> Bar -> ... -> m Baz
    • Beachten Sie, dass der Controller sowohl auf den Status der Ansicht als auch auf den Status des Submodells zugreifen kann
  • Der Treiber hat nur eine Definition, main :: IO ()die die fast triviale Arbeit des Kombinierens der anderen Komponenten in einem System erledigt
    • Die Ansicht und das Untermodell müssen in den gleichen Zustand versetzt werden, wie der Controller, der zoomeinen ähnlichen Kombinator verwendet
    • Das Modell ist rein und kann daher uneingeschränkt verwendet werden
    • Am Ende lebt alles in (einem kompatiblen Typ) StateT (DataState, UIState) IO, der dann mit dem tatsächlichen Inhalt der zu erstellenden Datenbank oder des zu erstellenden Speichers ausgeführt wird IO.
Pthariens Flamme
quelle
1
Dies ist ein ausgezeichneter Rat und genau das, wonach ich gesucht habe. Vielen Dank!
Benjamin Hodgson
2
Ich verdaue diese Antwort. Könnten Sie bitte die Rolle des "Submodells" in dieser Architektur klären? Wie wird "mit dem Datenträger oder der Datenbank gesprochen", ohne dass E / A-Vorgänge ausgeführt werden? Ich bin besonders verwirrt darüber, was Sie unter " DataStateIst eine reine Darstellung eines Schnappschusses des Status unserer Datenbank oder unseres Speichers" verstehen . Vermutlich wollen Sie nicht die gesamte Datenbank in den Speicher laden!
Benjamin Hodgson
1
Ich würde mich sehr über Ihre Gedanken zu einer C # -Implementierung dieser Logik freuen. Glaubst du nicht, ich kann dich mit einer Gegenstimme bestechen? ;-)
RJB
1
@RJB Leider müsste man das C # -Entwicklungsteam bestechen, um höhere Arten in der Sprache zuzulassen, denn ohne sie fällt diese Architektur etwas platt aus.
Pthariens Flamme
4

Also: Wie bringen wir die Trennung von Modell und Ansicht mit dem Wunsch in Einklang, den E / A-Code an den äußersten Rand des Programms zu bringen, wenn das Modell beibehalten werden muss?

Sollte das Modell bestehen bleiben müssen? In vielen Programmen ist das Speichern des Modells erforderlich, da der Status unvorhersehbar ist und jede Operation das Modell auf irgendeine Weise verändern kann. Der einzige Weg, den Status des Modells zu ermitteln, besteht darin, direkt darauf zuzugreifen.

Wenn in Ihrem Szenario die Abfolge von Ereignissen (Befehle, die validiert und akzeptiert wurden) immer den Status generieren kann, müssen die Ereignisse beibehalten werden, nicht unbedingt der Status. Der Zustand kann immer durch Wiederholen der Ereignisse erzeugt werden.

Allerdings wird der Status häufig nur als Momentaufnahme / Cache gespeichert, um die Wiederholung der Befehle zu vermeiden, und nicht als wesentliche Programmdaten.

Jetzt haben wir zwei Ebenen in unserem Programm, die IO ausführen müssen - den Befehlshandler und die Ansicht -, was in Haskell ein großes No-No ist.

Nachdem der Befehl akzeptiert wurde, wird das Ereignis an zwei Ziele (Ereignisspeicher und Berichtssystem) auf derselben Programmebene übertragen.

Siehe auch Eager-Read-Ableitung für
Event-Sourcing

FMJaguar
quelle
2
Ich kenne mich mit Event-Sourcing aus (ich verwende es in meinem obigen Beispiel!), Und um das Teilen von Haaren zu vermeiden, würde ich immer noch sagen, dass Event-Sourcing eine Herangehensweise an das Problem der Persistenz ist. In jedem Fall muss durch die Ereignisbeschaffung das Laden der Domänenobjekte im Befehlshandler nicht vermieden werden . Der Befehlshandler weiß nicht, ob die Objekte aus einem Ereignisstrom, einem ORM oder einer gespeicherten Prozedur stammen - er bezieht sie nur aus dem Repository.
Benjamin Hodgson
1
Ihr Verständnis scheint die Ansicht und den Befehlshandler zu koppeln, um mehrere E / A zu erstellen. Ich verstehe, dass der Handler das Ereignis generiert und kein weiteres Interesse hat. Die Ansicht in dieser Instanz fungiert als separates Modul (auch wenn sie sich technisch in derselben Anwendung befindet) und ist nicht an den Befehlshandler gekoppelt.
FMJaguar
1
Ich denke, wir reden vielleicht miteinander. Wenn ich 'view' sage, spreche ich von der gesamten Präsentationsebene, die eine REST-API oder ein Model-View-Controller-System sein kann. (Ich stimme zu, dass die Ansicht im MVC-Muster vom Modell entkoppelt werden sollte.) Ich meine im Grunde "was auch immer in den Befehlshandler gerufen wird".
Benjamin Hodgson
2

Sie versuchen, in Ihrer E / A-intensiven Anwendung Platz für alle Nicht-E / A-Aktivitäten zu schaffen. Leider machen typische CRUD-Apps, über die Sie sprechen, nur wenig anderes als IO.

Ich denke, Sie verstehen die relevante Trennung gut, aber wenn Sie versuchen, den Persistenz-E / A-Code in einigen Schichten vom Präsentationscode zu entfernen, ist die allgemeine Tatsache in Ihrem Controller zu suchen, wo Sie ihn anrufen sollten Persistenz-Ebene, die sich zu nahe an Ihrer Präsentation anfühlt - aber das ist nur ein Zufall, dass diese Art von App wenig anderes zu bieten hat.

Präsentation und Beharrlichkeit machen im Grunde genommen die Gesamtheit der Art von App aus, die Sie hier beschreiben.

Wenn Sie in Ihrem Kopf über eine ähnliche Anwendung nachdenken, die eine Menge komplexer Geschäftslogik und Datenverarbeitung enthält, werden Sie sich sicher vorstellen können, wie gut das von den Präsentations- und Persistenz-I / O-Dingen getrennt ist es braucht auch nichts zu wissen. Das Problem, das Sie gerade haben, ist nur ein Wahrnehmungsproblem, das dadurch verursacht wird, dass Sie versuchen, eine Lösung für ein Problem in einer Art von Anwendung zu finden, bei der dieses Problem zunächst nicht auftritt.

Jimmy Hoffa
quelle
1
Sie sagen, dass es für CRUD-Systeme in Ordnung ist, Persistenz und Präsentation zu verbinden. Das scheint mir vernünftig; CRUD habe ich jedoch nicht erwähnt. Ich frage speziell nach DDD, wo Sie Geschäftsobjekte mit komplexen Interaktionen, einer Persistenzschicht (Befehlshandler) und einer Präsentationsschicht darüber haben. Wie lassen sich die beiden E / A-Schichten voneinander trennen, während ein dünner E / A-Wrapper beibehalten wird ?
Benjamin Hodgson
1
NB, die Domain, die ich in der Frage beschrieben habe, könnte sehr komplex sein. Das Verwerfen eines Dokumententwurfs unterliegt möglicherweise einer Berechtigungsprüfung, oder es müssen möglicherweise mehrere Versionen desselben Entwurfs bearbeitet oder Benachrichtigungen gesendet werden, oder die Aktion muss von einem anderen Benutzer genehmigt werden, oder Entwürfe durchlaufen eine Reihe von Vorgängen Lebenszyklusphasen vor dem Abschluss ...
Benjamin Hodgson
2
@BenjaminHodgson Ich würde dringend davon abraten, DDD oder andere inhärente OO-Entwurfsmethoden in diese Situation in Ihrem Kopf zu mischen. Es wird nur verwirren. Während Sie in Pure FP Objekte wie Bits und Bobs erstellen können, sollten die darauf basierenden Designansätze nicht unbedingt Ihre erste Wahl sein. In dem von Ihnen beschriebenen Szenario stelle ich mir wie oben erwähnt einen Controller vor, der zwischen den beiden E / A und dem reinen Code kommuniziert: Präsentation E / A geht in den Controller ein und wird von diesem angefordert. Der Controller übergibt die Dinge an die reinen Abschnitte und an die Persistenzabschnitte.
Jimmy Hoffa
1
@BenjaminHodgson Sie können sich eine Blase vorstellen, in der all Ihr reiner Code lebt, mit all den Ebenen und der Phantasie, die Sie in jedem Design haben möchten, das Sie zu schätzen wissen. Der Einstiegspunkt für diese Blase wird ein winziges Stück sein, das ich als "Controller" bezeichne (vielleicht fälschlicherweise), der die Kommunikation zwischen Präsentation, Ausdauer und reinen Stücken übernimmt. Auf diese Weise weiß Ihre Beharrlichkeit nichts von Präsentation oder pur und umgekehrt - und dies hält Ihre IO-Inhalte in dieser dünnen Schicht über der Blase Ihres puren Systems.
Jimmy Hoffa
2
@BenjaminHodgson Dieser Ansatz von "intelligenten Objekten", von dem Sie sprechen, ist von Natur aus ein schlechter Ansatz für FP. Das Problem bei intelligenten Objekten in FP ist, dass sie viel zu viel koppeln und viel zu wenig verallgemeinern. Sie erhalten Daten und damit verbundene Funktionen, wobei FP bevorzugt, dass Ihre Daten lose mit Funktionen gekoppelt sind, sodass Sie die zu verallgemeinernden Funktionen implementieren können und diese dann für mehrere Datentypen verwendet werden können. Lesen Sie meine Antwort hier: programmers.stackexchange.com/questions/203077/203082#203082
Jimmy Hoffa
1

So gut ich Ihre Frage verstehen kann (was ich vielleicht nicht, aber ich dachte, ich würde meine 2 Cent einwerfen), da Sie nicht unbedingt Zugriff auf die Objekte selbst haben, müssen Sie eine eigene Objektdatenbank haben, die verfällt im Laufe der Zeit).

Idealerweise können die Objekte selbst so erweitert werden, dass sie ihren Status speichern, sodass verschiedene Befehlsprozessoren wissen, mit was sie arbeiten, wenn sie "herumgereicht" werden.

Wenn dies nicht möglich ist (icky icky), besteht die einzige Möglichkeit darin, einen gemeinsamen DB-ähnlichen Schlüssel zu haben, mit dem Sie die Informationen in einem Geschäft speichern können, das so eingerichtet ist, dass sie zwischen verschiedenen Befehlen geteilt werden können - und hoffentlich Öffnen Sie die Schnittstelle und / oder den Code, damit auch andere Befehlsschreiber Ihre Schnittstelle zum Speichern und Verarbeiten von Metainformationen übernehmen.

Im Bereich der Dateiserver hat Samba verschiedene Möglichkeiten, um Dinge wie Zugriffslisten und alternative Datenströme zu speichern, je nachdem, was das Host-Betriebssystem bereitstellt. Im Idealfall wird Samba auf einem Dateisystem gehostet, das erweiterte Attribute für Dateien bereitstellt. Beispiel 'xfs' unter 'linux' - Weitere Befehle kopieren erweiterte Attribute zusammen mit einer Datei (standardmäßig sind die meisten Linux-Utils "erwachsen", ohne dass sie wie erweiterte Attribute aussehen).

Eine alternative Lösung - die für mehrere Samba-Prozesse von verschiedenen Benutzern funktioniert, die mit gemeinsamen Dateien (Objekten) arbeiten - besteht darin, dass, wenn das Dateisystem das direkte Anhängen der Ressource an die Datei wie bei erweiterten Attributen nicht unterstützt, ein Modul verwendet wird, das implementiert wird eine virtuelle Dateisystemschicht, um erweiterte Attribute für Samba-Prozesse zu emulieren. Nur Samba weiß davon, aber es hat den Vorteil, dass es funktioniert, wenn das Objektformat es nicht unterstützt, es aber mit verschiedenen Samba-Benutzern (vgl. Befehlsprozessoren) zusammenarbeitet, die auf der Grundlage seines vorherigen Status einige Arbeiten an der Datei ausführen. Es speichert die Metainformationen in einer gemeinsamen Datenbank für das Dateisystem, die bei der Steuerung der Größe der Datenbank hilft (und nicht '

Es mag für Sie nicht nützlich sein, wenn Sie weitere Informationen benötigen, die spezifisch für die Implementierung sind, mit der Sie arbeiten, aber konzeptionell könnte dieselbe Theorie auf beide Problemgruppen angewendet werden. Wenn Sie also nach Algorithmen und Methoden suchen, um das zu tun, was Sie wollen, könnte dies hilfreich sein. Wenn Sie spezifisches Wissen in einem bestimmten Rahmen benötigen, dann vielleicht nicht so hilfreich ... ;-)

Übrigens - der Grund, warum ich "selbstverfallend" erwähne - ist, dass es nicht klar ist, ob Sie wissen, welche Objekte da draußen sind und wie lange sie bestehen. Wenn Sie nicht direkt wissen können, wann ein Objekt gelöscht wurde, müssen Sie Ihre eigene Metadatenbank zuschneiden, um zu verhindern, dass sie mit alten oder alten Metadaten gefüllt wird, für die die Benutzer die Objekte längst gelöscht haben.

Wenn Sie wissen, wann die Objekte abgelaufen / gelöscht sind, sind Sie dem Spiel voraus und können es gleichzeitig aus Ihrer Metadatenbank löschen, aber es war nicht klar, ob Sie diese Option hatten.

Prost!

Astara
quelle
1
Für mich scheint dies eine Antwort auf eine völlig andere Frage zu sein. Ich war auf der Suche nach Tipps zur Architektur in der rein funktionalen Programmierung im Kontext des domänengetriebenen Designs. Könnten Sie bitte Ihre Punkte klarstellen?
Benjamin Hodgson
Sie fragen nach der Datenpersistenz in einem rein funktionalen Programmierparadigma. Wikipedia zitiert: "Rein funktional ist ein Begriff in der Datenverarbeitung, der verwendet wird, um Algorithmen, Datenstrukturen oder Programmiersprachen zu beschreiben, die destruktive Änderungen (Aktualisierungen) von Entitäten in der laufenden Programmumgebung ausschließen." ==== Per Definition ist die Datenpersistenz irrelevant und hat keine Verwendung für etwas, das keine Daten ändert. Genau genommen gibt es keine Antwort auf Ihre Frage. Ich habe versucht, das, was Sie geschrieben haben, lockerer zu interpretieren.
Astara