L'architecture Puff est le salut dans le monde du développement d'entreprise. Avec son aide, vous pouvez décharger le fer, paralléliser les processus et restaurer l'ordre dans le code. Nous avons essayé d'utiliser le modèle CQRS lors du développement d'un projet d'entreprise. Tout est devenu plus logique et ... plus compliqué. Récemment, j'ai parlé de ce à quoi j'ai dû faire face lors de la réunion
Panda-Meetup C # .Net , et maintenant je partage avec vous.

Avez-vous déjà remarqué à quoi ressemble votre application d'entreprise? Pourquoi ne peut-il pas être comme Apple et Google? Oui, car nous manquons constamment de temps. Les exigences changent fréquemment, le terme pour leurs changements est généralement "hier". Et ce qui est le plus désagréable, l'entreprise n'aime vraiment pas les erreurs.

Afin de vivre avec cela, les développeurs ont commencé à diviser leurs applications en parties. Tout a commencé simplement - avec des données. Beaucoup connaissent le schéma lorsque les données sont séparées, le client est séparé, tandis que la logique est stockée au même endroit que les données.

Bon aperçu. Les plus grands SGBD ont des extensions procédurales entièrement fonctionnelles pour SQL. Il y a un proverbe sur Oracle: "Là où il y a Oracle, il y a de la logique." Il est difficile de discuter de la commodité et de la rapidité d'une telle configuration.
Mais nous avons une application d'entreprise, et il y a un problème: la logique est difficile à mettre à l'échelle. Et il est déraisonnable de charger la capacité du SGBD, qui souffre déjà de problèmes d'extraction et de mise à jour des données, ainsi que de tâches commerciales triviales.
Eh bien, les outils de programmation de logique métier intégrés au SGBD, pour être honnête, sont faibles pour créer des applications d'entreprise normales. Le maintien de la logique métier dans T-SQL / PL-SQL est un problème. Ce n'est pas sans raison que les langages OOP sont si répandus parmi les applications d'entreprise: C #, Java, vous n'avez pas besoin d'aller loin pour un exemple.

Cela semble une solution logique: nous mettons en évidence la logique métier. Elle vivra sur son serveur, la base seule, le client seul.
Que peut-on améliorer dans cette architecture à trois niveaux? L'architecture est impliquée dans la couche de la logique métier, je voudrais éviter cela. La logique métier ne veut rien savoir du stockage des données. L'interface utilisateur est également un monde distinct dans lequel il existe des entités qui ne sont pas spécifiques à la logique métier.
Augmenter les couches vous aidera. Cette solution semble presque parfaite, elle a une sorte de beauté intérieure.

Nous avons un DAL (Data Access Layer) - les données sont séparées de la logique, généralement un référentiel CRUD utilisant ORM, plus des procédures stockées pour les requêtes complexes. Cette option vous permet de développer assez rapidement et d'avoir des performances acceptables.
La logique métier peut faire partie des services ou être une couche distincte. L'interaction entre les couches peut être réalisée à travers des objets de transport (DTO).
La demande de l'interface utilisateur va au service, il communique avec la logique métier, monte dans le DAL pour accéder aux données. Cette approche est appelée N-tier et présente des avantages évidents.
Chaque couche a ses propres buts et objectifs évidents, que nous, en tant que programmeurs, aimons tant. Chaque couche de béton est engagée uniquement dans sa propre entreprise. Les services peuvent être mis à l'échelle horizontalement. L'approche est claire même pour un développeur novice, une personne comprend rapidement comment fonctionne le système. Il est très facile de tracer toutes les interactions au fur et à mesure que la demande va du début à la fin.
Autre cohérence: tous les sous-systèmes du projet fonctionnent avec les mêmes données, vous n'avez pas à vous soucier que nous ayons enregistré les données à un endroit et que l'utilisateur ne les voit pas dans une autre partie.
Gâteau de couche 1. N-Tier
Voici un exemple d'un fragment typique d'une application construite sur ces principes. Nous avons une exigence monétaire, ici j'ai examiné le modèle anémique. Et il existe un référentiel classique, dont le travail passe par ORM.

Il s'agit d'un service typique, ils sont aussi appelés managers. Il travaille avec le référentiel, reçoit les demandes et donne des réponses aux clients. Dans ce service, nous voyons une certaine confusion: nous avons un processus de traitement, un processus pour travailler avec l'interface utilisateur et un processus pour certaines unités de contrôle internes, elles sont faiblement interconnectées.
Voici à quoi ressemble une méthode typique de ce service. Par exemple, l'enregistrement d'une réclamation monétaire.

