logo le blog invivoo blanc

Domain Driven Design Part 5 – Les avantages

30 mai 2023 | Python | 0 comments

Nous avons pu voir les outils et les principes du Domain Driven Design dans le premier et le deuxième article. Tandis que dans le troisième et le quatrième, nous avons poursuivi les épreuves de l’Odyssée de la conception pilotée par le domaine, en les mettant en pratique avec Python. Maintenant, nous allons examiner les avantages et les défis par des exemples codés en Python.

1. Les avantages de la conception pilotée par le domaine

La conception pilotée par le domaine (DDD) est une méthodologie de développement de logiciels qui met l’accent sur l’importance de la modélisation du domaine du problème dans la conception de logiciels. L’utilisation de DDD présente plusieurs avantages.

1.1 Amélioration de la communication

En utilisant un langage ubiquitaire, la conception pilotée par le domaine (DDD) contribue à garantir que toutes les personnes impliquées dans le processus de développement logiciel ont une compréhension commune du domaine métier. Cela permet d’améliorer la communication et la collaboration entre les développeurs, les experts du domaine et les autres parties prenantes, ce qui conduit à une meilleure compréhension des besoins et des exigences du métier.

# Define the terms used in the domain
from typing import List

class Customer:
    def __init__(self, name : str, email : str):
        self.name = name
        self.email = email

class OrderItem:
    def __init__(self, name : str, price : float):
        self.name = name
        self.price = price

class Order:
    def __init__(self, customer : Customer, items : List[OrderItem]):
        self.customer = customer
        self.items = items

# Use the terms in a conversation between team members
def place_order(customer_name : str, customer_email : str, item_names : List[str]):
    customer = Customer(customer_name, customer_email)
    items = [OrderItem(name, .0) for name in item_names]
    order = Order(customer, items)
    
    # Do something with the order
    ...

En définissant ces termes et en les utilisant de manière cohérente dans la base de code et dans les conversations, l’équipe peut s’assurer que tout le monde partage une connaissance commune du domaine métier.

1.2 Meilleur alignement avec le métier

La conception pilotée par le domaine (DDD) se focalise sur le domaine du métier et crée un modèle de domaine qui le représente avec précision. Cela conduit à des systèmes logiciels mieux alignés sur les besoins du métier, ce qui les rend plus efficaces et efficients.

Voici un exemple de la façon dont cela pourrait conduire à livrer un système plus efficace et responsif :

# Define the domain model 
from typing import List

class Customer:
    def __init__(self, name : str, email : str):
        self.name = name
        self.email = email

class OrderItem:
    def __init__(self, name : str, price : float):
        self.name = name
        self.price = price

class Order:
    def __init__(self, customer : Customer, items : List[OrderItem]):
        self.customer = customer
        self.items = items
        self.status = 'new'

    def mark_as_paid(self):
        self.status = 'paid'

# Use the domain model to implement a business rule
class OrderService:
def process_order(order : Order):
    if order.status == 'new':
        total_price = sum(item.price for item in order.items)
        if total_price > 100:
            order.mark_as_paid()
            
    # Do something with the order
    ...

Dans cet exemple, le modèle de domaine représente avec précision le domaine métier (clients, commandes et articles) et une règle métier est implémentée à l’aide du modèle de domaine. Cela conduit à un système mieux aligné sur les besoins du métier et peut être plus efficace et efficient dans le traitement des commandes.

1.3 Amélioration de la maintenabilité

En créant un modèle de domaine bien structuré et bien conçu, la conception pilotée par le domaine (DDD) facilite la maintenance et la modification du système logiciel. Les modifications peuvent être apportées avec une meilleure compréhension de leur impact sur le système, ce qui entraîne moins de bogues et de meilleures performances globales.

Utilisons un modèle de domaine bien structuré pour apporter des modifications au système.

# Define the domain model 
from typing import List

class Customer:
    def __init__(self, name : str, email : str, address : str):
        self.name = name
        self.email = email
        self.address = address
        
    def change_address(self, new_address : str):
        self.address = new_address

class OrderItem:
    def __init__(self, name : str, price : float):
        self.name = name
        self.price = price
        

class Order:
    def __init__(self, customer : Customer, items : List[OrderItem]):
        self.customer = customer
        self.items = items
        
    def add_item(self, item : OrderItem):
        self.items.append(item)
        
    def remove_item(self, item : OrderItem):
        self.items.remove(item)

