Outils de conception pilotés par domaine

La baleine bleue est un excellent exemple de la façon dont la conception d'un projet complexe a mal tourné. La baleine ressemble à un poisson, mais c'est un mammifère: elle nourrit les petits avec du lait, elle a de la laine, et les os de l'avant-bras et des mains avec les doigts sont toujours conservés dans les nageoires, comme sur terre. Il vit dans les océans, mais ne peut pas respirer sous l'eau, il monte donc régulièrement à la surface pour avaler de l'air, même quand il dort. La baleine est le plus grand animal du monde, avec la longueur d'une maison de neuf étages, et pesant comme 75 voitures Volkswagen Touareg, mais pas un prédateur, mais se nourrit de plancton.

Lorsque les développeurs ont travaillé sur la baleine, ils n'ont pas commencé à tout écrire à partir de zéro, mais ont utilisé l'expérience d'anciens projets. Il semble être bricolé à partir de parties incompatibles du code qui n'ont pas été testées, et toute la conception s'est résumée à choisir un cadre et un «cycle» urgent déjà en production. En conséquence, le projet s'est avéré beau en apparence, mais avec des morceaux d'héritage dense et des béquilles sous le capot.



Pour créer des projets qui aident les entreprises à gagner de l'argent, plutôt que de ressembler à un animal marin qui ne peut pas respirer sous l'eau, il existe DDD. Il s'agit d'une approche qui ne se concentre pas sur les outils ou le code, mais sur l'étude du domaine, les processus métier individuels et le fonctionnement du code ou des outils pour la logique métier.

Qu'est-ce que DDD et quels outils sont là-dedans, nous le dirons dans un article basé sur le rapportArtyom Malyshev . L'approche DDD en Python, les outils, les pièges, la programmation des contrats et la conception des produits autour du problème à résoudre, plutôt que le cadre utilisé, sont tous sous la coupe.

Présentation complète du rapport .

Artem Malyshev (proofit404) - un développeur indépendant, écrit en Python pendant 5 ans, a activement aidé Django Channels 1.0. Plus tard, il s'est concentré sur les approches architecturales: il a étudié les outils qui manquent aux architectes Python et a commencé un projet de dry-python . Co-fondateur de Drylabs.

Complexité


Qu'est-ce que la programmation?
La programmation est une lutte constante avec la complexité que les développeurs eux-mêmes créent lorsqu'ils essaient de résoudre des problèmes.
La complexité est divisée en deux types: introduite et naturelle. L'introduit s'étend avec les langages de programmation, les frameworks, les OS, le modèle asynchrone. Il s'agit d'un défi technique qui ne s'applique pas aux entreprises. La complexité naturelle est cachée dans le produit et simplifie la vie des utilisateurs - pour cela, les gens paient de l'argent.
Les bons ingénieurs devraient réduire la complexité supplémentaire et augmenter le naturel pour augmenter l'utilité du produit.
Mais nous, les programmeurs, sommes des gens complexes et nous aimons ajouter de la complexité technique aux projets. Par exemple, nous ne nous sommes pas préoccupés des normes de codage, nous n'avons pas utilisé de linter, des pratiques de conception modulaire et nous avons reçu beaucoup de code de style dans les projets if c==1.

Comment travailler avec un tel code? Lisez beaucoup de fichiers, comprenez les variables, les conditions, et quand et comment tout cela fonctionnera. Ce code est difficile à garder à l'esprit - une complexité ajoutée absolument technique.

Un autre exemple de complexité supplémentaire est mon «enfer de rappel» préféré.



Lorsque nous écrivons dans le cadre de l'architecture orientée événement (EDA) et choisissons un cadre moderne pas si bon, nous obtenons un code dans lequel il n'est pas clair ce qui se passe et quand. Il est difficile de lire un tel code - c'est encore la complexité supplémentaire.

Les programmeurs adorent non seulement les difficultés techniques, mais argumentent également lequel est le meilleur:

  • AsyncIO ou Gevent;
  • PostgreSQL ou MongoDB;
  • Python ou Go;
  • Emacs ou Vim;
  • tabulations ou espaces;

La bonne réponse d'un bon programmeur à toutes ces questions: "Ça ne fait aucune différence!" Les bons développeurs ne discutent pas des chevaux sphériques dans le vide, mais résolvent les problèmes commerciaux et travaillent sur l'utilité du produit. Certains d'entre eux ont depuis longtemps établi un ensemble de pratiques qui réduisent la complexité introduite et vous aident à réfléchir davantage sur l'entreprise.