Nous recevons des données, effectuons des vérifications commerciales. Ensuite, il y a une mise à jour, puis quelques post-actions, par exemple, l'envoi d'une notification ou l'écriture dans le journal de l'utilisateur.
Dans cette approche, malgré toute sa beauté, il y a des problèmes. Très souvent dans les applications d'entreprise, la charge est asymétrique: les opérations de lecture sont un ordre ou deux de plus que les écritures. Il y a déjà un problème avec la mise à l'échelle de la base de données elle-même. Bien sûr, cela se fait, et même au moyen d'un SGBD à l'échelle d'une base de données, le partitionnement est appelé. Mais c'est difficile. Si cela est fait avec les mauvaises qualifications ou fait plus tôt que nécessaire, le partitionnement échouera.
Par exemple, dans l'un de nos systèmes, le volume de données a atteint 25 To, des problèmes sont apparus. Nous avons nous-mêmes essayé d'évoluer, invité des durs d'une entreprise bien connue. Ils ont regardé et ont dit: nous avons besoin de 14 heures d'arrêt complet de la base. Nous avons pensé et dit: les gars, ça ne marchera pas, l'entreprise ne l'acceptera pas.
En plus du volume de la base de données, le nombre de méthodes dans les services et les référentiels augmente. Par exemple, dans un service de réclamation monétaire, il existe plus d'une centaine de méthodes. Il est difficile à maintenir, il y a des conflits constants lors de la demande de fusion, la révision du code est plus difficile à réaliser. Et si vous tenez compte du fait que les processus sont différents, que différents groupes de développeurs y travaillent, alors la tâche de suivre tous les changements associés à un problème devient un vrai casse-tête.
Pâte feuilletée 2. CQRS
Alors que faire? Il y a une solution qui a été inventée dans la Rome antique: diviser pour régner.

Comme on dit, tout ce qui est nouveau est bien oublié. En 1988, Bertrand Meyer a formulé le principe de la programmation impérative CQS - Séparation commande-requête - pour travailler avec des objets. Toutes les méthodes sont clairement divisées en deux types. La première - Requête - requêtes qui renvoient le résultat sans changer l'état de l'objet. Autrement dit, lorsque vous examinez les exigences monétaires du client, personne ne devrait écrire dans la base de données que le client avait telle ou telle apparence, il ne devrait y avoir aucun effet secondaire dans la demande.
La seconde - Commandes - commandes qui modifient l'état d'un objet sans renvoyer de données. Autrement dit, vous avez commandé quelque chose à changer, et en retour, n'attendez pas un rapport de 10 000 lignes.

Ici, le modèle de données de lecture est clairement séparé du modèle d'écriture. La majeure partie de la logique métier fonctionne sur les opérations d'écriture. La lecture peut travailler sur des représentations matérialisées ou en général sur une base différente. Ils peuvent être divisés et synchronisés via des événements ou certains services internes. Il existe de nombreuses options.
Le CQRS n'est pas compliqué. Il faut bien distinguer les équipes qui changent l'état du système, mais ne retournent rien. Ici, l'approche peut être plus équilibrée. Ce n'est pas particulièrement effrayant si la commande retourne le résultat de l'exécution: une erreur ou, par exemple, l'identifiant de l'entité créée, alors il n'y a pas de crime là-dedans. Il est important que l'équipe ne travaille pas avec la demande, elle ne doit pas rechercher de données et renvoyer des entités commerciales.
Demandes - tout y est simple. Cela ne change pas la condition pour qu'il n'y ait pas d'effets secondaires. Cela signifie que si nous avons appelé la demande deux fois de suite et qu'il n'y avait pas d'autres commandes, l'état de l'objet dans les deux cas devrait rester identique. Cela vous permet de paralléliser les requêtes. Fait intéressant, un modèle distinct pour les requêtes n'est pas nécessaire pour le travail, car il est inutile de tirer la logique métier d'un modèle de domaine pour cela.
Notre projet CQRS
Voici ce que nous voulions faire dans notre projet:

Notre application existante fonctionne depuis 2006, elle a une architecture en couches classique. À l'ancienne mais toujours fonctionnel. Personne ne veut le changer et ne sait même pas par quoi le remplacer. Le moment est venu où il fallait développer quelque chose de nouveau, à partir de zéro pratiquement. En 2011-2012, Event Sourcing et CQRS étaient un sujet très à la mode. Nous avons pensé que c'était cool, afin de pouvoir stocker l'état d'origine de l'objet et les événements qui y ont conduit.
Autrement dit, nous ne mettons pas, pour ainsi dire, à jour l'objet. Il y a un état d'origine et à côté se trouve ce qui lui a été appliqué. Dans ce cas, il y a un énorme avantage - nous pouvons restaurer l'état d'un objet à tout moment de l'histoire. En fait, le magazine n'est plus nécessaire. Lorsque nous stockons des événements, nous comprenons ce qui s'est exactement passé. Autrement dit, ce n'est pas seulement que la valeur du client dans la cellule "adresse" a été mise à jour, nous aurons l'événement exact enregistré, par exemple, le déménagement du client.
Il est clair qu'un tel schéma fonctionne lentement lors de la réception de données, nous avons donc une base de données distincte avec des représentations matérielles pour la sélection. Eh bien, et la synchronisation des événements: à chaque arrivée d'événements sur un changement d'état, une publication se produit. En théorie, tout semble aller pour le mieux, mais ... Je n'ai jamais rencontré des gens qui l'ont pleinement réalisé en production, à des charges élevées avec une cohérence commerciale acceptable.

