J'imagine que vous avez déjà entendu parler du pattern CQRS, de ses bienfaits, de son implémentation… Si vous ne le connaissez pas, ce n'est pas grave vous êtes au bon endroit pour le découvrir 😊. D'expérience, nous travaillons majoritairement sur des applications qui ont vécu, donc je vous propose de partir d’un code existant et le factoriser pour arriver à l’implémentation de ce pattern en douceur. Je précise que les exemples sont en C# mais ne vous inquiétez pas, ils sont faciles à lire, enfin je l'espère. :p
CQRS quésaco ?
CQRS (Command and Query Responsibility Segregation) est un pattern de séparation des opérations de lecture et d'écriture des informations. Il est présenté pour la première fois par [Greg Young](https://twitter.com/gregyoung Greg). Traditionnellement, les modèles utilisés à la fois pour la lecture et l'écriture ne correspondent pas forcement à tous les cas d'utilisation, notamment ceux de lectures récurrentes de grappes d'objets ou de lectures massives de données. Comme les besoins ne sont pas forcément les mêmes entre l’écriture (write model) et la lecture (read model) des données, CQRS propose alors la séparation des deux modèles et donc de modéliser différemment les données de l’écriture et les données de la lecture. On peut donc utiliser une base relationnelle par exemple pour l’écriture et un graphe comme modèle de lecture. Mais il faut toujours garder en tête que la base d’écriture est notre base de confiance ou de référence (appelée aussi « Golden source »). Deux termes reviennent souvent quand on parle de CQRS :
- Une commande (command) correspond à une action d'écriture ou de modification des données.
- Une requête (query) porte la demande de lecture. Elle retourne la donnée correspondante à ce qui est attendu sans superflu et elle ne doit jamais modifier les données.
Techniquement, les solutions de mise en place de ce pattern sont multiples. On peut imaginer l’utilisation d’une même base de données mais en dématérialisant le schéma relationnel en créant des vues, comme on peut imaginer une séparation physique des bases de données, voire l’utilisation, comme on l’a évoqué plus haut, de différents types de base de données. La « golden source » est la base de référence. Elle correspond à la base d’écriture et sera utilisée comme base de confiance au cas où une défaillance de synchronisation arrive lors de la transformation des données.
La démarche
Souvent, quand on présente ce pattern, on le présente sur un kata en mode « green field ». Ce mode consiste à développer le kata de zéro (« from scratch »), sans aucune base de code existante. Pour cet article, j’ai décidé de le présenter sous une autre forme, en mode « brown field ». Je vais donc partir d’une base de code existant que je vais refactorer pour arriver à l’implémentation du pattern. Vous trouverez l’implémentation sur mon github https://github.com/iAmDorra/BankKata_and_cqrs. Ma base de code est sécurisée avec des tests unitaires et des tests d’acceptance. Ces tests représentent mon harnais de sécurité en cas d’erreur de refactoring. Pour en savoir plus sur les tests je vous invite à lire mon article sur le sujet « Différents tests pour un développeur ». Pour simplifier la lecture des commits, j’ai créé une branche par étape d’implémentation comme décrit dans le schéma ci-dessous. Le tout est mergé dans master au fur et à mesure de l’avancement du développement.
Je veux aussi signaler que j’ai mis le focus sur l’implémentation du pattern CQRS, donc j’ai omis exprès beaucoup de recommandations du DDD (Domain Driven Design).
Bank Kata
Le principe du kata est simple. L’idée est de pouvoir suivre les transactions d’un compte bancaire. On peut donc déposer de l’argent (deposit), retirer de l’argent (withdraw) et voir l’état du compte à tout moment (balance statements). Pour simplifier l’utilisation de ce service, il vaut mieux que les transactions soient dans un ordre chronologique décroissant. La dernière transaction effectuée doit être donc affichée en premier avec l’état de la balance globale. Et comme un exemple est plus clair pour expliquer les choses, en voici un sous le format « gherkin » :
Given a client makes a deposit of 100 on 01/01/2020
And a deposit of 200 on 02/01/2020
And a withdraw of 50 on 03/01/2020
When he prints his account statement
Then he would see:
|Date|Amount|Balance|
|03/01/2020|50|250|
|02/01/2020|200|300|
|01/01/2020|100|100|
Proposition sans CQRS
La branche NoCQRS correspond comme son nom l’indique à une proposition d’implémentation sans CQRS. Dans cette proposition, j’ai créé un service (BankingService) qui permet de faire un dépôt (Deposit) et un retrait (Withdraw) et retourne la balance (PrintBalance). Il s’agit de trois méthodes différentes. Les deux méthodes Deposit et Withdraw permettent de rajouter des transactions de dépôt et de retrait. Ces transactions sont gardées en mémoire au niveau de « Transactions » qui n’est pas dans le domaine mais dans une couche que j’ai nommé « Application » puisque c’est une implémentation technique.
A chaque demande de l’état du compte, on récupère tout l’historique des transactions au format enregistré et on les parcourt pour recréer de nouveaux objets (statements) qui correspondent à notre besoin. Pourquoi ne sépare-t-on pas les deux concepts ?
Refactoring pour implémenter CQRS
Séparation de l’écriture et de la lecture
Le pattern CQRS nous invite à séparer la partie lecture de la partie écriture étant donné que les deux modèles sont différents. J’ai donc créé une nouvelle branche (SeperateReadWrite) partant du dernier commit de la branche NoCQRS. L’idée est donc d’avoir la classe « Transactions » qui permet de rajouter les transactions de dépôt et de retrait et une autre classe « BalanceRetriever » qui retourne l’état de la balance au moment de l’appel. La classe « Transactions » contiendra aussi notre golden source. Pour simplifier, je garde tout en mémoire, mais on peut imaginer une base SQL derrière par exemple. Il faut juste se dire que ce choix est un problème technique non prioritaire pour l’instant. La classe « BalanceRetriever », quant à elle, retourne le relevé du compte. Par contre avant de la créer directement, je vais d’abord préparer le terrain pour minimiser la taille du refactoring à faire. Dans un premier temps, j’extrais la méthode GetStatements dans la classe « BankingService » ensuite je la bouge dans « Transactions » pour enfin l’extraire dans la classe « BalanceRetriever ». Une fois ces étapes faites, je me retrouve avec une dépendance de « BalanceRetriever » à « Transactions ». A ce niveau, réellement, rien n’a changé dans le traitement. On recalcule le relevé à partir de tout l’historique. Et si j’utilise les événements pour envoyer l’information en temps réel au « BalanceRetriever » lors d’un ajout d’une nouvelle transaction. Comment pourrai-je faire ?
Utilisation des événements
J’ai créé une branche à part pour cette modification que j’ai nommé « UseEvents ». Ce n’est pas vraiment original comme nom de branche :p L’idée est d’ajouter un évènement dans « Transactions » et le lancer à chaque ajout. Et ensuite, il n’y a plus qu’à refactorer « BalanceRetriever » pour capturer cet évènement et ajouter les lignes du relevé au fur à mesure des ajouts. L’utilisation des évènements permet d’enlever la dépendance et de réduire le couplage.
Pour information, j’ai laissé la classe « TransactionEventArgs », qui encapsule la transaction ajoutée, au niveau du projet « Banking.Application » car je considère que c’est technique. D’ailleurs les contrats de mon domaine n’ont pas changé.
Problème réglé
A ce niveau, on peut très bien se satisfaire de la solution. Ci-dessous un schéma représentant la solution actuelle. D’un côté, à droite du schéma, on a la partie d’écriture (write model) avec la transaction à ajouter et le handler de l’ajout et à gauche la partie lecture (read model) avec le calcul de la balance qui se base sur l’événement d’ajout de transaction et le relevé du compte (List).
Piège à éviter
Le schéma correspond bien à ce qu’on veut implémenter dans le CQRS, à savoir la séparation de la partie lecture de celle de l’écriture. Par contre, on peut facilement tomber dans certains pièges. D’ailleurs si on regarde le code, on va se rendre compte qu’une des règles métier est de faire la somme des transactions pour avoir la balance. Mais quand on cherche où est implémentée cette règle, on se rend compte à ce moment là qu’on était tellement focus sur le refactoring pour obtenir une implémentation du pattern qu’on a déporté accidentellement les règles métiers en dehors du domaine. Avant de continuer l’amélioration de l’implémentation du pattern, il faut remonter la règle de gestion dans le domaine. Il faut faire attention à l’étape de transformation car ça peut correspondre à une règle métier et donc doit être placée dans le domaine, et c’est notre cas. Etant donné que le calcul correspond à additionner le montant de la transaction et la valeur de la balance actuelle, le traitement peut, très bien, être placé dans la classe « Transaction » puisqu’on va accéder à ses données internes. Pour le suivi des modifications, j’ai créé une branche correspondante à ce refactoring nommée « MoveCalculationToDomain ». Certes la modification est anodine mais il faut faire attention car c’est très important de déterminer et de garder les règles métier dans le domaine pour séparer les contextes et les responsabilités de chaque élément. Lors des lectures, le modèle d'écriture n'est pas solicité (reduit la contention), on évite des aggregations au moment de la lecture qui peuvent etre couteuses si la quantité de données est importante. On gagne en indépendance des modèles. ## Plus loin encore une fois le pattern implémenté, on se rend compte de la réduction de la contention lors de la lecture. En effet, on présente un modèle simple en évitant les agrégations qui peuvent être couteuses si les données sont volumineuses. Ainsi, on gagne en indépendance des modèles et chacun évolue à son rythme. Pour aller plus loin dans l’implémentation du pattern, on peut très bien imaginer un « service bus » dans lequel on envoie les messages correspondants aux transactions ajoutées. Donc le repository « Transactions » va créer le message et l’envoyer dans ce bus et « BalanceRetriever », qui est à l’écoute (le « listener ») de ce bus, va récuperer les messages au fur et à mesure pour recalculer la balance. Dans certains cas, on peut considérer l’écriture et la lecture comme deux domaines différents et donc mettre une ACL (« Anti-Corruption Layer ») entre les deux. Jérémie Chassaing en parle d’ailleurs dans un interview par ici.
Avant de finir
Je suis partie d’un exemple simple pour le factoriser et arriver à l’implémentation du pattern. Mon but est de montrer que le pattern est plutôt simple à implémenter. Mais même avec un exemple simple, on peut tomber dans des pièges. Et ces pièges sont multiples :
- Certain·e·s développeu·r·se·s trouvent ce pattern tellement intéressant qu’elles/ils confondent pattern et architecture et veulent l’implémenter pour l’ensemble de l’application. Gardons en tête que c’est un pattern donc on peut l’implémenter pour une fonctionnalité de l’application. 😉
- Pour une application type CRUD (« Create, Read, Update, Delete ») par exemple, l’implémentation de CQRS n’apporte que de la complexité inutile à l’application car on n’a pas forcément besoin d’optimiser le temps de la recherche qui est généralement unitaire (recherche d’une entité ou quelques lignes de données).
- Enfin, parfois vouloir implémenter le CQRS dans une architecture déjà complexe ou vouloir le combiner avec d’autres patterns comme l’« event-sourcing » peut rendre le pattern difficile à assimiler pour les membres de l’équipe et donc les évolutions peuvent devenir fastidieuses au fil du temps. Je vous conseille de ne l’implémenter que quand vous avez des performances médiocres au niveau de la lecture massives des données, autrement, vous risquez de rajouter de la complexité inutile qui peut engendrer des dettes techniques accidentelles par la suite.
- Il vaut mieux partir sur une solution alternative et ensuite la factoriser pour arriver à l’implémentation du CQRS que l’implémenter au démarrage du projet/fonctionnalité et se rendre compte que le code devient un château de cartes ; dès qu’on touche à une brique, on engendre plusieurs régressions.
Pour information
Si vous vous demandez comment j’ai réalisé le graphe des branches, j’ai utilisé le template de Bryan Braun par ici. Très pratique 😊
Conclusion
J’espère que ça devient plus clair pour vous. Si ce n’est pas le cas ou si vous n’êtes pas d’accord avec mes choix, je compte sur vous pour me contacter en mettant un commentaire ou via les réseaux sociaux (Twitter, LinkedIn) afin d’échanger et de partager nos points de vue. 😉
A bientôt,