L'un d'eux est Eric Evans . En 2004, il a écrit le livre Domain Driven Design. Elle a «tiré» et a donné une impulsion à réfléchir davantage sur l'entreprise, et à pousser les détails techniques à l'arrière-plan.



Qu'est-ce que DDD?


D'abord une solution au problème, puis des outils . Tout d'abord, Evans a investi dans le concept de DDD, que ce n'est pas une technologie, mais une philosophie. En philosophie, vous devez d'abord réfléchir à la façon de résoudre le problème, puis seulement à l'aide de quels outils.

Travailler sur des modèles avec des experts en la matière et des développeurs de logiciels. Nous devons communiquer avec les gens d'affaires: chercher un langage commun, construire un modèle du monde dans lequel notre produit fonctionnera et résoudre les problèmes.

Écrire un logiciel qui exprime explicitement des modèles. La différence la plus importante entre DDD et une simple collaboration au sein d'une équipe est que nous devons écrire des logiciels dans le même style que nous parlons avec des experts du domaine. Toute la terminologie, les approches de la discussion et la prise de décision doivent être stockées dans le code source afin que même une personne non technique puisse comprendre ce qui s'y passe.

Parlez la même langue avec les entreprises . DDD est une philosophie sur la façon de parler le même langage avec des experts commerciaux dans un domaine spécifique et d'appliquer la terminologie à ce domaine. Nous avons une langue ou un dialecte commun dans le contexte lié, que nous considérons comme vrai. Nous créons des frontières autour des solutions architecturales.

DDD n'est pas une question de technologie.

D'abord, la partie technique, ensuite - DDD. Le sculpteur qui sculpte la statue dans la pierre ne lit pas le manuel sur la façon de tenir un marteau et un burin - il sait déjà travailler avec eux. Pour intégrer DDD dans votre projet, maîtrisez la partie technique: apprenez Django jusqu'au bout, lisez le tutoriel et arrêtez de vous demander si vous souhaitez utiliser PostgreSQL ou MongoDB.

La plupart des motifs de conception et des motifs sont des bruits techniques. La plupart des modèles que nous connaissons et utilisons sont techniques. Ils disent comment réutiliser le code, comment le structurer, mais ils ne disent pas comment l'utiliser pour les utilisateurs, les entreprises et modéliser le monde extérieur. Par conséquent, les usines ou les classes abstraites sont vaguement liées à DDD.

Le premier livre «bleu» est sorti il ​​y a près de 20 ans. Les gens ont essayé d'écrire dans ce style, ont marché un râteau et ont réalisé que la philosophie est bonne, mais en pratique incompréhensible. Par conséquent, un deuxième livre est apparu - "rouge", sur la façon dont les programmeurs pensent et écrivent en DDD.


Les livres «rouge» et «bleu» sont les piliers sur lesquels repose tout DDD.

Remarque. Les livres rouges et bleus sont une source unique d'informations sur DDD, mais ils sont lourds. Les livres ne sont pas faciles à lire: dans l'original en raison de la langue et des termes complexes, et en russe en raison d'une mauvaise traduction. Par conséquent, commencez à apprendre DDD avec un livre vert . Il s'agit d'une version simplifiée des deux premiers avec des exemples plus simples et des descriptions générales. Mais c'est mieux que si les livres rouges et bleus battaient votre désir d'étudier et d'appliquer le DDD. Il vaut mieux lire dans l'original.

Le livre rouge ignore l'idée de la meilleure façon d'intégrer DDD dans le projet, de structurer le travail autour de cette approche. Une nouvelle terminologie apparaît - «Model-Driven Design», dans laquelle notre modèle du monde extérieur est mis en premier lieu.



Le seul endroit où la technologie est choisie est Smart UI. Il s'agit d'une couche entre le monde extérieur, l'utilisateur et nous (une référence à Robert Martin et à son architecture propre avec des couches). Comme vous pouvez le voir, tout va au modèle.

