paint-brush
Introduction pratique à la conception pilotée par domainepar@tobi
1,435 lectures
1,435 lectures

Introduction pratique à la conception pilotée par domaine

par Piotr Tobiasz15m2023/07/22
Read on Terminal Reader

Trop long; Pour lire

Terminologie DDD, blocs de construction et autres concepts pour commencer.
featured image - Introduction pratique à la conception pilotée par domaine
Piotr Tobiasz HackerNoon profile picture
0-item
1-item

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.

Qu'est-ce que la conception pilotée par domaine ?

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.

Terminologie

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 :


  • La principale source de revenus d'une entreprise,
  • Ce pour quoi l'entreprise est surtout connue,
  • Tout ce que l'entreprise fait mieux que ses concurrents.


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 :


  • Les commandes clients, qui représentent la version simplifiée de tous les processus se déroulant en arrière-plan,
  • Menus de restaurant, où les éléments répertoriés sur le menu sont les produits finaux, au lieu d'énumérer chaque ingrédient et chaque étape du processus de préparation,
  • Les réservations de voyage, où le voyage réservé ne met en évidence que les détails les plus critiques, même si beaucoup plus va dans la planification du voyage, de l'hôtel, etc.


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.

Blocs de construction

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 :


  • Objets de valeur,
  • Entités,
  • Agrégats,
  • Evénements de domaine,
  • Référentiels,
  • Services de domaine.

Objets de valeur

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 :


  • Montant monétaire,
  • Plage de dates,
  • Adresse postale.


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.

Entités

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 .

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.

Dépôt

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.

Service de domaine

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.

Mettre tous ensemble

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é.

Résumé

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.

Postface et prochaines étapes

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 :


  • Contextes délimités
  • Événements de domaine
  • Prise d'assaut d'événements


Vous les trouverez sans aucun doute dans les sources listées ci-dessous.

Sources

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 .