logo le blog invivoo blanc

Domain Driven Design Part 4 – Les pratiques

11 mai 2023 | Python | 0 comments

Dans l’article [6, 7], nous avons présenté les outils et les principes en Domain Driven Design dans un premier temps, puis dans l’article [8], nous avons poursuivi les épreuves de l’Odyssée sur la conception collaborative entre les contextes bornés, en les mettant en œuvre avec Python. Maintenant, nous allons continuer nos aventures, avec les pratiques du Domain Driven Design grâce à des exemples codés en Python.

En nous appuyant sur les principes du Domain Driven Design [6, 7], nous allons voir découvrir plusieurs pratiques utiles pour les développeurs.

1. Event Storming

Event Storming est une technique d’atelier collaboratif utilisée pour découvrir le domaine d’un système, identifier les processus métier et définir les limites du domaine [5]. En visualisant les processus et les événements commerciaux, l’équipe peut identifier les goulots d’étranglement potentiels, réduire le gaspillage et prendre de meilleures décisions.

Dans le contexte du commerce électronique, Event Storming peut aider à identifier les processus commerciaux et les événements clés qui régissent l’expérience client, tels que la navigation, la recherche, la sélection de produits, la validation et le paiement.

Afin de créer une carte de l’Event Storming, nous allons procéder aux étapes suivantes :

  • Étape 1 : Collecter les événements de domaine
  • Étape 2 : Affiner les événements de domaine
  • Étape 3 : Suivre les causes – modéliser l’écosystème
  • Étape 4 : Retrier les événements dans les contextes – catégoriser les événements et créer des contextes bornés

Pour illustrer l’utilisation d’Event Storming, prenons un exemple dans le commerce électronique, où nous devons identifier les contextes bornés et cartographier les processus impliqués dans une plate-forme de commerce électronique, en particulier les contextes de commande capturée, de panier d’achat, d’offres, de processus de paiement et de traitement des paiements.

La première étape d’Event Storming consiste à identifier les événements. Les événements sont des événements qui se produisent dans un système importants pour le métier ou le domaine. Dans cet exemple, nous pouvons commencer par des événements tels que « la commande capturée », « l’article ajouté au panier », « l’offre de promotion appliquée », « la commande vérifiée », « le paiement traité » et « la commande passée » dans la figure 1.

Une fois que nous avons identifié les événements, nous pouvons commencer à les regrouper dans des processus connexes ou des contextes bornés, ce qui implique plusieurs domaines différents, notamment la gestion des commandes, la gestion du panier, les offres, le paiement, le traitement des paiements et l’exécution des commandes. Examinons de plus près chacun de ces domaines et identifions les contextes bornés qu’ils contiennent.

La commande capturée :

La première étape du processus de commerce électronique consiste à capturer les commandes des clients. Ce contexte est responsable de la gestion des événements liés à la création et à la mise à jour des commandes client. Cela implique des événements tels que la création de la commande et la vérification de la commande avec l’inventaire. La commande est le principal agrégat dans ce contexte et elle peut avoir plusieurs éléments de ligne.

Le panier :

Le contexte du panier est responsable de la gestion du panier du client. Cela implique des événements tels que l’ajout de produit au panier, la mise à jour du panier, etc. Le panier est le principal agrégat dans ce contexte, et il peut avoir plusieurs éléments de ligne.

L’offre de promotion :

Le contexte des offres est responsable de la gestion des événements liés aux remises et aux promotions. Cela implique des événements tels que les promotions proposées, la remise appliquée, etc. Le principal agrégat dans ce contexte est la promotion, qui peut être appliquée à un seul produit ou à plusieurs produits.

Le processus de vérification :

Le processus de paiement est responsable de la gestion des événements liés au paiement et au traitement de la commande du client. Cela implique des événements tels que la vérification sélectionnée, l’e-mail saisi, l’adresse de livraison fournie, la facturation calculée et la mise à jour de l’inventaire, etc. Le principal agrégat dans ce contexte est la caisse, qui contient des informations sur la commande du client et les détails de paiement.

Le traitement des paiements :

Le contexte de traitement des paiements est responsable de la gestion des événements liés au traitement des paiements pour la commande du client. Cela implique des événements tels que la méthode de paiement, le paiement traité, etc. Le principal agrégat dans ce contexte est le paiement, qui contient des informations sur les détails et le statut du paiement.