Dans cet exemple, les entités Customer et Order sont clairement définies et séparées, ce qui facilite la compréhension et la maintenance du code. La classe client a une méthode pour changer l’adresse du client, ce qui est une opération importante dans ce contexte. La classe de Order a des méthodes pour ajouter et supprimer des éléments, qui sont des opérations importantes pour la gestion des commandes.

1.4 Développement plus efficace

La conception pilotée par le domaine (DDD) met l’accent sur une approche itérative et collaborative du développement logiciel, avec des boucles de rétroaction et des tests fréquents [9]. Cela conduit à un développement plus efficace et à une mise sur le marché plus rapide du système logiciel.

Voici un exemple pour la conception collaborative et le développement itératif :

class OrderService:
    def __init__(self, order_repository: OrderRepository):
        self.order_repository = order_repository

    def place_order(self, customer_id: str, items: List[OrderItem]):
        customer = self.customer_repository.get_by_id(customer_id)
        order = Order(items=items, customer=customer)
        self.order_repository.add(order)

Dans cet exemple, nous avons une classe OrderService qui prend un objet OrderRepository comme dépendance. La classe OrderService a une méthode place_order() qui prend un customer_id et une liste d’objets Item.

Lorsque la méthode place_order() est appelée, elle utilise le customer_id pour récupérer l’objet Customer pertinent du CustomerRepository. Il crée ensuite un nouvel objet Order avec les éléments donnés et l’objet Customer récupéré. Enfin, il ajoute le nouvel objet Order au OrderRepository.

Cette approche nous permet d’itérer rapidement et d’obtenir les commentaires des parties prenantes au fur et à mesure que nous développons le système. Cela facilite également les tests, car nous pouvons facilement nous moquer des dépendances de la classe OrderService et les tester de manière isolée.

1.5 Évolutivité

En utilisant des contextes bornés et des racines agrégées, la conception pilotée par le domaine (DDD) facilite la mise à l’échelle des systèmes logiciels pour répondre aux besoins du métier. Les systèmes peuvent être divisés en parties plus petites et plus faciles à gérer, ce qui facilite l’ajout de nouvelles fonctionnalités et capacités à au fur et à mesure que le métier se développe petit à petit.

Voici un exemple de la manière dont les contextes bornés peuvent être utilisés pour créer un système évolutif :

# Order bounded context
class Order:
    def __init__(self, order_id : str, customer_id : str, order_items: List[OrderItem]):
        self.order_id = order_id
        self.customer_id = customer_id
        self.order_items = order_items

# Customer bounded context
class Customer:
    def __init__(self, customer_id : str, name : str, address : str):
        self.customer_id = customer_id
        self.name = name
        self.address = address

# Shipping bounded context
class Shipment:
    def __init__(self, shipment_id : str, order_id : str, destination_address : str):
        self.shipment_id = shipment_id
        self.order_id = order_id
        self.destination_address = destination_address

Dans cet exemple, nous avons trois contextes bornés : Order, Customer et Shipment, dont chacun a son propre modèle de domaine. En divisant le système en parties plus petites et plus faciles à gérer, nous pouvons plus facilement faire évoluer le système à mesure que le métier évolue.

2. Les défis de la conception pilotée par le domaine

Bien que la conception pilotée par le domaine présente de nombreux avantages, elle donne lieu également à certains défis.

2.1 Courbe d’apprentissage

La conception pilotée par le domaine (DDD) nécessite un investissement important dans l’apprentissage et la compréhension des principes et des pratiques de la méthodologie. Cela peut être difficile pour les développeurs qui sont habitués à des approches de développement de logiciels plus traditionnelles.

Ce défi est plus lié au processus d’apprentissage et de compréhension de DDD jusqu’à l’implémentation dans le code.

Alors, voici un exemple simple en Python qui montre comment DDD nécessite une façon différente de penser le développement logiciel par rapport aux approches traditionnelles : disons que nous construisons une simple application de commerce électronique où les utilisateurs peuvent acheter des produits. Dans le développement de logiciels traditionnels, nous pourrions commencer par créer un schéma de base de données avec des tables pour les utilisateurs, les produits, les commandes, etc. Nous créerions ensuite du code pour interagir avec ces tables, comme l’insertion de nouveaux utilisateurs, la recherche de produits, etc.