Qu'est-ce qu'un modèle? C'est la douleur fantôme de tout architecte. Tout le monde pense que c'est UML, mais ce n'est pas le cas.
Un modèle est un ensemble de classes, de méthodes et de liens entre eux qui reflètent les scénarios métier du programme.
Le modèle reflète un objet réel avec toutes les propriétés et fonctions nécessaires. Il s'agit d'une boîte à outils de haut niveau pour prendre des décisions du point de vue des analyses de rentabilisation. Les méthodes et les classes, cependant, sont une boîte à outils de bas niveau pour les solutions architecturales.

Python sec


Pour combler le créneau du modèle, j'ai commencé un projet de python sec qui est devenu une collection de bibliothèque architecturale de haut niveau pour la construction de Model Driven Design. Chacune des bibliothèques essaie de fermer un cercle dans l'architecture et n'interfère pas avec l'autre. Les bibliothèques peuvent être utilisées séparément ou ensemble si vous en avez le goût.



La séquence du récit correspond à la chronologie de l'ajout optimal de DDD au projet - par couches. La première couche est les services , une description des scénarios (processus) commerciaux dans notre système. La bibliothèque Stories est responsable de cette couche.

Histoires


Les scénarios d'entreprise sont divisés en trois parties:

  • spécification - une description du processus opérationnel;
  • L'état dans lequel le scénario d'entreprise peut exister
  • mise en œuvre de chaque étape du script.

Ces pièces ne doivent pas être mélangées. La bibliothèque Stories sépare ces parties et trace une ligne claire entre elles.

Considérez l'introduction de DDD et Stories avec un exemple. Par exemple, nous avons un projet sur Django avec un méli-mélo de signaux Django et des modèles obscurs «épais». Ajoutez-y un package de services vide. En utilisant la bibliothèque Stories en plusieurs parties, nous réécrivons ce hachage dans un ensemble de scripts clair et compréhensible dans notre projet.

Spécification DSL. La bibliothèque vous permet d'écrire une spécification et fournit DSL pour cela. C'est une façon de décrire les actions des utilisateurs étape par étape. Par exemple, pour acheter subscription, je poursuis plusieurs étapes: je vais trouver une commande, vérifier la pertinence du prix, vérifier si l'utilisateur peut se le permettre. Il s'agit d'une description de haut niveau.

Contrat.En dessous de cette classe, nous rédigerons un contrat pour l'état du scénario commercial. Pour ce faire, nous désignons le domaine des variables qui surviennent dans le processus métier, et pour chaque variable, nous attribuons un ensemble de validateurs.

Dès que quelqu'un essaie d'affecter une variable à cette zone dans le cadre du processus métier, un ensemble de validateurs sera élaboré. Nous serons sûrs que l'état du processus à l'exécution fonctionne toujours. Sinon, il tombe douloureusement et crie fort à ce sujet.

Étape de mise en œuvre de chaque étape . Dans la même classe, subscriptionnous écrivons un ensemble de méthodes dont les noms correspondent aux étapes métier. Chaque méthode d'entrée reçoit un état avec lequel elle peut fonctionner, mais n'a pas le droit de le modifier. La méthode peut renvoyer un marqueur et un rapport:

  • , () ;
  • - , .


Il existe des marqueurs plus complexes: ils peuvent confirmer que l'État fonctionne, suggérer de supprimer ou de modifier certaines parties du processus métier. Vous pouvez également écrire en classe.

Lancez l'histoire. Comment exécuter Story lors de l'exécution? Il s'agit d'un objet métier qui fonctionne comme une méthode: on transfère des données à l'entrée, il les valide, interprète les étapes. L'histoire en cours d'exécution se souvient de l'historique d'exécution, enregistre l'état qui s'y est produit dans le processus métier et nous indique qui a influencé cet état .

Barre d'outils de débogage. Si nous écrivons dans Django et utilisons le panneau de débogage, nous pouvons voir quels scénarios commerciaux ont été traités dans chaque demande et leur statut.

Py.test. Si nous écrivons dans py.test, pour le test échoué, nous pouvons voir quels scripts métier ont été exécutés sur chaque ligne et ce qui n'a pas fonctionné. C'est pratique - au lieu de choisir le code, nous lisons la spécification et comprenons ce qui s'est passé.



Sentinelle. Encore mieux, lorsque nous obtenons l'erreur 500. Dans un système normal, nous le supportons et commençons à enquêter. Dans Sentry, un rapport détaillé apparaîtra sur ce que l'utilisateur a fait pour commettre l'erreur. Il est pratique et agréable quand à 3 heures du matin de telles informations ont été collectées pour vous.