L’exécution des commandes:

Enfin, le contexte “Exécution des commandes” implique des événements liés à l’exécution des commandes des clients, tels que la notification de l’entrepôt et l’expédition. Cela implique des événements tels que la commande livrée, ou annulée, etc. Le principal agrégat dans ce contexte est l’exécution de la commande, qui contient des informations sur l’état de la commande et des informations de suivi.

En identifiant les différents contextes bornés dans l’application de commerce électronique, nous pouvons voir que chaque contexte a son propre ensemble d’événements et d’agrégats. Cela nous permet de décomposer l’application en composants plus petits et plus faciles à gérer, qui peuvent être développés et testés indépendamment.

Par exemple, si nous développions le module de traitement des paiements, nous pourrions nous focaliser uniquement sur le contexte de traitement des paiements et ses événements et agrégats associés. Cela nous permettrait de développer et de tester le module de traitement des paiements de manière indépendante, sans nous soucier des autres parties de l’application.

Event Storming peut également aider à identifier les zones de l’application qui peuvent être sujettes à des pannes ou à des problèmes. Par exemple, si nous remarquons que plusieurs événements liés au traitement des paiements ne sont pas traités correctement, nous pouvons concentrer nos efforts sur l’amélioration de ce domaine de l’application.

Figure [1] : Carte de l’Event Storming

En résumé, Event Storming est un outil puissant pour créer un langage commun entre les experts du domaine et les développeurs et réduire le risque de mauvaise communication. En décomposant l’application en composants plus petits et plus faciles à gérer, nous pouvons développer, améliorer et tester chaque composant indépendamment, ce qui peut conduire à une application plus fiable et plus efficace et mieux alignée sur les besoins du métier.

2. Développement itératif

La conception pilotée par le domaine (DDD) recommande une approche itérative du développement logiciel, avec des boucles de rétroaction fréquentes entre les développeurs et les experts du domaine pour s’assurer que le système est sur la bonne voie et répond aux besoins du métier.

Voici un exemple de la façon dont le développement itératif peut être implémenté en Python à l’aide du framework Web Flask [9]:

from flask import Flask, request


app = Flask(__name__)


@app.route('/orders', methods=['POST'])
def create_order():
    # Get the order data from the request
    order_data = request.get_json()

    # Validate the order data
    if not validate_order(order_data):
        return {'success': False, 'message': 'Invalid order data'}

    # Create the order in the database
    if not create_order_in_database(order_data):
        return {'success': False, 'message': 'Error creating order in database'}

    # Send a confirmation email to the customer
    if not send_confirmation_email(order_data):
        return {'success': False, 'message': 'Error sending confirmation email'}

    return {'success': True}


def validate_order(order_data):
    # Add validation logic here, e.g. check if required fields are present
    return True


def create_order_in_database(order_data):
    # Add database creation logic here, e.g. create a new order record in a database
    return True


def send_confirmation_email(order_data):
    # Add email sending logic here, e.g. send a confirmation email to the customer
    return True


if __name__ == '__main__':
    app.run(debug=True)

Dans cet exemple, nous utilisons le framework Web Flask pour créer un point de terminaison API simple pour créer des commandes. Au fur et à mesure que nous développons notre système, nous pouvons recueillir en permanence les commentaires des experts du domaine et d’autres parties prenantes pour nous assurer que le point de terminaison de l’API répond à leurs besoins. Nous pouvons également utiliser des outils tels que les tests automatisés et les révisions de code pour détecter tout problème au début du processus de développement.

3. Développement piloté par les tests

La conception pilotée par le domaine (DDD) sollicite l’utilisation du développement piloté par les tests (TDD) [10] pour s’assurer que le logiciel est à la fois bien conçu et bien structuré, tout en répondant aux exigences du métier. Par ailleurs, le développement piloté par les tests (TDD) est un processus de développement logiciel qui repose sur la conversion des exigences logicielles en cas de test avant le développement complet du logiciel et sur le suivi de tout le développement logiciel en testant à plusieurs reprises le logiciel par rapport à tous les cas de test.

