Il semble y avoir un large consensus dans la communauté OOP selon lequel le constructeur de classe ne doit pas laisser un objet partiellement ou même non initialisé.
Qu'est-ce que j'entends par "initialisation"? Grosso modo, le processus atomique qui amène un objet nouvellement créé dans un état où tous ses invariants de classe tiennent. Cela devrait être la première chose qui arrive à un objet (il ne devrait s'exécuter qu'une seule fois par objet) et rien ne devrait être autorisé à mettre la main sur un objet non initialisé. (D'où les conseils fréquents pour effectuer une initialisation d'objet directement dans le constructeur de classe. Pour la même raison, les
Initialize
méthodes sont souvent désapprouvées, car elles séparent l'atomicité et permettent de saisir et d'utiliser un objet qui n'est pas encore dans un état bien défini.)
Problème: lorsque CQRS est combiné avec le sourcing d'événements (CQRS + ES), où tous les changements d'état d'un objet sont capturés dans une série ordonnée d'événements (flux d'événements), je me demande quand un objet atteint réellement un état entièrement initialisé: À la fin du constructeur de classe, ou après que le tout premier événement a été appliqué à l'objet?
Remarque: je m'abstiens d'utiliser le terme "racine agrégée". Si vous préférez, remplacez-le chaque fois que vous lisez "objet".
Exemple de discussion: Supposons que chaque objet est identifié de manière unique par une Id
valeur opaque (pensez GUID). Un flux d'événements représentant les changements d'état de cet objet peut être identifié dans le magasin d'événements par la même Id
valeur: (Ne nous inquiétons pas du bon ordre des événements.)
interface IEventStore
{
IEnumerable<IEvent> GetEventsOfObject(Id objectId);
}
Supposons en outre qu'il existe deux types d'objet Customer
et ShoppingCart
. Concentrons-nous sur ShoppingCart
: Une fois créés, les paniers sont vides et doivent être associés à exactement un client. Ce dernier bit est un invariant de classe: un ShoppingCart
objet qui n'est pas associé à un Customer
est dans un état non valide.
Dans la POO traditionnelle, on pourrait modéliser cela dans le constructeur:
partial class ShoppingCart
{
public Id Id { get; private set; }
public Customer Customer { get; private set; }
public ShoppingCart(Id id, Customer customer)
{
this.Id = id;
this.Customer = customer;
}
}
Cependant, je ne sais pas comment modéliser cela dans CQRS + ES sans se retrouver avec une initialisation différée. Puisque ce simple bout d'initialisation est effectivement un changement d'état, ne devrait-il pas être modélisé comme un événement?:
partial class CreatedEmptyShoppingCart
{
public ShoppingCartId { get; private set; }
public CustomerId { get; private set; }
}
// Note: `ShoppingCartId` is not actually required, since that Id must be
// known in advance in order to fetch the event stream from the event store.
Cela devrait évidemment être le tout premier événement dans ShoppingCart
le flux d'événements d' un objet, et cet objet ne serait initialisé qu'une fois que l'événement lui aurait été appliqué.
Donc, si l'initialisation fait partie du flux d'événements "lecture" (qui est un processus très générique qui fonctionnerait probablement de la même manière, que ce soit pour un Customer
objet ou un ShoppingCart
objet ou tout autre type d'objet d'ailleurs) ...
- Le constructeur devrait-il être sans paramètre et ne rien faire, laissant tout le travail à une
void Apply(CreatedEmptyShoppingCart)
méthode (qui est à peu près la même que celle désapprouvéeInitialize()
)? - Ou le constructeur doit-il recevoir un flux d'événements et le lire (ce qui rend l'initialisation atomique à nouveau, mais signifie que le constructeur de chaque classe contient la même logique générique "lecture et application", c'est-à-dire duplication de code indésirable)?
- Ou devrait-il y avoir à la fois un constructeur OOP traditionnel (comme indiqué ci-dessus) qui initialise correctement l'objet, puis tous les événements sauf les premiers lui sont
void Apply(…)
liés?
Je ne m'attends pas à ce que la réponse fournisse une implémentation de démonstration pleinement fonctionnelle; Je serais déjà très heureux si quelqu'un pouvait expliquer où mon raisonnement est défectueux, ou si l'initialisation d'objet est vraiment un "point douloureux" dans la plupart des implémentations CQRS + ES.
Initialize
qu'auraient occupé les constructeurs d'agrégats (+ peut-être une méthode). Cela m'amène à la question: à quoi pourrait ressembler une telle usine?À mon avis, je pense que la réponse ressemble plus à votre suggestion # 2 pour les agrégats existants; pour les nouveaux agrégats plus comme # 3 (mais en traitant l'événement comme vous l'avez suggéré).
Voici du code, j'espère qu'il vous sera utile.
la source
Eh bien, le premier événement doit être qu'un client crée un panier d'achat , donc lorsque le panier d'achat est créé, vous avez déjà un identifiant client dans le cadre de l'événement.
Si un état se tient entre deux événements différents, par définition, c'est un état valide. Donc, si vous dites qu'un panier d'achat valide est associé à un client, cela signifie que vous devez avoir les informations du client lors de la création du panier lui-même
la source
CreatedEmptyShoppingCart
événement dans ma question: il contient les informations client, comme vous le suggérez. Ma question porte davantage sur la manière dont un tel événement est lié au constructeur de classe ou en concurrence avec luiShoppingCart
pendant la mise en œuvre.Remarque: si vous ne souhaitez pas utiliser la racine agrégée, "entité" englobe la plupart de ce que vous demandez ici, tout en évitant les préoccupations concernant les limites des transactions.
Voici une autre façon de voir les choses: une entité est identité + état. Contrairement à une valeur, une entité est le même type même lorsque son état change.
Mais l'État lui-même peut être considéré comme un objet de valeur. J'entends par là que l'État est immuable; l'historique de l'entité est une transition d'un état immuable au suivant - chaque transition correspond à un événement dans le flux d'événements.
La méthode onEvent () est une requête, bien sûr - currentState n'est pas changé du tout, mais currentState calcule les arguments utilisés pour créer le nextState.
Suivant ce modèle, toutes les instances du panier peuvent être considérées comme commençant à la même valeur de départ ....
Séparation des préoccupations - le ShoppingCart traite une commande pour déterminer quel événement devrait être le suivant; l'État ShoppingCart sait comment passer à l'état suivant.
la source