ELK . Maintenant, nous travaillons activement sur un plugin qui écrit tout cela dans Elasticsearch dans la pile Kibana et construit des index compétents.



Par exemple, nous avons un contrat pour le statut d'un processus métier. Nous savons ce qu'il y a, par exemple,relation IDrapport. Au lieu de recherches archaïques sur ce qui s'est passé là-bas, nous écrivons une demande à Kibana. Il affichera toutes les histoires liées à un utilisateur spécifique. Ensuite, nous examinons l'état de nos processus commerciaux et de nos scénarios commerciaux. Nous n'écrivons pas une seule ligne de code de consignation, mais le projet est consigné au niveau même d'abstraction auquel nous sommes intéressés de regarder.

Mais je veux quelque chose de plus haut niveau, par exemple, des objets légers. Ces objets contiennent des structures de données et des méthodes alphabétisées qui se rapportent à l'adoption de décisions commerciales et non à l'utilisation de la base de données, par exemple. Par conséquent, nous passons à la partie suivante de l'architecture pilotée par les modèles - entités, agrégats et objets de valeur.



Entités, agrégats et objets de valeur


Comment tout cela est-il interconnecté? Par exemple, un utilisateur passe une commande de produit et nous facturons. Quelle est la racine de l'agrégation et qu'est-ce qu'un simple objet?



Tout ce qui est souligné est la racine de l'agrégation. C'est ce avec quoi je veux travailler directement: important, précieux, holistique.

Où commencer? Nous allons créer un package vide dans le projet, où nous placerons nos unités. Les agrégats sont mieux écrits avec quelque chose de déclaratif, comme dataclassesou attrs.

Classes de données . Si dataclassnous indiquons une sorte d' agrégat, nous écrirons une annotation dessus en utilisant NewType . Dans l'annotation, nous indiquons une référence explicite, qui est exprimée dans le système de types. S'il ne s'agit dataclassque d'une structure de données (entité), enregistrez-la dans l'agrégat.

Dans le contexte de Stories, seuls les agrégats peuvent mentir. L'accès à quelque chose qui y est intégré ne peut être obtenu que par des méthodes publiques et des règles de haut niveau. Cela vous permet de construire de manière logique et compétente un modèle sur lequel nous travaillons avec des experts du domaine. Il s'agit de la même langue unique .



Le problème se pose immédiatement - les référentiels. J'ai une base de données avec laquelle je travaille via Django, un microservice voisin auquel j'envoie des requêtes, il y a JSON et une instance du modèle Django. Pour recevoir des données et les transférer à la main, il suffit d'appeler ou de tester magnifiquement la méthode? Bien sûr que non. Dry-python possède une bibliothèque Mappers qui vous permet de mapper des abstractions de haut niveau et des agrégats de domaine aux endroits où nous les stockons.

Mappeurs


Nous ajoutons un package supplémentaire à notre projet - un référentiel dans lequel nous stockons le nôtre mappers. C'est ainsi que nous transférerons la logique métier de haut niveau dans le monde réel.

Par exemple, nous pouvons décrire comment nous en mappons un dataclasssur un modèle Django.

Django ORM. Nous comparons le modèle de commande avec la description de Django ORM - nous regardons les champs.

Par exemple, nous pouvons réécrire certains champs via la configuration facultative. Il se passera ce qui suit: mapperpendant la déclaration, il comparera la façon dont le dataclassmodèle est écrit . Par exemple, les annotations int( Order dataclassil y a un champ costavec annotation int) dans le modèle Django correspond integer fieldà l'option nullable="true". Ici , il va dataclassoffrir à ajouter optionalà dataclass, ou pour supprimernullablede field.

Grâce aux mappeurs, vous pouvez ajouter des fonctions qui lisent ou écrivent quelque chose. Les lecteurs sont des fonctions qui reçoivent un agrégat à l'entrée et lui renvoient une référence. Les écrivains font le contraire: les unités de rendement. Sous le capot, par exemple, il peut y avoir une demande à la base de données via Django.

Définitions Swagger Les mêmes opérations peuvent être effectuées avec des microservices. Vous pouvez y écrire une partie du schéma de swagger et vérifier dans quelle mesure le schéma de swagger d'un service particulier correspond à vos modèles de domaine. De plus, la demande retournée de la bibliothèque de demandes sera traduite de manière transparente en dataclass.

