Avec autant de buzzwords dans le titre, explicitons le menu :? – Nous commencerons avec une étude des principes du CQRS et la notion de projection pour construire les modèles de données dédiées à la lecture, le tout avec un datastore traditionnel (relationnel). ? – Nous continuerons avec le concept d’état en programmation fonctionnelle, et comment les gérer au sein d’une application tout en respectant le principe d’immutabilité. Et comment ils ont transformé la gestion d’états pour la construction d’interface utilisateur. ? – Dans un troisième temps, nous nous intéresserons aux évènements du domaine-métier dans le Domain Driven Design et comment ceux-ci s’intègrent dans la mécanique de construction des projections. ?– Enfin, nous assemblerons toutes ces notions pour faire apparaitre l’«?event sourcing?» comme modèle de persistance pour nos données. ? – Pour clôturer, nous verrons les erreurs les plus courantes rencontrées lors de l’implémentation d’un modèle en event sourcing. Take away: ?– Utiliser CQRS (sans event-sourcing) pour simplifier la gestion de la persistance dans son application. ?– Comprendre comment gérer des états dans un contexte fonctionnel ? – Gérer facilement les évènements-métier au sein d’une architecture DDD. ?– Savoir comment implémenter correctement un système basé sur l’event sourcing.
Avant de démarrer, disclaimer : j'espère que vous avez pris du café pendant la pose car j'ai une centaine de slides à dérouler en 40 minutes ! Si certains sont intéressés mon Twitter c'est @lilobase et je posterai l'ensemble dessus.
Il y a du code dans ces slides : ne cherchez pas à lire tout ce qui à l'écran, vous n'y arriverez pas.
L'idée est justement que vous puissiez aussi reprendre plus calmement après certains des exemples affichés au fur et à mesure.
Aujourd'hui on va discuter d'une opposition assez fréquente : le data driven vs. le domain driven.
La question c'est : est-ce qu'on parle de quelles données je vais manipuler ou de quels sont les comportements que je suis en droit d'attendre au niveau de mon applicatif ? Pour vous donner un exemple, généralement le data driven est accroché à des architectures de type CRUD : vous avez ce type d'ordre qui est envoyé ("je souhaite mettre à jour une adresse").
La problématique avec ce type d'ordre c'est qu'on a aucune idée de si l'adresse a été mise à jour parce que c'était la correction d'une erreur, ou si potentiellement la personne à déménagé. Or au niveau d'un système et au niveau business, ça n'a rien à voir.
Sur les systèmes CRUD, dans la plupart des cas vous perdez totalement l'intention utilisateur.
Et si on extrapole légèrement, quand vous faites du CRUD, la valeur ajoutée est généralement équivalente à celle d'une base Access, en un peu plus jolie et accessible par le web.
Donc on va discuter à partir d'un petit exemple : imaginez qu'on vous demande de faire un logiciel pour gérer des salles de sport.
Souvent quand les gens souhaitent démarrer on voit ce genre de questions.
On commence par faire un MCD pour décrire quelles sont les données, quelles sont leurs relations, où se trouvent les contraintes d'intégrité, les foreign keys etc. et on les associe à une série de contrôleurs, d'actions, de ressources. Et on mélange les deux.
Même si vous ne faites pas directement un MCD dans MySQL Workbench, c'est la même chose que des projets qui démarrent par la description des entités Doctrine.
Faire un descriptif des entités revient au même que faire un MCD - mais vous l'exprimez en termes de code.
J'ai tendance à le déconseiller parce qu'ici on ne prend pas du tout en compte la dimension comportementale, ni la dimension business de ce qui est demandé : on ne prend vraiment en compte que la dimension technique.
Généralement, vous irez assez vite au démarrage mais vous aurez rapidement une stagnation dans la productivité de l'équipe, parce que vous vous rendre compte que pour implémenter de nouveaux comportements, les données ne sont pas forcément les bonnes.
Donc on commence à tordre les trucs et on passe un temps dingue à se battre contre la technique.
L'idée en fait du domain driven c'est, au lieu de prendre l'aspect technique exprimé et faire rentrer plus ou moins bien le domaine à l'intérieur, de prendre la problématique inverse, se demander quelles sont les intentions business exprimées à travers mon système d'information, et de venir adapter la technique à cette expression.
Donc on démarre plutôt en listant des use cases (c'est-à-dire ce que le système doit être capable de faire) et des collaborateurs (ici, des des entités du business), qui possèdent des comportements.
Quand on démarre, on se fiche de savoir exactement quelles données vont se trouver dedans, on s''intéresse aux comportements qui peuvent être assignés au système.
Bien sûr, l'objectif n'est pas d'avoir une liste exhaustive avant de démarrer, mais juste d'avoir de quoi démarrer.
Ces architectures ont aussi l'objectif de pouvoir découvrir au fur et à mesure les besoins métiers, et donc accompagner de l'autre côté la découverte des besoins du marketing, des chefs de produits, etc.
Et, petit conseil qui fait gagner énormément de productivité sur ces questions : ne jamais écrire de code qui n'est pas directement liée à un use case.
Parce que si c'est le cas, vous êtes en train d'écrire du code "au cas où", du code qui ne répond pas à un objectif métier.
Et on voit ça très fréquemment, c'est ce qu'on appelle l'over-engineering et on finit avec ce genre de choses.
Et c'est très sympa d'avoir une navette spatiale, mais ça coûte très cher à maintenir, et vous n'en aviez probablement absolument pas besoin pour répondre à la problématique.
Donc on va démarrer en discutant de ces entités, qui sont en gros des objets classiques sur lesquels on vient appliquer des comportements.
On va catégoriser ces objets entre des entités qui, elles, ont un cycle de vie (elles naissent, elle vivent, elles meurent), et vous avez la plupart des objets que nous allons manipuler dans le système, par rapport à ce qu'on appelle des value objects qui ont une égalité basée sur leur contenu.
Parmi les plus classiques vous avez les dates (deux dates sont équivalentes même si ce n'est pas le même objet en tant que tel, même si elles n'ont pas la même identité), les questions d'argent (5 euros = 5 euros même si ce sont des objets qui sont différents), ou des choses un peu plus métier comme des BillingInterval, on pourrait imaginer qu'on facture la personne soit à l'année soit au mois, et ce type de durée n'a pas une identité propre.
Contrairement à une personne, où on a beau avoir le même prénom et le même nom, ça ne veut pas dire que nous sommes exactement la même personne, on a une identité propre.
Donc ça c'est ce qu'il faut retenir, j'en parlerai plus beaucoup mais c'était pour faire un rapide rappel.
Et tout ça, on va l'organiser dans ce qu'on appelle des agrégats.
Des agrégats c'est un ensemble d'objets qui vont collaborer ensemble pour représenter un concept métier.
Dedans vous avez soit des entités soit des value objects qui vont travailler, le tout coordonné par un objet racine qu'on appelle l'aggregate root.
Si j'en parle c'est parce que souvent, quand on voit des architectures qui se disent CQRS, il manque très très souvent cette modélisation. Et cette modélisation est très importante sur plusieurs points.
Le premier, c'est que là l'entité racine va protéger le reste de l’agrégat de potentiels problèmes d'intégrité.
Donc c'est un moyen simple de valider que votre système ne peut pas se retrouver dans un état complètement fucké.
C'est lié au fait que toutes les opérations qui peuvent être faites sur cet ensemble d'objets passent obligatoirement par cette racine.
Si jamais vous n'avez pas ce genre de choses, vous perdez un deuxième avantage énorme : normalement (sinon c'est que vous avez mal designé l'ensemble), tous ces objets ont un cycle de vie identique.
C'est-à-dire qu'ils vivent, évoluent et meurent en même temps.
Par exemple, si on parle de factures, vous pouvez avoir la facture comme racine et des lignes de facture.
On comprend bien qu'une ligne de factures n'a aucune raison d'exister sans sa facture correspondante.
Ici, un autre exemple sur la notion de membership : le fait que la personne puisse être abonnée à des activités, ou qu'elle ait un abonnement, est irrémédiablement lié avec le fait qu'elle soit membre du club de sport.
Ce sont des objets qui vivent ensemble et le gros avantage c'est qu'en terme de persistance, il n'y a aucune raison de s'amuser à faire des diffs là dedans, ce sont des choses qu'on va récupérer en un tout, et sauvegarder en un tout - c'est-à-dire qu'on éclate l'intégralité de la donnée, parce que la racine a garanti qu'elles étaient systématiquement valides, que l'ensemble de l'agrégat était systématiquement intègre.
C'est le principal avantage, et si vous n'avez pas designé ces agrégats dans votre système, vous ne pouvez pas bénéficier de cet avantage énorme que fournit ce type d'architecture au niveau de la simplicité de la persistance.
Et là on comprend bien que pour pouvoir faire ça, les entités ne sont pas des entités Doctrine, ce sont vraiment des objets PHP dédiés qui collaborent entre eux et exposent le comportement métier attendu de la part de ces différents concepts.
Je l'ai évoqué juste avant : tout ça est organisé dans ce qu'on appelle les use cases.
On va tout de suite les dissocier en deux grandes parties.
La première c'est les opérations de lecture, visualisation ; on va l'envoyer à l'utilisateur pour qu'il puisse prendre des décisions éclairées.
Et quand l'utilisateur a pris une décision, il va exprimer une intention, c'est ce qu'on appelle une commande.
A partir de maintenant je vais utiliser les termes de query et command, c'est-à-dire que c'est un cas d'usage pour le système.
A quoi ressemble une commande ? C'est un DTO extrêment simple qui porte une intention, et les données permettant de réaliser cette intention.
Pour information cette commande est souvent dans un namespace dédié, et elle a un handler associé, et ce handler va se charger d'orchestrer le métier associé.
Ici vous avez un handler correspondant à la méthode d'avant, on spécifie la commande associée (on verra pourquoi juste après), et vous avez aussi les collaborateurs qui lui sont passés par construction.
C'est un autre point important dans ce genre de design : ces collaborateurs sont systématiquement des interfaces.
C'est à dire que le handler n'a connaissance que d'interfaces, et au runtime ou boot time, on lui injecter une implémentation concrète qui vérifie cette interface. C'est ce qu'on appelle l'architecture hexagonale.
C'est ce qui va nous permettre d'avoir un découplage énorme sur notre application, parce que ce qui va verrouiller la maintenance d'une application c'est souvent les considérations d'ordre technique des collaborateurs techniques, et donc la possibilité de changer facilement ces collaborateurs techniques c'est une énorme garantie en termes de maintenabilité.
Enfin, le handler a une méthode handle, dans laquelle il effectue l'opération métier.
Là vous voyez un cycle traditionnel : créer un agrégat à partir des données qu'il a reçues de l'extérieur, le persister et renvoyer.Je ne renvoie pas la donnée correspondant à cet agrégat mais simplement son identifiant, et on verra après que c'est du côté de la lecture qu'on pourra récupérer tout ça.
A l'usage ça donne ça, donc c'est plutôt simple à utiliser.
Ça typiquement, c'est ce que l'on va retrouver dans un test unitaire.
Donc à tester, c'est 5-6 lignes et vous avez testé un cas d'utilisation business.
Donc vous testez déjà un niveau très intéressant parce que vous pouvez commencer à valider que ce que vous demandez à votre système est bien exécuté.
C'est assez sympa encore une fois, souvent le gros problème qu'on a c'est la persistance.
C'est ce dont on va parler maintenant en commençant par la notion de CQS.
Il manque le "R" parce que le CQS a la particularité de partager l'objet Repository (qui se charge des intéractions avec votre base de données) entre les deux côtés.
Et ce type de repository il faut le voir comme une interface qui expose des objets métiers au reste du domaine, et l'implémentation concrète est injectée au runtime et n'est jamais connue du code métier.
Et encore une fois vous n'êtes jamais sensé avoir d'indication sur l'ORM sous-jacent utilisé.
Si ce que je récupère en sortie d'un repository DDD me permet de connaître l'outil utilisé pour persister l'agréagat, généralement c'est très mauvais signe, ça veut dire que vous avez des capacités très techniques, en train de leaker vers votre domaine et donc vous avez du couplage très insidieux qui peut démarrer, tout simplement parce qu'un développeur ça reste quelqu'un (moi le premier) très flemmard, donc le jour où il y a une possibilité d'avoir un petit quick win en défonçant directement depuis le métier la couche ORM... on se fera un plaisir de le faire ! Un repository ressemble à ça : vous avez l'interface métier "Domain", il n'y a aucune indication technique sur comment ça va être réalisé, et vous avez ces entités du domaine qui sont systématiquement retournées, et très important : les identifiants sont maîtrisés.
Des identifiants qui viennent de la base de données ça veut dire que vous avez un couplage à votre persistance, donc le jour où vous voudrez faire évoluer les choses vous serez embêtés.
Si l'identifiant est connu du métier, il doit être maîtrisé par le métier.
C'est vraiment un principe de ces architectures : la frontière absolue entre les aspects métier et les aspects des services techniques.
Bon maintenant si on parle des queries, donc le côté lecture, on voit que c'est aussi des DTO.
C'est un des gros avantages de cette archi : on retrouve systématiquement les mêmes concepts partout.
La query est dans un namspace dédié et a aussi un handle qui expose la query à laquelle il répond, qui reçoit aussi ses collaborateurs via une interface, et qui au niveau du handle va tout retourner un vieux modèle à destination de l'utilisateur.
Donc le côté query (on est en train de faire du CQS), c'est une brique utilisant le même repository que celui utilisé lors de l'écriture.
Il y a par contre assez rapidement une problématique fréquente : on veut appliquer des comportements qui ne sont pas vraiment métier sur l'intégralité des commandes qui passent.
Typiquement : "je veux avoir un système de login ou un système pour checker mes performances, je veux pouvoir valider les payloads qui arrivent dans mon système, je veux un système d'authentification...
C'est pourquoi on construit très souvent un bus permettant de rajouter une couche d'abstraction entre la commande qui arrive en haut et moment où elle sera exécutée par le handler.
Donc par exemple vous pouvez avoir du login, la gestion des erreurs, etc.
Retenez par contre que les bus de query et de command sont séparés, et c'est très important parce que vous avez des comportements qui potentiellement n'ont rien à voir entre les deux.
Par exemple sur le côté query, vous pouvez avoir des systèmes de cache : comme ce n'est que de la lecture, ils peuvent être quasi-gratuits.
Souvent, côté lecture, on a une ferme de Redis branché en parallèle, et si la commande a déjà été traitée dans les n dernières minutes, on ne redescend pas au handler, on ne fait que renvoyer directement la réponse mise en cache.
Mais si vous vous amusez à faire ça sur le côté command, vous aurez des problèmes, c'est pour ça qu'on les a complètement dissociés.
Un middleware ressemble à ça : c'est aussi des objets assez simples à construire.
Le message vraiment c'est que ce sont des architectures très simples à mettre en œuvre.
Là je vous montre du quasi code de prod, il ne manque que quelques lignes pour que ce soit complètement prodready.
Un middleware reçoit l'opération suivante à la construction et peut recevoir des collaborateurs avec lesquels il travaille.
Là par exemple le middleware mesure le temps que prend l'exécution d'une commande.
Vous pouvez le faire de cette manière dans la méthode dispatch : vous pouvez exécuter du code avant, exécuter l'opération suivante qui in fine déclenche l'exécution du handler, vous pouvez récupérer derrière et exécuter du code une fois l'exécution faite, et enfin, retourner la réponse.
Ce sont des constructions permettant d'ajouter en 5-6 lignes des comportements parfois très pénibles à obtenir.
Le dispatcher c'est l'outil qui match une commande avec son handler, ça ressemble à ça.
On prend tous les handlers, on les enregistre (j'expliquerai après d'où je les récupère), et lorsque je reçois une commande je vais chercher le handler associé à la classe de la commande, - c'est pour ça qu'on avait besoin de la méthode listento() - et je vais exécuter le cœur du handler afin d'appeler le métier correspondant.
On compose tout ça dans le bus - ici vous avez une factory pour des services Symfony par exemple - et ça permet de récupérer dans notre système un endroit unique où invoquer l'ensemble de nos opérations de mutations.
Et alors pour récupérerer la liste de tous les handlers, vous pouvez utiliser le dependency injection container de Symfony, en demandant aux containers de mettre des tags sur toutes les classes qui implémentent une certaine interface, qui ici est l'interface command handler, et dans la configuration du service vous dites que le service de bus veut recevoir tous les machins taggés avec le tag défini précédemment.
Ça vous permet de ne plus se poser de question : vous ajoutez des use cases dans votre système et ça marche.
Et c'est quand même l'objectif ! A l'utilisation voilà ce que ça donnerait dans un controler des plus classiques : vous le demandez en paramètre, vous attendez que le système d'injection de dépendance travaille, il vous renvoie le bus, vous allez dans l'action correspondant à la root, et vous récupérez depuis la payload ce qui vous permet de construire l'intention que vous souhaitez propager, et vous la dispatchez vers le bus. Tout ce qu'on a vu avant va se déclencher, et à la fin vous récupérez le tout et vous renvoyez ça au client.
Ça permet de voir aussi la stickyness à un framework est extrêmement faible.
C'est le seul endroit où je commence à avoir du code spécifique à mon implémentation en termes de briques techniques.
Tout le reste c'est juste des plain PHP objects, et je peux utiliser ça dans n'importe quel contexte.
Donc si on reprend : cette commande arrive dans un bus, passe dans des dans des middlewares, puis est dispatchée vers son handler.
L'information remonte et on envoie cette information auprès du client.
Autre chose qu'on aime bien faire généralement c'est valider que tout ce qui se passe dans le handler fasse partie d'une même unité transactionnelle d'un point de vue logiciel.
C'est à dire que soit le handler réussi toute l'opération métier en 1 tout, soit il n'exécute absolument rien.
Cette architecture à base de middleware permet très facilement de garantir ça de manière systématique.
Voilà par exemple à quoi ça ressemblerait au niveau d'un middleware.
On démarre une transaction à l'aller, on exécute la suite dans un try catch : si tout marche, on flushe et on commite, s'il y a la moindre exception on rollback.
C'est finalement pas si compliqué et on s'enlève une énorme épine du pied avec cinq lignes.
Tout ça pour arriver aux événements du domaine.
Pour l'instant le problème c'est qu'on n'a pas fait grand chose : on s'est contenté de prendre une intention, exécuter des machins, la persister.
Très souvent dans les applications de ce type, on va avoir des besoins d'orchestration.
Et pour faire ça, en plus de répondre un simple acknowledgement ou non-aknowledgement en sortie d'un handler, on va rajouter une série d'événements qui décrivent la vérité de ce qui vient de se passer.
C'est très important d'avoir cette distinction en tête parce qu'on voit souvent cette erreur de mélange des genres entre la commande et l'événement.
La commande est une intention qu'on envoie au système et on espère que ça va bien se passer, l'événement est ce qui s'est réellement passé, donc on va pouvoir prendre des décisions business basées sur cet événement.
Autre souci : ne broadcastez pas ce type d'événements sur un messaging bus global.
On le voit très fréquemment et le problème c'est que vous couplez des détails d'implémentation, qui sont extrêmement locaux au niveau du système d'information, à tout le reste.
Et quand des gens se plaignent de problèmes de versionning d'événements, qu'ils ne peuvent plus changer la façon dont ils expriment tout ça, c'est parce qu'il y a ces événements extrêmement locaux broadcastés tels quels.
Mettez toujours une couche entre les deux pour faire une traduction entre des événements locaux et des événements globaux.
Un événement c'est aussi un DTO dans le namespace Domain.
Ces événements sont retournés par notre handler.
Là c'est avant notre commande response je lui met que l'identifiant et je rajoute un événement.
Pour information voilà à quoi ressemble la signature du WithValue : tout ce que j'ajoute va être mis dans un bag d'événement.
Ces événements ont un handler associé, c'est construit exactement de la même manière : les collaborateurs sont injectés, l'événement associé est décrit dans le listento, et la méthode handle vous permet d'exécuter ce que vous voulez faire au niveau de l'événement.
Petite note : un event handler est systématiquement void, c'est censé représenter un effet de bord, vous n'êtes pas censé tenir compte de la réponse d'un événement.
Ce qui ne veut pas dire par contre que vous devez ne pas tenir compte du succès ou non de son exécution, mais ça doit être traité en interne de l'event handler, ça ne doit pas venir perturber le fonctionnement du système.
Vous ne voulez pas qu'un système de paiement se vautre parce que votre passerelle de mails est tombée.
C'est à peu près l'idée qu'il y a derrière ce type de signature.
Ici le code permet d'envoyer un mail une fois qu'un membre a rejoint le club.
C'est typiquement le genre d'endroit où on va mettre tous ces petits effets de bord, des choses qui ne font pas partie du cas d'usage nominal, ça ne fait pas partie du métier d'ajouter le membre dans le club, ce sont des choses qui doivent se déclencher comme réaction à ça et ça permet vraiment de modéliser ce genre de problématique.
Si on reprend ce qui se passe : la commande rentre, elle est envoyée au handler, qui retourne notamment un événenement, qui va être dispatché, le dispatcher va invoquer l'event handler associé, et comme ce sont des événements, plusieurs handler peuvent être attachés au même événement, donc vous pouvez déclencher des réactions en chaîne.
Un dispatcher ressemble exactement à ce qu'on avait vu pour les commandes.
L'idée est de prendre ce qui vient d'arriver et de l'envoyer en dispatch via un middleware.
Les événements sont aussi envoyés dans un bus, ce qui permet aussi, sur la partie événementielle, d'empiler des middleware pour prendre en compte les allers retours.
Avec la différence que comme un handler peut avoir plusieurs peut avoir plusieurs événements, vous avez potentiellement plusieurs dispatch à déclencher de manière séquentielle.
L'event dispatcher reçoit les handler, ici encore vous pouvez utiliser l'injection via Symfony pour récupérer toute cette collection.
Généralement c'est ce qu'on fait : on utilise systématiquement les mêmes pattern, on les répète.
la seule différence c'est que dans le dispatch vous pouvez avoir plusieurs handler associés à un même événement.
C'est très important de l'avoir en tête puisque c'est ce qui va permettre de fournir ce que l'on appelle des systèmes de projection.
La projection c'est tenir compte du fait qu'un event handler, quel qu'il soit, peut s'autoriser à mettre à jour un datastore.
Donc en réaction à un événement on va pouvoir mettre à jour de la donnée.
Par exemple, si vous avez un programme de referral et vous avez besoin de checker qui a amené qui dans votre club de sport, de manière récursive, vous ne voulez pas le faire à la demande, le calculer une bonne fois pour toutes lors de l'inscription c'est parfait : vous le faites une seule fois, et vous enregistrez les résultats dans une table dédiée.
Donc des projecteurs vont préparer de la donnée et d'autres visualisations de cette donnée : c'est ce qu'on appelle des projections.
Par contre, bien garder en tête que la source de vérité reste unique, elle est portée par le repository qui travaille avec ses espaces de données, et vous ne devez surtout pas partager votre source de vérité entre des projections et les sources utilisées par le côté commandes.
Par contre ça vous permet de créer des tables de statistiques anonymisées par défaut (dans le contexte de la RGPD c'est très sympa), vous pouvez tout à fait aussi balancer ça vers d'autres bases de données, y compris une base de données legacy, ce qui permet d'avoir un système tout neuf capable d'alimenter un système legacy, sans que le système legacy ne se rende compte qu'il n'est plus utilisé.
Et vous pouvez aussi mettre à jour des data store qui n'ont strictement rien à voir : bases de données géographiques, de search, etc.
L' objectif c'est vraiment de préparer la donnée pour son usage futur, donc d'arrêter de faire de la recherche full text dans MySQL par exemple.
Et bien sûr l'objectif c'est de faire un mix de tout ça : par rapport à une opération, on peut construire autant de représentations qu'on veut.
Et ça nous permet d'arriver au CQRS : le R est réapparu.
En CQS, le repository est partagé mais en CQRS, le repository va partir de son côté, et rester pour le côté command, et le côté query il va être un peu en mode free for all.
Donc le repository va ressembler à ça : il n'y a plus aucune méthode de quering spécifique présente, et si on extrapole légèrement, ça a la forme de l'interface que l'on pourrait avoir pour un tableau associatif.
L'idée est vraiment que côté commande, la persistance soit vue comme quelque chose de très simple, comme si j'étais en train de travailler avec un tableau associatif pour sauvegarder ma donnée.
Voilà ce que ça peut donner : c'est une implémentation avec Doctrine (rien de très folichon).
Vous noterez que j'ai des mappers chargés de détacher complètement l'entité Doctrine de mon entité métier, ça généralement en PHP on peut le faire avec un trait, ça permet d'accéder à des données privées, et donc de faire une hydratation à la main très facilement.
Petite astuce (même si ce sera supprimé dans Doctrine 3 donc je ne sais pas comment il faudra faire) : utiliser l'opérateur merge lors de l'ajout de nouveau en persistance, et ça permet de remettre en manager l'entité dont Doctrine avait perdu la trace, puisqu'on l'avait décrochée de ce que nous avait retourné les unit of work.
Ça aussi, généralement, le map from entity et le map from Doctrine, c'est très souvent le même trait qui est injecté dans les deux classes, pour permettre cette transformation dans un sens et dans l'autre.
Voilà ce que ça donne : la query va être capable d'aller taper directement dans le data store, ce qui veut dire qu'on lui passe une connexion en direct, et que dans le corps on exécute la requête en direct.
Il n'y a aucune raison sur le côté query de se prendre la tête : on voit rapidement que même si on utilise un ORM, on écrit souvent, si on prend Doctrine, du son prendre doctrine, du DQL, au lieu d'utiliser les méthodes auto-magiques de l'ORM, quand on essaye de faire des requêtes sur des ensembles de données potentiellement complexes.
Donc sur ce côté query on va s'autoriser cet aspect d'aller récupérer tout ça en direct.
Et comme on a des représentations potentiellement différentes, la query récupérer la données où elle veut.
Si la query fait de la recherche, elle peut rechercher dans le moteur de recherche, si elle a besoin de statistiques, elle peut rechercher dans les tables de stats, si elle a besoin de parcourir des graphes, elle peut rechercher dans une base de données graphes.
Et comme c'est de la lecture, ça scale gratos ! Vous prenez les systèmes qui gèrent la query, vous les alignez, c'est bon, vous n'avez aucun soucis.
Il me reste moins de 5 min, on va très rapidement passer sur l'event sourcing.
Jusqu'à présent tout ce que je vous ai montré n'est pas de l'event sourcing. C'est important car beaucoup de gens associent CQRS et event sourcing, alors que même si l'event sourcing nécessite une approche CQRS, on peut tout à fait faire du CQRS sans event sourcing.
Maintenant nos agrégats retournent des événements : ils peuvent s'appliquer des événements sur eux-mêmes, et retourner cette application et l'événement.
L'event applier c'est un trait pour pouvoir manipuler l'objet au niveau de ces éléments privés, qui va pouvoir mettre à jour l'état de l'agrégat par rapport à un évènement donné.
Donc ça permet quelque chose d'assez sympa : le handler n'a plus besoin de persister explicitement.
Le système d'event dispatching va mettre ça au niveau de la DB, et derrière on a le repository, et là on a même plus de save qui se charge de récupérer les événements pour un agrégat donné et les réapplique un par un sur l'agrégat avant de le retourner.
Donc on récupère un agrégat dont l'état est le dernier connu par rapport aux événements qui ont été mis dans le système.
Vous voyez que les handlers sont devenus d'une simplicité désarmante ! Je récupère un agrégat, je travaille dessus, j'émets les événements que j'ai reçu et voilà ce que ça donne au global.
L' idée de l'event sourcing c'est basiquement de prendre les événements comme source de vérité, c'est-à-dire que là où avant on sauvegardait un état et on écrasait l'état précédent à chaque mise à jour, ici on garde tout ce qu'il s'est passé dans le système.
Ça permet vraiment de créer de nouvelles projections à la demande.
Quand le business se réveille un matin, et vous demande telle visualisation des données sur les cinq dernières années, vous n'avez aucun problème pour lui donner puisque vous avez gardé l'historique de tout l'événementiel du système.
Je vais terminer avec ça : ces styles d'architectures permettent de prendre beaucoup de liberté par rapport à l'outillage technique utilisé.
Comme on est sur des questions de persistance simplifiées au maximum et une relation avec les couches de transport très faible, on a vraiment l'opportunité de s'adapter au plus près à l'implémentation dans le logiciel des demandes métier.
Je vais m'arrêter là et je crois qu'il nous reste même deux minutes pour des questions. Merci Effectivement il reste une minute pour une question : qui sera l'heureux gagnant ? Bonjour. Super conférence ! Y a-t-il des best practices (au moins une) pour intégrer l'event sourcing à une architecture actuelle au fonctionnement normal ? Ce qui peut être fait c'est détecter des endroits où il y a de l'événementiel métier qui peut être généré.
Donc on regarde l'existant et on se branche sur des zones où on peut générer de l’événementiel, où on est sûr d'avoir des sources de vérité sur ce qu'il s'est passé, on utilise ça pour construire notre propre base d'événements, et à partir de cette base, on peut réutiliser toute la mécanique de projections etc.
C'est notamment ce qu'on utilise quand régulièrement quand on reprend des systèmes legacy : on cherche dans le système legacy des endroits où on peut mettre de l'événementiel, pour commencer à créer d'autres visualisations de cette donnée pour sortir de cette énorme problématique qui est que la façon dont les données sont organisées vont très souvent bloquer la possibilité de faire évoluer le système.
Merci Merci Arnaud
Commentaires