In che modo il modello di utilizzo dei gestori di comandi per gestire la persistenza si inserisce in un linguaggio puramente funzionale, in cui vogliamo rendere il codice relativo all'IO il più sottile possibile?
Quando si implementa il Domain-Driven Design in un linguaggio orientato agli oggetti, è comune usare il schema Command / Handler per eseguire cambiamenti di stato. In questa progettazione, i gestori di comandi si trovano in cima ai tuoi oggetti di dominio e sono responsabili della logica relativa alla persistenza, come l'utilizzo di repository e la pubblicazione di eventi di dominio. I gestori sono la faccia pubblica del tuo modello di dominio; il codice dell'applicazione come l'interfaccia utente chiama i gestori quando è necessario modificare lo stato degli oggetti di dominio.
Uno schizzo 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);
}
}
L'oggetto dominio document
è responsabile dell'implementazione delle regole aziendali (come "l'utente dovrebbe avere il permesso di scartare il documento" o "non puoi scartare un documento che è già stato scartato") e per generare gli eventi del dominio dobbiamo pubblicare ( document.NewEvents
sarebbe un IEnumerable<Event>
e probabilmente conterrebbe un evento DocumentDiscarded
).
Questo è un bel progetto - è facile da estendere (puoi aggiungere nuovi casi d'uso senza cambiare il tuo modello di dominio, aggiungendo nuovi gestori di comandi) ed è agnostico su come gli oggetti sono persistenti (puoi facilmente scambiare un repository di NHibernate per un repository Mongo, o scambiare un editore RabbitMQ per un editore di EventStore) che rende facile testare utilizzando falsi e mock. Obbedisce anche alla separazione modello / vista: il gestore comandi non ha idea se viene utilizzato da un processo batch, una GUI o un'API REST.
In un linguaggio puramente funzionale come Haskell, potresti modellare il gestore di comandi più o meno così:
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"
Ecco la parte che sto cercando di capire. Tipicamente, ci sarà una sorta di codice di 'presentazione' che chiama nel gestore comandi, come una GUI o un'API REST. Così ora abbiamo due livelli nel nostro programma che devono fare IO - il gestore di comandi e la vista - che è un grande no-no in Haskell.
Per quanto posso capire, ci sono due forze opposte qui: una è la separazione modello / vista e l'altra è la necessità di mantenere il modello. Ci deve essere un codice IO per mantenere il modello da qualche parte , ma la separazione modello / vista dice che non possiamo metterlo nel livello di presentazione con tutti gli altri codici IO.
Naturalmente, in una lingua "normale", IO può (e succede) accadere ovunque. Il buon design impone che i diversi tipi di IO siano tenuti separati, ma il compilatore non lo applica.
Quindi: come conciliare la separazione modello / vista con il desiderio di spingere il codice IO fino al limite del programma, quando il modello deve essere persistente? Come possiamo mantenere separati i due diversi tipi di IO , ma siamo comunque lontani da tutto il codice puro?
Aggiornamento : la taglia scade tra meno di 24 ore. Non credo che nessuna delle risposte attuali abbia affrontato la mia domanda. @ Il commento di Ptharien's Flame su acid-state
sembra promettente, ma non è una risposta e manca nei dettagli. Odio per questi punti sprecare!