Il ne devrait pas être innovant de dire qu'écrire un logiciel ne consiste pas simplement à écrire du code - il s'agit de résoudre un problème particulier. Même si ce sont les développeurs qui implémentent finalement la solution, ce ne sont pas eux qui définissent le problème en premier lieu. Cette tâche est effectuée par divers hommes d'affaires, qui examinent les processus, les risques et les résultats pour décrire quel est le problème, pourquoi il existe et comment il doit être résolu. Dans un contexte axé sur le domaine, ces gens d'affaires sont appelés Experts du domaine .
Du point de vue de l'ingénierie, il apparaît que les experts du domaine détiennent un atout précieux : leur connaissance du domaine. Cependant, ces connaissances sont rarement partagées sous leur forme brute. Au lieu de cela, il est généralement traduit en exigences afin que les développeurs puissent le comprendre et l'implémenter. Le problème avec cette approche est que la connaissance du domaine des gens d'affaires et des développeurs peut diverger. Cela signifie que les points de vue de ceux qui définissent le problème et de ceux qui travaillent à le résoudre peuvent ne pas s'aligner, ce qui entraîne des malentendus et des conflits.
Alors, quelle est la sortie ? Assurez-vous que les commerciaux et les techniciens utilisent le même langage et la même terminologie.
La conception pilotée par le domaine (DDD) est une méthodologie qui met l'accent sur l'importance de créer une compréhension partagée entre les experts du domaine et les parties prenantes techniques et d'aligner la solution logicielle sur les exigences commerciales sous-jacentes. Cela semble être une définition de haut niveau et non technique, mais elle peut également être décomposée en quelque chose de plus convivial pour les développeurs :
DDD représente des concepts du monde réel dans le code et sa structure en cultivant et en utilisant un langage omniprésent, qui est construit en modélisant le domaine métier.
Il y a encore une terminologie à introduire, donc ce n'est peut-être pas clair à 100% pour le moment. Le plus important est que DDD fournit des outils et des activités qui permettent d'écrire et de structurer du code aligné sur la vision de l'entreprise. Il ne s'agit donc pas seulement de communiquer, mais aussi de prendre des décisions de conception qui façonnent réellement le langage commun.
Il n'est pas surprenant que le terme le plus important dans le monde DDD soit le domaine . Voici comment Vlad Khononov, auteur de "Learning Domain-Driven Design", le décrit :
Un domaine d'activité définit le principal domaine d'activité d'une entreprise.
Cela signifie que le domaine d'activité pourrait également être considéré comme :
Le domaine peut également être divisé en sous-domaines - des gammes d'activités plus spécifiques. Bien qu'il existe trois types de sous-domaines différents, le plus important est le core . Il décrit comment l'entreprise réalise l'avantage commercial. Les deux autres concernent des problèmes génériques plus courants, tels que les systèmes d'authentification ou les panneaux d'administration internes.
Avoir une compréhension approfondie du domaine d'activité d'une entreprise est absolument crucial pour tirer pleinement parti des avantages de la conception pilotée par domaine. La meilleure source de cette compréhension n'est autre que les experts du domaine . Ce sont les personnes dont le problème est résolu avec le logiciel - parties prenantes, divers hommes d'affaires et même utilisateurs. Cela ne veut pas dire que les ingénieurs ne sont pas informés du domaine sur lequel ils travaillent, mais plutôt que les experts sont la source de vérité de la connaissance du domaine. En travaillant avec les experts du domaine, les développeurs peuvent s'assurer que les modèles du domaine restent précis et à jour.
Cela conduit à un autre terme critique mais potentiellement ambigu : modèle . Eric Evans, dans son livre sur le DDD, décrit le modèle comme suit :
C'est une interprétation de la réalité qui fait abstraction des aspects pertinents pour résoudre le problème en question et ignore les détails superflus.
Vlad Khononov poursuit en expliquant cette idée en des termes plus pertinents :
Un modèle n'est pas une copie du monde réel, mais une construction humaine qui nous aide à donner un sens aux systèmes du monde réel.
En conclusion, un modèle est une représentation d'un concept ou d'un processus métier qui facilite la compréhension et la communication de la complexité sous-jacente du domaine.
Vlad a utilisé une carte pour illustrer efficacement le concept de modèle de domaine. Les cartes sont un exemple parfait de la façon dont elles affichent uniquement les informations pertinentes pour le type de carte, comme la topographie, les routes ou les frontières. Une carte qui affiche tous les détails à la fois serait écrasante et quasiment inutile. Les modèles de domaine peuvent également être trouvés sous d'autres formes, telles que :
La dernière pièce du puzzle terminologique Domain-Driven Design (DDD) est le langage ubiquitaire. Il fait référence au langage commun utilisé par les acteurs techniques et métier d'un projet. Avoir un langage commun pour décrire le domaine métier dérivé du modèle de domaine est crucial dans DDD. Cela permet de s'assurer que tous les membres de l'équipe ont une compréhension claire de l'espace du problème, de ses concepts et de leurs relations. Cela conduit à un meilleur alignement et réduit le risque de malentendus. En utilisant Ubiquitous Language, la solution logicielle peut refléter avec précision les exigences commerciales sous-jacentes, ce qui en fait un composant essentiel de DDD.
Avec la majeure partie de la terminologie couverte, il devrait être plus facile de comprendre ce qu'est la conception pilotée par le domaine . Il est maintenant temps de se plonger dans le véritable comment - les éléments constitutifs du DDD.
Les blocs de construction DDD servent de base à la création d'un modèle de domaine efficace et efficient . Vlad Khononov définit le modèle de domaine de la manière suivante :
Un modèle de domaine est un modèle objet du domaine qui intègre à la fois le comportement et les données.
Le modèle de domaine se compose de divers blocs de construction et structures. Les plus importants sont :
Les objets de valeur sont les blocs de construction les plus élémentaires disponibles. Ce sont des objets qui sont définis par un ensemble d'attributs et de valeurs. Ils n'ont pas d'identifiant unique - leurs valeurs définissent leur identité. Ils sont immuables dans le sens où différentes valeurs représentent déjà un objet de valeur différent. Voici des exemples d'objets de valeur :
Voici comment un simple objet de valeur pourrait être implémenté en Python :
from pydantic import BaseModel class Address(BaseModel): """Customer address.""" country: str city: str street: str house_number: str class Config: frozen = True
Cela signifie que l'utilisation de l'opérateur d'égalité ( ==
) pour comparer deux adresses renverra True
uniquement si les deux objets ont exactement les mêmes valeurs assignées.
Les entités sont le prochain type de bloc de construction. Les entités représentent des objets individuels dans le domaine avec une identité distincte, comme une personne ou une commande. Ils sont similaires aux objets de valeur dans la manière dont ils stockent également les données, mais leurs attributs peuvent changer et sont censés changer, et ils ont donc besoin d'un identifiant unique. Les commandes et les informations personnelles ne sont que deux exemples simples des Entités :
import uuid from pydantic import BaseModel, Field from practical_ddd.building_blocks.value_objects import Address class Person(BaseModel): """Personal data.""" id: uuid.UUID = Field(default_factory=uuid.uuid4) first_name: str last_name: str address: Address class Order(BaseModel): """Customer order.""" id: uuid.UUID = Field(default_factory=uuid.uuid4) description: str value: float
Comme les valeurs des instances sont modifiables dans les deux cas, elles nécessitent une identification, qui peut être un UUID. Ce qui est plus important, c'est que, dans la plupart des cas, les entités ne sont pas censées être gérées directement mais via un agrégat .
Un agrégat est un type d'entité car il est modifiable et nécessite un identifiant unique. Sa principale responsabilité, cependant, n'est pas de stocker des données mais de regrouper un ensemble d'objets liés (entités et objets de valeur) en une seule unité de cohérence. L'agrégat est l'objet racine, avec une frontière bien définie qui encapsule son état interne et applique des invariants pour assurer la cohérence de l'ensemble du groupe. Les agrégats permettent de raisonner sur le domaine de manière plus naturelle et intuitive en se concentrant sur les relations entre les objets plutôt que sur les objets eux-mêmes.
Dans la continuité des exemples précédents, un agrégat pourrait être représenté comme un client :
import uuid from pydantic import BaseModel, Field from practical_ddd.building_blocks.entities import Person, Order from practical_ddd.building_blocks.value_objects import Address class Customer(BaseModel): """Customer aggregate. Manages personal information as well as orders. """ id: uuid.UUID = Field(default_factory=uuid.uuid4) person: Person orders: list[Order] = Field(default_factory=list) def change_address(self, new_address: Address) -> None: self.person.address = new_address def add_order(self, order: Order) -> None: if self.total_value + order.value > 10000: raise ValueError("Order cannot have value higher than 10000") self.orders.append(order) def remove_order(self, order_id: uuid.UUID) -> None: order = next((order for order in self.orders if order.id == order_id), None) if order is None: raise IndexError("Order not found") self.orders.remove(order) @property def total_value(self) -> float: return sum(order.value for order in self.orders)
Le client est directement lié aux données personnelles, et il stocke toutes les commandes. En plus de cela, l'agrégat expose une interface pour gérer l'adresse de la personne ainsi que l'ajout et la suppression de commandes. Cela est dû au fait que l'état de l'agrégat ne peut être modifié qu'en exécutant les méthodes correspondantes.
Bien que l'exemple précédent soit relativement simple, avec une seule contrainte (la valeur de l'ordre ne peut pas être supérieure à 10 000), il doit démontrer l'utilisation des blocs de construction DDD et leurs relations. Dans les systèmes réels, les agrégats sont souvent plus complexes, avec plus de contraintes, de limites et éventuellement plus de relations. Après tout, leur existence même consiste à gérer cette complexité. De plus, dans le monde réel, les agrégats seraient généralement conservés dans un magasin de données, tel qu'une base de données. C'est là que le modèle de référentiel entre en jeu.
Les changements d'état de l'agrégat doivent tous être validés de manière transactionnelle en une seule opération atomique. Cependant, ce n'est pas la responsabilité de l'agrégat de "se maintenir". Le modèle de référentiel permet d'abstraire les détails du stockage et de la récupération des données et, à la place, de travailler avec des agrégats à un niveau d'abstraction plus élevé. En termes simples, un référentiel peut être considéré comme une couche entre l'agrégat et le stockage des données. Un fichier JSON est un exemple assez simple d'un tel magasin. L'agrégat client peut avoir un référentiel qui fonctionne sur des fichiers JSON :
import json import uuid from practical_ddd.building_blocks.aggregates import Customer class CustomerJSONRepository: """Customer repository operating on JSON files.""" def __init__(self, path: str) -> None: self.path = path def get(self, customer_id: uuid.UUID) -> Customer: with open(self.path, "r") as file: database = json.load(file) customer = database["customers"].get(str(customer_id)) if customer is None: raise IndexError("Customer not found") person = database["persons"][str(customer["person"])] orders = [database["orders"][order_id] for order_id in customer["orders"]] return Customer( id=customer["id"], person=person, orders=orders, ) def save(self, customer: Customer) -> None: with open(self.path, "r+") as file: database = json.load(file) # Save customer database["customers"][str(customer.id)] = { "id": customer.id, "person": customer.person.id, "orders": [o.id for o in customer.orders], } # Save person database["persons"][str(customer.person.id)] = customer.person.dict() # Save orders for order in customer.orders: database["orders"][str(order.id)] = order.dict() file.seek(0) json.dump(database, file, indent=4, default=str)
Bien sûr, cette classe pourrait (et devrait peut-être) faire beaucoup plus, mais elle n'est pas destinée à être un ORM parfait et multifonctionnel. Cela devrait donner une idée des responsabilités du référentiel, qui dans ce cas sont le stockage et la récupération de l'agrégat client dans le fichier JSON . Il convient également de noter comment le référentiel gère les entités associées à l'agrégat. Étant donné que les données personnelles et les commandes sont étroitement liées au cycle de vie du client, elles doivent être gérées précisément au moment du traitement de l'agrégat.
Un autre cas à considérer est lorsqu'il existe une logique métier qui ne rentre tout simplement pas dans l'agrégat ou dans l'une de ses entités ou objets de valeur. Il peut s'agir d'une logique qui dépend de plusieurs agrégats ou de l'état du magasin de données. Dans de tels cas, une structure connue sous le nom de service de domaine peut s'avérer utile. Le service de domaine doit pouvoir gérer les agrégats, par exemple, en utilisant le référentiel, puis il peut stocker la logique de domaine qui n'appartient pas à l'agrégat. Par exemple, un client peut avoir besoin de logique pour éviter de perdre trop de commandes :
import uuid from typing import Protocol from practical_ddd.building_blocks.aggregates import Customer class CustomerRepository(Protocol): """Customer repository interface.""" def get(self, customer_id: uuid.UUID) -> Customer: ... def save(self, customer: Customer) -> None: ... class CustomerService: """Customer service.""" def __init__(self, repository: CustomerRepository) -> None: self.repository = repository def get_customer(self, customer_id: uuid.UUID) -> Customer | None: try: return self.repository.get(customer_id) except IndexError: return None def save_customer(self, customer: Customer) -> None: existing_customer = self.get_customer(customer.id) # If customer is already in the database and has more than 2 orders, # he cannot end up with half of them after a single save. if ( existing_customer is not None and len(existing_customer.orders) > 2 and len(customer.orders) < (len(existing_customer.orders) / 2) ): raise ValueError( "Customer cannot lose more than half of his orders upon single save!" ) self.repository.save(customer)
Aggregate ne peut pas garantir en quoi son état diffère de celui du fichier JSON, car il n'a aucune connaissance du fichier JSON en premier lieu. C'est pourquoi la logique de comparaison doit être incluse dans le service de domaine. Il est également important de noter que le service de domaine doit fonctionner avec l'abstraction du référentiel. Cela simplifie le remplacement de l'implémentation concrète par une autre en utilisant l'injection de dépendances.
Toutes les pièces étant maintenant couvertes, elles peuvent maintenant être considérées comme un programme de travail :
import uuid from practical_ddd.building_blocks import aggregates, entities, value_objects from practical_ddd.database.repository import CustomerJSONRepository from practical_ddd.service import CustomerService # Initialize domain service with json repository srv = CustomerService(repository=CustomerJSONRepository("test.json")) # Create a new customer customer = aggregates.Customer( person=entities.Person( first_name="Peter", last_name="Tobias", address=value_objects.Address( country="Germany", city="Berlin", street="Postdamer Platz", house_number="2/3", ), ), ) srv.save_customer(customer) # Add orders to existing customer customer = srv.get_customer(uuid.UUID("a32dd73a-6c1b-4581-b1d3-2a1247320938")) assert customer is not None customer.add_order(entities.Order(description="Order 1", value=10)) customer.add_order(entities.Order(description="Order 2", value=210)) customer.add_order(entities.Order(description="Order 3", value=3210)) srv.save_customer(customer) # Remove orders from existing customer # If there are only 3 orders, it's gonna fail customer = srv.get_customer(uuid.UUID("a32dd73a-6c1b-4581-b1d3-2a1247320938")) assert customer is not None customer.remove_order(uuid.UUID("0f3c0a7f-67fd-4309-8ca2-d007ac003b69")) customer.remove_order(uuid.UUID("a4fd7648-4ea3-414a-a344-56082e00d2f9")) srv.save_customer(customer)
Tout a ses responsabilités et ses limites. Aggregate est chargé de gérer ses entités et ses objets de valeur, ainsi que de faire respecter ses contraintes. Le service de domaine utilise le référentiel JSON injecté pour conserver les données dans le fichier JSON et appliquer des limites de domaine supplémentaires. En fin de compte, chaque composant a une fonction et une signification distinctes dans le domaine spécifié.
Le Domain-Driven Design est, sans aucun doute, une idée complexe à appréhender. Il fournit des pratiques, des modèles et des outils pour aider les équipes logicielles à résoudre les problèmes métier les plus difficiles en mettant fortement l'accent sur le domaine métier. Cependant, DDD est plus qu'un simple ensemble de blocs de construction. C'est un état d'esprit qui nécessite une collaboration et une communication entre les acteurs techniques et métiers. Une compréhension partagée du domaine, exprimée à travers un langage omniprésent, est essentielle au succès d'un projet DDD. Lorsqu'il est bien fait, DDD peut conduire à des logiciels mieux alignés sur les besoins de l'entreprise et plus efficaces pour résoudre des problèmes complexes.
Cet article n'a jamais été destiné à ressembler à "DDD : de zéro à héros", mais plutôt à servir d'introduction à l'univers DDD. Je voulais démontrer les concepts les plus importants de Domain-Driven Design d'une manière très simple et pratique. Je pense que l'apprentissage de la conception pilotée par le domaine est un excellent moyen de renforcer l'expertise en programmation. Cependant, vous n'en entendez pas parler trop souvent - du moins pas autant que "11 trucs et astuces JavaScript INSANE - un fil 🧵".
Dans tous les cas, si vous avez trouvé l'un de ces éléments intéressants, vous pouvez consulter la section des sources pour les livres et les articles qui m'ont inspiré pour écrire cet article en premier lieu. Il y a certains concepts que je n'ai pas abordés parce que je pensais qu'ils sortaient du cadre de cette introduction, mais ils valent la peine d'être étudiés :
Vous les trouverez sans aucun doute dans les sources listées ci-dessous.
Des exemples de code utilisés dans l'article peuvent être trouvés ici : lien .
Apprendre la conception pilotée par le domaine par Vlad Khononov. Un livre incroyable qui a été pour moi une source d'inspiration majeure. Explique plus en détail tous les concepts abordés dans cet article.
Modèles d'architecture en Python par Harry Percival et Bob Gregory. J'ai lu le livre il y a presque deux ans, et il a eu un impact significatif sur moi en tant que développeur. J'y suis retourné en écrivant cet article, et ça m'a aidé une fois de plus.
DDD en Python par Przemysław Górecki. J'ai découvert ce blog vers la fin de la rédaction de l'article, mais il a piqué ma curiosité en raison de son professionnalisme incroyable. Fait amusant : je travaillais dans la même entreprise que Przemysław, et je l'ignorais complètement.
Également publié ici .