Le schéma peut être développé davantage si les gestionnaires et les commandes / requêtes sont séparés. Ici, à titre d'exemple, nous avons une équipe - une réclamation monétaire enregistrée: il y a une date, un montant, un client et d'autres champs.
Nous mettons une restriction sur le processeur d'enregistrement de la réclamation monétaire qu'il ne peut accepter que notre équipe (où TCommand: ICommand). Nous pouvons écrire des gestionnaires sans changer les anciens, simplement en ajoutant des exigences complexes. Par exemple, mettez d'abord à jour la date, puis notez la valeur, et ici vous envoyez une notification au client - tout cela est écrit dans différents gestionnaires par commande.
Comment causons-nous tout cela? Il y a un répartiteur qui sait où il a tous ces gestionnaires stockés.

Le répartiteur est transmis (par exemple, via un conteneur DI) à l'API. Et lorsque la commande arrive, elle ne s'exécute que. Il sait où se trouve le conteneur, où se trouvent les équipes et les exécute. Avec des demandes - de même.
Quel est le problème avec un tel schéma: toutes les interactions deviennent moins évidentes. Nous construisons une hiérarchie sur les types qui sont enregistrés dans des conteneurs, puis répondons à nos commandes / demandes. Cela nécessite une conception très claire de l'architecture. Toute action avec une méthode avec un paramètre n'est plus limitée. Vous écrivez une commande, écrivez un gestionnaire, vous vous inscrivez dans un conteneur. Le montant des frais généraux augmente. Dans un grand projet, des problèmes surviennent avec la navigation élémentaire. Nous avons décidé d'aller de manière plus classique.
Pour la communication asynchrone, le bus de service Rebus a été utilisé.

Pour des tâches simples, c'est plus que suffisant.
Le CQRS vous fait aborder le code un peu différemment, vous concentrer sur le processus, car toutes les actions naissent dans le cadre du processus. Nous avons alloué un référentiel pour les demandes, effectué séparément les commandes liées au traitement et traité séparément les demandes liées. Pour la lecture, nous n'avons pas utilisé de référentiel séparé, nous travaillons simplement avec ORM en équipe.

Voici, par exemple, une méthode à partir de laquelle tout ce qui est superflu est rejeté. Dans l'équipe d'enregistrement de la réclamation, nous enregistrons la demande et publions l'événement dans le bus où la réclamation est enregistrée.

Toute personne intéressée y réagira. Par exemple, l'authentification et la journalisation des utilisateurs y fonctionneront.
Voici un exemple de demande. Tout est devenu simple aussi: nous lisons et donnons au dépôt.

Je veux rester séparément chez Rebus.Saga. Il s'agit d'un modèle qui vous permet de diviser une transaction commerciale en actions atomiques. Cela vous permet de bloquer non pas tous à la fois, mais progressivement et tour à tour.

Le premier élément agit et envoie un message, le deuxième abonné y répond, le remplit, envoie son message, auquel la troisième partie du système répond déjà. Si tout s'est bien terminé, Saga génère son propre message du type spécifié, auquel les autres abonnés répondront déjà.
Voyons à quoi ressemble la classe de traitement d'une demande de remboursement dans ce cas. Tout est clair: il y a des commandes, il y a des requêtes qui concernent le processus d'enregistrement, enfin, un bus avec des logs.

Dans ce cas, il y a un gestionnaire. Lorsqu'un événement se produit et qu'une équipe arrive pour enregistrer des réclamations monétaires, il y répond. A l'intérieur, tout est le même qu'avant, mais la particularité est qu'il y a un regroupement par processus.

Pour cette raison, cela est devenu un peu plus facile, moins de changements dans chaque fichier.
Conclusions
De quoi devez-vous vous souvenir lorsque vous travaillez avec le CQRS? Vous avez besoin d'une meilleure approche de la conception, car la réécriture du processus est un peu plus compliquée. Il y a une petite surcharge, un peu plus de classes sont devenues, mais ce n'est pas critique. Le code est devenu moins connecté, cependant, ce n'est pas tant à cause du CQRS, mais à cause de la transition vers le bus. Mais c'est le CQRS qui nous a incités à utiliser cette interaction événementielle. Le code est devenu plus souvent ajouté que modifié. Il y a plus de classes, mais elles sont maintenant plus spécialisées.
Est-ce que tout le monde doit tout abandonner et passer massivement au CQRS? Non, vous devez voir quel scénario fonctionne le mieux pour un projet particulier. Par exemple, si votre sous-système fonctionne avec des répertoires, le CQRS n'est pas nécessaire, l'approche classique en couches donne un résultat plus simple et plus pratique.
La version complète des performances de Panda Meetup est disponible ci-dessous.
Si vous souhaitez approfondir le sujet, il est logique d'étudier ces ressources:
Style d'architecture CQRS - de MicrosoftLe blog d'Alexander BendyuExemples d'université Contoso avec CQRS, MediatR, AutoMapper et plus - par Jimmy BogardCQRS - par Martin FowlerRebus