Voici un exemple de la façon dont TDD peut être implémenté en Python à l’aide de la bibliothèque pytest [11].

from entities import Customer, Order

def test_create_order():
    # Create a new customer
    customer = Customer(name='Leo Hu', email='leo.hu@invivoo.com')

    # Create a new order for the customer
    order = Order(customer=customer, items=['Item 1', 'Item 2'])

    # Check that the order was created correctly
    assert order.customer == customer
assert order.items == ['Item 1', 'Item 2']

Dans cet exemple, nous utilisons la bibliothèque pytest pour écrire un test de création de commandes. En écrivant d’abord le test, nous pouvons nous assurer que notre code est bien conçu, bien structuré et répond aux exigences du métier avant de commencer à écrire le code réel. Cela peut nous aider à détecter les problèmes dès le début du processus de développement et à garantir que notre logiciel est de haute qualité.

4. Refactorisation du développement (ou Reusinage du code)

La conception pilotée par le domaine (DDD) invite les développeurs à refactoriser en permanence le code pour améliorer sa conception et sa structure et pour s’assurer qu’il reste aligné avec le domaine métier.

Voici un exemple de la refactorisation d’une fonction qui calcule le prix total d’une commande :

def calculate_total_price(order):
    total = 0
    for item in order.items:
        subtotal = item.price * item.quantity
        if item.is_taxable:
            subtotal *= 1.1
        total += subtotal
    return total

Cette fonction pourrait être refactorisée pour améliorer sa lisibilité et sa maintenabilité :

def calculate_total_price(order):
    total = sum([item.total_price for item in order.items])
    return total


class OrderItem:
    def __init__(self, price, quantity, is_taxable):
        self.price = price
        self.quantity = quantity
        self.is_taxable = is_taxable
    
    @property
    def total_price(self):
        subtotal = self.price * self.quantity
        if self.is_taxable:
            subtotal *= 1.1
        return subtotal

En utilisant une classe pour représenter les éléments de la commande et en calculant le prix total en tant que propriété de cette classe, la fonction calculate_total_price est simplifiée et plus facile à comprendre.

5. Approvisionnement en événements en développement

La conception pilotée par le domaine (DDD) recommande d’utiliser l’approvisionnement en événements (Event Sourcing) [12] pour capturer les changements significatifs dans le domaine et pour permettre la communication entre des contextes bornés. Par ailleurs, l’approvisionnement en événements est un modèle permettant de conserver l’historique des modifications apportées à l’état d’une application. Les données sont stockées sous la forme d’une série d’événements, capturant un instantané de l’état de l’application. Les possibilités sont stockées et gérées dans un entrepôt d’événements, qui peut être mis en œuvre avec différentes technologies.

Voici un exemple de mise en œuvre de la recherche d’événements pour un système d’enregistrement d’utilisateur :

import uuid

class EventStore:
    def __init__(self):
        self.events = []

    def add_event(self, event):
        self.events.append(event)

    def get_events_for_id(self, id):
        return [event for event in self.events if event.user_id == id]


class UserRegistered:
    def __init__(self, user_id, name, email):
        self.user_id = user_id
        self.name = name
        self.email = email


class UserRepository:
    def __init__(self, event_store):
        self.event_store = event_store
    
    def register_user(self, name, email):
        user_id = uuid.uuid4()
        event = UserRegistered(user_id, name, email)
        self.event_store.add_event(event)
        return user_id
    
    def get_user(self, user_id):
        events = self.event_store.get_events_for_id(user_id)
        user = None
        for event in events:
            if isinstance(event, UserRegistered):
                user = User(event.user_id, event.name, event.email)
        return user


class User:
    def __init__(self, user_id, name, email):
        self.user_id = user_id
        self.name = name
        self.email = email


if __name__ == "__main__": 
    # create an event store
    event_store = EventStore()
    
    # create a user repository with the event store
    user_repository = UserRepository(event_store)
    
    # register a new user
    user_id = user_repository.register_user("Léo Hu", "leo.hu@invivoo.com")
    
    # get the user by ID
    user = user_repository.get_user(user_id)
    
    # print the user's information
    print(f"User ID: {user.user_id}")
    print(f"Name: {user.name}")
    print(f"Email: {user.email}")