Requêtes GraphQL. GraphQL et microservices: le schéma de type d'interface GraphQL fonctionne bien contredataclass. Vous pouvez traduire des requêtes GraphQL spécifiques en structures de données internes.

Pourquoi s'embêter avec le modèle de données de haut niveau interne à l'application? Pour illustrer le «pourquoi», je vais raconter une histoire «divertissante».

Dans l'un de nos projets, les sockets Web ont fonctionné via le service Pusher. Nous n'avons pas pris la peine, nous l'avons enveloppé dans une interface pour ne pas appeler directement. Cette interface était liée dans toutes les histoires et était satisfaite.

Mais les exigences commerciales ont changé. Il s'est avéré que les garanties que Pusher fournit pour les sockets Web ne sont pas suffisantes. Par exemple, vous avez besoin de la remise garantie des messages et de l'historique des messages pendant les 2 dernières minutes. Par conséquent, nous avons décidé de passer au service Ably Realtime. Il a également une interface - nous écrirons un adaptateur et le lierons partout, tout ira bien. Pas vraiment.

Les abstractions utilisées par Pusher (arguments de fonction) sont piégées dans chaque objet métier. J'ai dû corriger environ 100 histoires et corriger la formation du canal utilisateur auquel nous envoyons quelque chose.

Retour aux tests.

Tests et simulations


Comment testez-vous généralement ce comportement avec des services externes? Nous mouillons quelque chose, nous regardons comment une bibliothèque tierce est appelée, et c'est tout - nous sommes sûrs que tout va bien. Mais lorsque la bibliothèque change, les formats d'argument changent également.

Vous pouvez économiser une semaine en réécrivant des milliers de tests et des centaines d'analyses de rentabilisation si vous testez le comportement du modèle interne différemment. Par exemple, quelque chose de similaire aux tests d'intégration: nous écrivons dans le flux utilisateur, et déjà à l'intérieur de l'adaptateur, Pusher ou Ably, nous traduisons ce flux dans le nom du canal normal afin de ne pas tout écrire dans la logique métier.

Dépendances


Dans une telle architecture de modèle, de nombreuses entités superflues apparaissent. Auparavant, nous avons pris une sorte de fonction Django et l'avons écrite: demande, réponse, mouvements corporels minimaux. Ici, vous devez initialiser les mappeurs, mettre des histoires et initialiser, traiter la ligne de demande de la demande HTTP, voir quelle réponse donner. Tout cela se traduit par 30-50 lignes de code appelant passe-partout Stories à l'intérieur de Django-view.

D'autre part, nous avons déjà écrit des interfaces et des mappeurs. Nous pouvons vérifier leur compatibilité avec une analyse de rentabilisation spécifique, par exemple, à l'aide de la bibliothèque de dépendances. Comment? Grâce au modèle d'injection de dépendance, tout est collé de manière déclarative sur un passe-partout minimal.

Ici, nous indiquons la tâche de prendre une classe dans le package de services, mettez troismappers, initialisez l'objet Stories et donnez-le nous. Avec cette approche, le nombre de passe-partout dans le code est considérablement réduit.

Refactoring de la carte


En utilisant tout ce dont j'ai parlé, nous avons développé un schéma par lequel nous avons réécrit un grand projet de signaux Django (implicite "callback hell") à Django en utilisant DDD.

La première étape sans DDD . Au début, nous n'avions pas DDD - nous avons écrit MVP. Quand ils ont gagné leur premier argent, ils ont invité des investisseurs et les ont convaincus de passer au DDD.

Histoires sans contrats. Nous avons divisé le projet en analyses de rentabilisation logiques sans contrats de données.

Contrats et agrégats . Ensuite, un par un, nous avons fait glisser le contrat de données pour chaque modèle qui peut être tracé dans notre architecture.

Mappeurs . Les mappeurs ont écrit pour se débarrasser des modèles d'entrepôt de données.

Injection de dépendance . Débarrassez-vous des motifs de collage.

Si votre projet a dépassé MVP et qu'il doit être changé d'urgence en architecture afin qu'il ne glisse pas dans l'héritage - regardez vers DDD.

legacy Python-, , , Moscow Python Conf++ 27 . Python . unconference, , , , Drylabs.

DDD Python, TechLead ConfIT-, DDD . 8 , Call for Papers 6 .

All Articles