Dans DDD, nous commençons par comprendre le domaine métier et par créer un modèle de domaine qui le représente avec précision. Nous pouvons identifier des concepts tels que les utilisateurs, les produits, les commandes et les paiements, et les définir comme des entités, des objets de valeur ou des agrégats. On se concentre ensuite sur la définition du comportement de ces entités, comme la création d’une nouvelle commande, l’ajout d’articles à une commande ou le traitement d’un paiement.

Voici un exemple de code Python simple qui illustre cette approche :

from typing import List

class Customer:
    def __init__(self, customer_id: int, name: str):
        self. customer_id = customer_id
        self.name = name

class Product:
    def __init__(self, product_id: int, name: str, price: float):
        self.product_id = product_id
        self.name = name
        self.price = price

class OrderItem:
    def __init__(self, product: Product, quantity: int):
        self.product = product
        self.quantity = quantity

class Order:
    def __init__(self, customer: Customer):
        self.customer = customer
        self.items = []

    def add_item(self, item: OrderItem):
        self.items.append(item)

    def total_price(self) -> float:
        return sum(item.product.price * item.quantity for item in self.items)

class Payment:
    def __init__(self, amount: float):
        self.amount = amount

def main():
    # create some example data
    customer = Customer(1, "Alice")
    products = [
        Product(1, "Book", 10.0),
        Product(2, "Toy", 5.0),
    ]

    # create a new order
    order = Order(customer)
    order.add_item(OrderItem(products[0], 2))
    order.add_item(OrderItem(products[1], 3))

    # process the payment
    payment = Payment(order.total_price())

    # do something with the order and payment, such as saving them to a database or sending them to a payment processor
    # ...

Dans cet exemple, nous commençons par définir les entités principales de notre domaine de commerce électronique : Customer, Product, OrderItem, Order et Payment. Nous définissons ensuite le comportement de ces entités en fonction de leurs méthodes, telles que l’ajout d’articles à une commande ou le calcul du prix total d’une commande.

Du coup, au lieu de penser de la façon traditionnelle, où nous pourrions commencer avec un schéma de base de données et écrire du code pour interagir avec lui, nous nous concentrons sur la création d’un modèle de domaine qui représente avec précision le domaine métier, puis définissons le comportement des entités en termes de leurs méthodes.

2.2 Collaboration

La conception pilotée par le domaine (DDD) souligne la collaboration entre les développeurs et les experts du domaine. Cela peut être difficile, car les experts du domaine peuvent ne pas avoir une compréhension approfondie du développement de logiciels et les développeurs peuvent ne pas avoir une compréhension approfondie du domaine métier.

Voici un exemple de collaboration entre développeurs et experts du domaine en DDD :

class OrderItem:
    def __init__(self, product_id : str, quantity : float):
        self.product_id = product_id
        self.quantity = quantity

class Order:
    def __init__(self, order_id : str, customer_id : str, order_items: List[OrderItem]):
        self.order_id = order_id
        self.customer_id = customer_id
        self.order_items = order_ items

Imaginez un expert du domaine qui souhaite ajouter une nouvelle fonctionnalité à la classe Order et suggère le changement suivant :

class Order:
    def __init__(self, order_id : str, customer  : Customer, order_items: List[OrderItem]):
        self.order_id = order_id
        self.customer = customer
        self.order_items = order_items

Le développeur peut être préoccupé à propos de ce changement, car il viole le principe d’encapsulation et peut conduire à un couplage entre les classes Order et Customer. Cela peut conduire à d’autres discussions et collaborations entre le développeur et l’expert du domaine.

2.3 Complexité

La conception pilotée par le domaine (DDD) peut conduire à des systèmes logiciels plus complexes, car le modèle de domaine peut devenir assez volumineux et complexe. Cela peut compliquer la maintenance et la modification du système au fil du temps.

Voici un exemple de modèle de domaine complexe en DDD :

class Order:
    def __init__(self, order_id : str, customer : str, order_items: List[OrderItem]):
        self.order_id = order_id
        self.customer = customer
        self.order_items = order_items

class Customer:
    def __init__(self, customer_id : str, name : str, address : str, orders : List[Order]):
        self.customer_id = customer_id
        self.name = name
        self.address = address
        self.orders = orders

class OrderItem:
    def __init__(self, product_id : str, quantity : int, price : float):
        self.product_id = product_id
        self.quantity = quantity
        self.price = price

Imaginez que le métier ajoute une nouvelle exigence au système qui nécessite le calcul de la valeur totale de toutes les commandes pour un client donné. Cela peut nécessiter des modifications importantes du modèle de domaine et peut entraîner un système global plus complexe.