Dans cet exemple, l’événement UserRegistered est utilisé pour capturer l’enregistrement d’un nouvel utilisateur. La classe UserRepository est chargée d’ajouter l’événement à l’entrepôt d’événements et de récupérer l’utilisateur de l’entrepôt d’événements en fonction de son ID. La classe User représente un utilisateur enregistré et est construite à partir des événements de l’entrepôt d’événements.

6. Ségrégation des responsabilités de commande et de requête (CQRS)

CQRS (Command Query Responsibility Segregation) est un modèle de conception utilisé dans le développement de logiciels pour séparer la responsabilité de la lecture et de l’écriture des données dans des modèles distincts [13]. Il repose sur le principe de séparer les responsabilités de lecture et d’écriture des données en créant deux modèles différents : un pour le traitement des commandes (opérations d’écriture) et un autre pour le traitement des requêtes (opérations de lecture).

Dans CQRS, le modèle d’écriture est chargé de gérer les commandes qui modifient l’état du système, telles que la création, la mise à jour ou la suppression de données. Le modèle de lecture, quant à lui, est chargé de gérer les requêtes qui récupèrent les données du système sans modifier son état.

La conception pilotée par le domaine (DDD) encourage l’utilisation de CQRS pour séparer les côtés écriture et lecture du modèle de domaine et pour optimiser le système pour chacun.

Voici un exemple d’implémentation de CQRS pour un système de commerce électronique :

# Define the command model
class OrderPlaced:
    def __init__(self, order_id: int, customer_id: int, total: float):
        self.order_id = order_id
        self.customer_id = customer_id
        self.total = total


# Define the command handler
class OrderPlacedHandler:
    def __init__(self, order_repository):
        self.order_repository = order_repository


    def handle(self, command: OrderPlaced):
        order = Order(command.order_id, command.customer_id, command.total)
        self.order_repository.add(order)


# Define the query model
class Order:
    def __init__(self, order_id: int, customer_id: int, total: float):
        self.order_id = order_id
        self.customer_id = customer_id
        self.total = total


# Define the query handler
class OrderQueryHandler:
    def __init__(self, order_repository):
        self.order_repository = order_repository

    def get_order(self, order_id: int) -> Order:
        return self.order_repository.get(order_id)


class OrderRepository:
    def __init__(self):
        self.orders = {}

    def add(self, order: Order):
        self.orders[order.order_id] = order

    def get(self, order_id: int) -> Order:
        return self.orders.get(order_id)



if __name__ == "__main__":
    order_repository = OrderRepository()
    order_placed_handler = OrderPlacedHandler(order_repository)
    order_query_handler = OrderQueryHandler(order_repository)

    # Create a new order
    order_placed_command = OrderPlaced(order_id=1, customer_id=1, total=100.00)
    order_placed_handler.handle(order_placed_command)

    # Get the order
    order = order_query_handler.get_order(1)
    if order is None:
        print("Order not found")
    else:
        print(f"Order found: order_id={order.order_id}, customer_id={order.customer_id}, total={order.total}")

Dans cet exemple, la commande OrderPlaced est responsable de la création d’une nouvelle commande. La classe OrderPlacedHandler gère cette commande en créant un nouvel objet Order et en l’ajoutant à l’entrepôt : order_repository.

A côté de lecture, nous avons le modèle de requête Order et le gestionnaire de requête OrderQueryHandler. La classe OrderQueryHandler est responsable de la récupération des informations de commande à partir de order_repository en fonction du order_id fourni.

En séparant les côtés écriture et lecture de l’application, nous pouvons optimiser chacun pour ses besoins spécifiques. Par exemple, le côté d’écriture peut nécessiter une validation complexe et des règles métier, tandis que le côté de lecture peut avoir besoin d’une interrogation et une mise en cache des données efficaces.

Conclusion

Dans cet article, nous avons bien poursuivi les pratiques de la conception pilotée par le domaine (DDD), telles que l’Event Storming, le développement itératif, le développement piloté par les tests, la refactorisation, l’approvisionnement en événements et la ségrégation des responsabilités de commande et de requête. En suivant ces pratiques, les développeurs peuvent créer des systèmes logiciels bien conçus, efficaces et faciles à maintenir.

Références