Comment le modèle d'utilisation des gestionnaires de commandes pour gérer la persistance s'intègre-t-il dans un langage purement fonctionnel, où nous voulons rendre le code lié aux IO aussi fin que possible?
Lors de l'implémentation de la conception pilotée par domaine dans un langage orienté objet, il est courant d'utiliser le modèle de commande / gestionnaire pour exécuter des changements d'état. Dans cette conception, les gestionnaires de commandes sont placés au-dessus de vos objets de domaine et sont responsables de la logique ennuyeuse liée à la persistance, comme l'utilisation de référentiels et la publication d'événements de domaine. Les gestionnaires sont la face publique de votre modèle de domaine; Le code d'application comme l'interface utilisateur appelle les gestionnaires lorsqu'il doit changer l'état des objets de domaine.
Un croquis en 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' document
objet de domaine est responsable de la mise en œuvre des règles métier (comme "l'utilisateur doit avoir l'autorisation de supprimer le document" ou "vous ne pouvez pas supprimer un document qui a déjà été supprimé") et de générer les événements de domaine que nous devons publier ( document.NewEvents
serait être un IEnumerable<Event>
et contiendrait probablement un DocumentDiscarded
événement).
Ceci est une conception agréable - il est facile à étendre (vous pouvez ajouter de nouveaux cas d'utilisation sans changer votre modèle de domaine, en ajoutant de nouveaux gestionnaires de commandes) et est agnostique quant à la persistance des objets (vous pouvez facilement échanger un référentiel NHibernate pour un Mongo référentiel ou échanger un éditeur RabbitMQ contre un éditeur EventStore), ce qui facilite le test à l'aide de contrefaçons et de simulations. Il obéit également à la séparation modèle / vue - le gestionnaire de commandes n'a aucune idée s'il est utilisé par un travail par lots, une interface graphique ou une API REST.
Dans un langage purement fonctionnel comme Haskell, vous pouvez modéliser le gestionnaire de commandes à peu près comme ceci:
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"
Voici la partie que j'ai du mal à comprendre. En règle générale, il y aura une sorte de code de «présentation» qui appelle le gestionnaire de commandes, comme une interface graphique ou une API REST. Alors maintenant, nous avons deux couches dans notre programme qui doivent faire des IO - le gestionnaire de commandes et la vue - qui est un gros non-non dans Haskell.
Autant que je sache, il y a deux forces opposées ici: l'une est la séparation modèle / vue et l'autre est la nécessité de maintenir le modèle. Il doit y avoir du code d'E / S pour conserver le modèle quelque part , mais la séparation modèle / vue indique que nous ne pouvons pas le mettre dans la couche de présentation avec tous les autres codes d'E / S.
Bien sûr, dans un langage "normal", les E / S peuvent (et se produisent) n'importe où. Une bonne conception dicte que les différents types d'E / S doivent être séparés, mais le compilateur ne l'applique pas.
Donc: comment réconcilier la séparation modèle / vue avec le désir de pousser le code IO jusqu'au bord du programme, alors que le modèle doit être persistant? Comment séparer les deux différents types d'E / S , mais toujours à l'écart de tout le code pur?
Mise à jour : la prime expire dans moins de 24 heures. Je ne pense pas que l'une des réponses actuelles ait répondu à ma question. @ Le commentaire de Flame de Ptharien acid-state
semble prometteur, mais ce n'est pas une réponse et il manque de détails. Je détesterais que ces points soient perdus!
acid-state
semble très bien, merci pour ce lien. En termes de conception d'API, il semble toujours être lié à IO
; ma question est de savoir comment un cadre de persistance s'intègre dans une architecture plus large. Connaissez-vous des applications open source qui utilisent à acid-state
côté d'une couche de présentation et réussissent à garder les deux séparées?
Query
et Update
sont assez éloignées IO
, en fait. Je vais essayer de donner un exemple simple dans une réponse.
acid-state
semble être proche de ce que vous décrivez .