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 document
Domä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.NewEvents
würden) ein sein IEnumerable<Event>
und wahrscheinlich ein DocumentDiscarded
Ereignis 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-state
scheint vielversprechend, aber es ist keine Antwort und es fehlt im Detail. Ich würde es hassen, wenn diese Punkte verschwendet würden!
quelle
acid-state
scheint insbesondere nah an dem zu sein, was Sie beschreiben .acid-state
sieht ziemlich gut aus, danke für diesen Link. In Bezug auf API-Design scheint es immer noch gebunden zu seinIO
; Meine Frage ist, wie ein Persistenz-Framework in eine größere Architektur passt. Kennen Sie Open-Source-Anwendungen, dieacid-state
neben einer Präsentationsebene verwendet werden, und schaffen Sie es, die beiden voneinander zu trennen?Query
undUpdate
Monaden sind eigentlich ziemlich weit entferntIO
. Ich werde versuchen, ein einfaches Beispiel in einer Antwort zu geben.Antworten:
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:
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:
MonadState DataState m => Foo -> Bar -> ... -> m Baz
DataState
ist eine reine Darstellung eines Schnappschusses des Status unserer Datenbank oder unseres SpeichersMonadState UIState m => Foo -> Bar -> ... -> m Baz
UIState
ist eine reine Darstellung eines Schnappschusses des Zustands unserer BenutzeroberflächeMonadState (DataState, UIState) m => Foo -> Bar -> ... -> m Baz
main :: IO ()
die die fast triviale Arbeit des Kombinierens der anderen Komponenten in einem System erledigtzoom
einen ähnlichen Kombinator verwendetStateT (DataState, UIState) IO
, der dann mit dem tatsächlichen Inhalt der zu erstellenden Datenbank oder des zu erstellenden Speichers ausgeführt wirdIO
.quelle
DataState
Ist eine reine Darstellung eines Schnappschusses des Status unserer Datenbank oder unseres Speichers" verstehen . Vermutlich wollen Sie nicht die gesamte Datenbank in den Speicher laden!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.
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
quelle
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.
quelle
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!
quelle