2.4 Performances

La conception pilotée par le domaine (DDD) peut entraîner des problèmes de performances, s’il n’est pas correctement implémenté. L’utilisation d’événements de domaine et d’autres techniques peut entraîner une surcharge plus élevée, ce qui peut avoir un impact sur les performances globales du système.

Voici un exemple qui illustre l’impact potentiel sur les performances de l’utilisation d’événements de domaine en DDD. Disons que nous avons un système de traitement des commandes en ligne. Lorsqu’une nouvelle commande est créée, nous voulons envoyer un e-mail au client pour confirmer la commande. Nous pourrions implémenter cela en utilisant un événement de domaine, où la création de la commande déclenche la confirmation par e-mail :

class Order:
    def __init__(self, customer_name : str, total : float):
        self.customer_name = customer_name
        self.total = total
        self.events = []

    def create(self):
        self.events.append(OrderCreatedEvent(self.customer_name, self.total))

class OrderCreatedEvent:
    def __init__(self, customer_name : str, total : float):
        self.customer_name = customer_name
        self.total = total

    def send_confirmation_email(self):
        # send email to customer confirming order
        pass

Dans cet exemple, nous créons un objet Order avec une méthode create(), qui ajoute un événement OrderCreatedEvent à une liste d’événements. L’objet OrderCreatedEvent contient le nom du client et le total, et a également une méthode send_confirmation_email() pour envoyer le courrier électronique.

Néanmoins, cette implémentation peut avoir un impact sur les performances si plusieurs commandes sont créées en même temps. Chaque instance OrderCreatedEvent devra être créée et ajoutée à la liste des événements, puis le courrier électronique de confirmation devra être envoyé pour chaque événement. Cela pourrait potentiellement ralentir le système et donner lieu à un impact sur les performances globales.

Pour atténuer cela, nous pourrions envisager d’utiliser une file d’attente de messages ou un autre traitement asynchrone pour gérer la confirmation par e-mail séparément du processus de création de commande. Cela permettrait à la création de la commande de se poursuivre sans attendre l’envoi de l’e-mail de confirmation, améliorant ainsi les performances globales.

Dans l’ensemble, malgré que les événements de domaine puissent fournir un moyen puissant de découpler différentes parties d’un système dans DDD, il est aussi important de prendre en compte l’impact potentiel sur les performances et de concevoir le système en conséquence.

Jusqu’à maintenant, nous vous avons bien accompagné pour finir notre Odyssée de la conception pilotée par le domaine (DDD) en Python. D’ores et déjà, vous vous engagerez de la mettre en œuvre dans vos projets au grès des besoins du métier. D’ailleurs, seriez-vous prêt à compléter les exercices d’un mini-projet en DDD :  https://github.com/Invivoo/formation_python_by_ddd, où vous pourrez pratiquer et évaluer vos connaissances en DDD ?

Conclusion

Dans cet article, nous continuons notre voyage pour le retour de l’Odyssée de la conception pilotée par le domaine (DDD) en Python, qui est mise en examen pour une révision de la conception. Les avantages de la conception pilotée par le domaine comprennent une amélioration de la communication, un meilleur alignement avec le métier et une réduction de la complexité du logiciel. Néanmoins, il est important de noter que la mise en œuvre de la conception pilotée par le domaine puisse également présenter des défis, notamment la complexité de la modélisation de domaine et le coût de développement initial plus élevé. Par conséquent, la prise en décision d’utiliser la conception pilotée par le domaine doit être basée sur les besoins réels et les objectifs du métier.

Références

[1] Evans, E. (2003). Domain-driven design: tackling complexity in the heart of software. Addison-Wesley Professional.
[2] Vernon, V. (2011). Implementing domain-driven design. Addison-Wesley Professional.
[3] Fowler, M. (2013). Patterns of enterprise application architecture. Addison-Wesley Professional.
[4] Ghosh, S. (2016). Domain-driven design: A practical approach. Packt Publishing.
[5] Larman, C. (2004). Applying UML and patterns: an introduction to object-oriented analysis and design and iterative development. Prentice Hall PTR.
[6] L’article de la Conception Pilotée par Domain en Python (1)
[7] L’article de la Conception Pilotée par Domain en Python (2)
[8] L’article de la Conception Pilotée par Domain en Python (3)
[9] L’article de la Conception Pilotée par Domain en Python (4)