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!
la source
acid-state
semble être proche de ce que vous décrivez .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
etUpdate
sont assez éloignéesIO
, en fait. Je vais essayer de donner un exemple simple dans une réponse.Réponses:
La manière générale de séparer les composants dans Haskell consiste à utiliser des piles de transformateurs monades. J'explique cela plus en détail ci-dessous.
Imaginez que nous construisons un système qui comprend plusieurs composants à grande échelle:
Nous décidons que nous devons garder ces composants faiblement couplés afin de maintenir un bon style de code.
Par conséquent, nous codons chacun de nos composants de manière polymorphe, en utilisant les différentes classes MTL pour nous guider:
MonadState DataState m => Foo -> Bar -> ... -> m Baz
DataState
est une pure représentation d'un instantané de l'état de notre base de données ou de notre stockageMonadState UIState m => Foo -> Bar -> ... -> m Baz
UIState
est une pure représentation d'un instantané de l'état de notre interface utilisateurMonadState (DataState, UIState) m => Foo -> Bar -> ... -> m Baz
main :: IO ()
qui fait le travail presque trivial de combiner les autres composants en un seul systèmezoom
ou un combinateur similaireStateT (DataState, UIState) IO
, qui est ensuite exécuté avec le contenu réel de la base de données ou du stockage à produireIO
.la source
DataState
est une pure représentation d'un instantané de l'état de notre base de données ou stockage". Vraisemblablement, vous ne voulez pas charger toute la base de données en mémoire!Le modèle doit-il être conservé? Dans de nombreux programmes, l'enregistrement du modèle est nécessaire car l'état est imprévisible, toute opération peut muter le modèle de quelque manière que ce soit, donc la seule façon de connaître l'état du modèle est d'y accéder directement.
Si, dans votre scénario, la séquence d'événements (commandes qui ont été validées et acceptées) peut toujours générer l'état, alors ce sont les événements qui doivent être persistés, pas nécessairement l'état. L'état peut toujours être généré en rejouant les événements.
Cela dit, souvent, l'état est stocké, mais juste comme un instantané / cache pour éviter de rejouer les commandes, pas comme des données de programme essentielles.
Une fois la commande acceptée, l'événement est communiqué à deux destinations (le stockage des événements et le système de génération de rapports) mais sur la même couche du programme.
Voir aussi Dérivation de lecture avide de
sourçage d'événements
la source
Vous essayez de mettre de l'espace dans votre application intensive IO pour toutes les activités non-IO; malheureusement, les applications CRUD typiques comme vous en parlez ne font guère autre chose que IO.
Je pense que vous comprenez l'amende de séparation pertinente, mais lorsque vous essayez de placer le code d'E / S persistant à un certain nombre de couches du code de présentation, le fait général de la question se trouve dans votre contrôleur quelque part que vous devriez appeler votre couche de persistance, qui peut vous sembler trop proche de votre présentation - mais ce n'est qu'une coïncidence dans ce type d'application qui n'a pas grand-chose d'autre.
La présentation et la persistance constituent essentiellement l'intégralité du type d'application que je pense que vous décrivez ici.
Si vous pensez dans votre tête à une application similaire qui contenait beaucoup de logique métier complexe et de traitement de données, je pense que vous vous trouverez en mesure d'imaginer comment cela est bien séparé de l'IO de présentation et des choses d'E / S de persistance telles que il n'a besoin de rien savoir non plus. Le problème que vous avez en ce moment est juste un problème de perception causé par la recherche d'une solution à un problème dans un type d'application qui n'a pas ce problème pour commencer.
la source
Autant que je puisse comprendre votre question (ce que je ne peux pas, mais pensais que je mettrais mes 2 cents), puisque vous n'avez pas nécessairement accès aux objets eux-mêmes, vous devez avoir votre propre base de données d'objets qui s'auto expire avec le temps).
Idéalement, les objets eux-mêmes peuvent être améliorés pour stocker leur état. Ainsi, lorsqu'ils sont «contournés», les différents processeurs de commande sauront avec quoi ils travaillent.
Si ce n'est pas possible, (icky icky), la seule façon est d'avoir une clé de type DB commune, que vous pouvez utiliser pour stocker les informations dans un magasin qui est configuré pour être partagé entre différentes commandes - et, espérons-le, "ouvrez" l'interface et / ou le code afin que tout autre rédacteur de commandes adopte également votre interface pour la sauvegarde et le traitement des méta-informations.
Dans le domaine des serveurs de fichiers, samba a différentes façons de stocker des choses comme les listes d'accès et les flux de données alternatifs, en fonction de ce que le système d'exploitation hôte fournit. Idéalement, samba est hébergé sur un système de fichiers qui fournit des attributs étendus sur les fichiers. Exemple 'xfs' sur 'linux' - plus de commandes copient des attributs étendus avec un fichier (par défaut, la plupart des utilitaires sur linux "ont grandi" sans penser à des attributs étendus).
Une solution alternative - qui fonctionne pour plusieurs processus samba de différents utilisateurs opérant sur des fichiers communs (objets), est que si le système de fichiers ne prend pas en charge l'attachement de la ressource directement au fichier comme avec les attributs étendus, utilise un module qui implémente une couche de système de fichiers virtuel pour émuler des attributs étendus pour les processus samba. Seul samba le sait, mais il a l'avantage de fonctionner lorsque le format d'objet ne le prend pas en charge, mais fonctionne toujours avec divers utilisateurs de samba (cf. processeurs de commande) qui effectuent un certain travail sur le fichier en fonction de son état précédent. Il stockera les méta-informations dans une base de données commune pour le système de fichiers qui aide à contrôler la taille de la base de données (et ne
Il peut ne pas vous être utile si vous aviez besoin de plus d'informations spécifiques à l'implémentation avec laquelle vous travaillez, mais conceptuellement, la même théorie pourrait être appliquée aux deux ensembles de problèmes. Donc, si vous cherchiez des algorithmes et des méthodes pour faire ce que vous voulez, cela pourrait vous aider. Si vous aviez besoin de connaissances plus spécifiques dans un cadre spécifique, alors peut-être pas si utile ... ;-)
BTW - la raison pour laquelle je mentionne «expirant automatiquement» - est que ce n'est pas clair si vous savez quels objets sont là-bas et combien de temps ils persistent. Si vous n'avez aucun moyen direct de savoir quand un objet est supprimé, vous devez couper votre propre métaDB pour l'empêcher de se remplir d'anciennes ou anciennes métadonnées pour lesquelles les utilisateurs ont depuis longtemps supprimé les objets.
Si vous savez quand les objets sont arrivés à expiration / supprimés, vous êtes en avance sur le jeu et pouvez l'expirer en même temps de votre métaDB, mais il n'était pas clair si vous aviez cette option.
À votre santé!
la source