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!
acid-state
sieht 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-state
neben einer Präsentationsebene verwendet werden, und schaffen Sie es, die beiden voneinander zu trennen?
Query
und Update
Monaden sind eigentlich ziemlich weit entfernt IO
. Ich werde versuchen, ein einfaches Beispiel in einer Antwort zu geben.
acid-state
scheint insbesondere nah an dem zu sein, was Sie beschreiben .