26 août 2016

Domain Driven Design Part 1 - Les Outils

Design & Code

Dr. Ruijing

HU

Image d'une équipe.

26 août 2016

Domain Driven Design Part 1 - Les Outils

Design & Code

Dr. Ruijing

HU

Image d'une équipe.

26 août 2016

Domain Driven Design Part 1 - Les Outils

Design & Code

Dr. Ruijing

HU

Image d'une équipe.

Introduction

La conception pilotée par le domaine (Domain Driven Design en anglais, ou DDD en abréviation) est une méthodologie de développement de logiciels qui met l'accent sur l'importance de comprendre le domaine du métier et de créer des logiciels alignés sur les besoins du métier. Il a été introduit par Eric Evans dans son livre « Domain-Driven Design : Tackling Complexity in the Heart of Software » en 2003 [1]. Depuis sa publication, la conception pilotée par le domaine (DDD) est devenue une approche populaire pour développer des systèmes logiciels complexes ou non anémique [2].

L'idée de base derrière la conception pilotée par le domaine (DDD) est de se concentrer sur le domaine métier et de créer un modèle de domaine qui représente avec précision les processus et concepts métier [3]. Ce modèle de domaine sert de base au système logiciel, guidant sa conception et sa mise en œuvre [4, 5]. Il nous ouvre également beaucoup de portes pour s’adapter aux besoins du métier très divers lors des mises en emplacement sophistiquées.

Dans cet optique, nous explorerons de manière pratique les outils, les principes et les mises en pratique de la conception pilotée par le domaine (DDD), en examinant ses avantages et ses défis via les exemples codés en Python.

Comme cette Odyssée de la conception pilotée par le domaine est assez ambitieuse, nous allons présenter les outils dans cet article, alors que les restes seront abordés dans les articles suivants.

Outils de la conception pilotée par le domaine

Eric Evans a présenté la conception pilotée par le domaine dans son livre [1] avec différents éléments par une navigation dans la figure [1]. En effet, ce sont les outils comme les couteux suisses qui coupent les logiciels complexes en petits morceaux, puis assemblent et réutilisent ces pièces au cours de la pratique. Nous allons prendre l’exemple du commerce électronique qui est codé en Python pour apprendre les outils.

Figure [1] : Navigation de DDD

1 Domaines

Dans la conception pilotée par le domaine (DDD), un domaine fait référence à un domaine d'activité ou à une activité spécifique autour duquel un système logiciel est construit. Le domaine est le cœur du système logiciel et contient la logique métier et les règles qui régissent le comportement du système.

La conception pilotée par le domaine met l'accent sur l'importance de comprendre le domaine et de le modéliser d'une manière qui reflète avec précision les règles et processus métier. En se concentrant sur le domaine, la conception pilotée par le domaine vise à créer des systèmes logiciels qui correspondent étroitement aux besoins et aux exigences du métier, et qui sont flexibles et adaptables aux changements de l'environnement commercial.

1.1 Domaine principal

Le domaine principal (Core Domain en anglais) fait référence au domaine du métier qui offre le plus de valeur et de différenciation au métier. C'est le cœur de l'application et contient la logique métier la plus complexe et la plus critique.

1.2 Domaine générique

Le domaine générique (Generic Sub-Domain en anglais) fait référence aux parties du métier qui sont communes et pourraient être mises en œuvre de manière générique. Ce sont généralement les parties de l'application qui n'offrent pas de différenciation ou d'avantage concurrentiel significatif.

1.3 Domain de support

Le domaine de support est un domaine qui fournit des fonctionnalités de support au domaine principal. Les domaines de support ne sont pas directement liés à la valeur commerciale du système, mais sont nécessaires pour prendre en charge le fonctionnement du domaine principal.

Nous pouvons distinguer les 3 types de domaine par les branches de décision dans la figure 2.

Figure [2] : Distinction des domaines

Voici un exemple en Python d'une application e-commerce simple pour mettre en lumière le concept entre le domaine principal, le domaine générique et le domaine de support.

from typing import List
class Product:
    def __init__(self, name, description, price):
        self.name = name
        self.description = description
        self.price = price
class Order:
    def __init__(self, customer_name, items, shipping_address, total_price):
        self.customer_name = customer_name
        self.items = items
        self.shipping_address = shipping_address
        self.total_price = total_price
class ProductRepository:
    def __init__(self):
        self.products = []
    def add_product(self, product):
        self.products.append(product)
    def get_all_products(self):
        return self.products
class OrderService:
    def __init__(self, product_repository):
        self.product_repository = product_repository
    def create_order(self, order):
        for product in order.products:
            if product not in self.product_repository.get_all_products():
                raise ValueError("Product not found")
        total_price = sum(product.price for product in order.products)
        order.total_price = total_price
        return order
# Supporting domain module: authentication.py
class User:
    def __init__(self, username, password):
        self.username = username
        self.password = password
    def authenticate(self, password):
        return self.password == password
class UserRepository:
    def __init__

Dans cet exemple, les classes Product et Order représentent des entités dans le domaine principal de l'application de commerce électronique. Ce sont les parties du système qui offrent le plus de valeur et de différenciation.

La classe ProductRepository, en revanche, est un exemple de domaine générique. Il est responsable de l'accès aux données et de la gestion de l'entité Produit, mais il ne contient aucune logique métier complexe.

En fait, la classe OrderService est un service d'application qui relie le domaine principal et le domaine générique. Il contient une logique spécifique à l'application pour la création de commandes et utilise ProductRepository pour valider que tous les produits de la commande sont valides.

Sinon, le module d'authentification fournit des fonctionnalités pour l'authentification de l'utilisateur. La classe User représente un compte d'utilisateur avec un nom d'utilisateur et un mot de passe, et la classe UserRepository fournit des méthodes pour ajouter et récupérer des utilisateurs à partir du stockage.

Bien que le module d'authentification ne soit pas directement lié à la valeur métier du système, il est nécessaire pour prendre en charge le fonctionnement du domaine central. Le module d'authentification peut être intégré au domaine central de manière à permettre aux utilisateurs de s'authentifier avant de soumettre une commande, par exemple.


2 Architecture en couches

L'architecture en couches (Layered Architecture en anglais) est un modèle architectural couramment utilisé dans le développement de logiciels et peut également être appliqué à la conception pilotée par le domaine. Dans une architecture en couches, l'application ciblée est divisée en couches verticales et horizontales dans la figure 3, chacune étant responsable d'un type spécifique de fonctionnalité.

Figure [3] : Architecture en couches


2.1 Couche de domaine

La couche de domaine est responsable de l'encapsulation de la logique métier et des modèles de domaine. Dans cet exemple, supposons que nous ayons un modèle Product qui représente un produit dans notre boutique e-commerce.

# domain/models.py
class Product:
    def __init__


2.2 Couche d'application

La couche application agit comme un pont entre la couche présentation et la couche domaine. Dans cet exemple, supposons que nous ayons une classe ProductService qui expose des méthodes pour créer, lire, mettre à jour et supprimer des produits.

# application/product_service.py
from typing import List
from domain.models import Product
from infrastructure.repositories import ProductRepository
class ProductService:
    def __init__(self, product_repository: ProductRepository):
        self.product_repository = product_repository
    def create_product(self, name: str, price: float) -> Product:
        product_id = self.product_repository.get_next_id()
        product = Product(product_id, name, price)
        self.product_repository.save(product)
        return product
    def get_product(self, product_id: int) -> Product:
        return self.product_repository.get_by_id(product_id)
    def update_product(self, product: Product):
        self.product_repository.save(product)
    def delete_product(self, product_id: int):
        self.product_repository.delete(product_id)
    def calculate_product_tax(self, product_id: int, tax_rate: float) -> float:
        product = self.product_repository.get_by_id(product_id)
        return product.calculate_tax(tax_rate)
    def get_all_products(self) -> List[Product]


2.3. Couche d'infrastructure

La couche d'infrastructure fournit l'infrastructure technique qui prend en charge l'application. Dans cet exemple, supposons que nous ayons une classe ProductRepository qui gère l'accès aux données pour les produits. Nous utiliserons une simple implémentation en mémoire à des fins de démonstration.

# infrastructure/repositories.py
from typing import List
from domain.models import Product
class ProductRepository:
    def __init__(self):
        self.products = {}
        self.next_id = 1
    def get_by_id(self, product_id: int) -> Product:
        return self.products.get(product_id)
    def save(self, product: Product):
        self.products[product.id] = product
    def delete(self, product_id: int):
        del self.products[product_id]
    def get_next_id(self) -> int:
        next_id = self.next_id
        self.next_id += 1
        return next_id
    def get_all(self) -> List[Product]


2.4 Couche de présentation

La couche de présentation est responsable de la gestion des entrées et sorties de l'utilisateur. Dans cet exemple, supposons que nous ayons une application Web Flask qui expose une API REST pour la gestion des produits.

from flask import Flask, render_template, request
from application import ProductService
app = Flask(__name__)
product_service = ProductService()
@app.route('/')
def index():
    # Render the home page with a list of products
    products = product_service.get_all_products()
    return render_template('index.html', products=products)
@app.route('/product/<product_id>

Dans ce code, nous utilisons le framework Web Flask pour créer une application Web de base avec trois routes :

'/' : Cette route affiche la page d'accueil de notre site e-commerce. Il appelle la méthode get_all_products() de la classe ProductService pour obtenir une liste de tous les produits et transmet cette liste à un modèle appelé 'index.html' à afficher dans le navigateur.

'/product/<product_id>' : cette route affiche une page de détails de produit pour un produit spécifique. Il prend un paramètre product_id de l'URL, appelle la méthode get_product_by_id() de la classe ProductService pour obtenir le produit correspondant et le transmet à un modèle appelé 'product_detail.html' à afficher dans le navigateur.

'/search' : cette route affiche une page de résultats de recherche pour une requête. Il prend un paramètre de requête à partir de l'URL, appelle la méthode search_products() de la classe ProductService pour obtenir une liste de produits correspondant à la requête, et transmet la requête et les résultats à un modèle appelé 'search_results.html' à afficher dans le navigateur .

Notez que dans ce code, la couche de présentation est responsable de la gestion des entrées et sorties de l'utilisateur et de l'appel des méthodes de la couche d'application (ProductService) pour obtenir et manipuler des données.

2.5 Couche de persistance

La couche de persistance est responsable du stockage et de la récupération des données d'une base de données ou d'un autre support de stockage persistant. Il est généralement mis en œuvre à l'aide d'un système de gestion de base de données (SGBD) ou d'un cadre de mappage objet-relationnel (ORM).

Dans une application de commerce électronique, la couche de persistance serait responsable de la gestion des données relatives aux produits, commandes, clients et autres entités. Par exemple, il fournirait la fonctionnalité pour créer, lire, mettre à jour et supprimer (CRUD) des produits et des commandes de la base de données.

Voici un exemple de la façon dont vous pourriez implémenter une couche de persistance pour une application de commerce électronique en Python, en utilisant un framework ORM comme SQLAlchemy :

from sqlalchemy import create_engine, Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship, sessionmaker
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class Product(Base):
    __tablename__ = 'products'
    id = Column(Integer, primary_key=True)
    name = Column(String)
    price = Column(Integer)
class Order(Base):
    __tablename__ = 'orders'
    id = Column(Integer, primary_key=True)
    customer_name = Column(String)
    customer_email = Column(String)
    total_price = Column(Integer)
    items = relationship('OrderItem')
class OrderItem(Base):
    __tablename__ = 'order_items'
    id = Column(Integer, primary_key=True)
    order_id = Column(Integer, ForeignKey('orders.id'))
    product_id = Column(Integer, ForeignKey('products.id'))
    quantity = Column(Integer)
engine = create_engine('sqlite:///ecommerce.db')
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
session = Session()
# Create a new product
product = Product(name='iPhone', price=1000)
session.add(product)
session.commit()
# Create a new order with some items
order = Order(customer_name='Léo', customer_email='leo.hu@invivoo.com', total_price=2000)
order.items = [OrderItem(product_id=product.id, quantity=2)]

Dans cet exemple, nous définissons trois modèles SQLAlchemy pour les entités Product, Order et OrderItem. Nous créons ensuite une base de données SQLite et les tables correspondantes à l'aide de Base.metadata.create_all(). Nous utilisons ensuite l'ORM pour créer de nouveaux produits et commandes, ainsi que pour les récupérer dans la base de données.

3 Conception pilotée par le modèle (MDD)

La conception pilotée par le modèle (Model Driven Design, ou MDD en anglais) [6] est une approche du développement logiciel qui met l'accent sur la création de modèles précis et détaillés du système en cours de développement, y compris des modèles du domaine métier, de l'architecture du système et du comportement. La conception pilotée par le modèle est basé sur l'idée qu'en créant des modèles détaillés du système, les développeurs peuvent mieux comprendre et communiquer les exigences du système aux parties prenantes et s'assurer que le produit logiciel final répond à ces exigences. La conception pilotée par le modèle implique généralement l'utilisation de langages et d'outils de modélisation, tels que le langage de modélisation unifié (UML), pour créer ces modèles.

Voici un exemple de système de commerce électronique utilisant une approche de conception pilotée par un modèle en Python :

from typing import List
class Customer:
    def __init__(self, name: str, email: str):
        self.name = name
        self.email = email
class Product:
    def __init__(self, name: str, price: float):
        self.name = name
        self.price = price
class Order:
    def __init__(self, customer: Customer, products: List[Product]

Dans cet exemple, nous avons quelques modèles de domaine définis à l'aide de classes Python, notamment Customer, Product et Order. La classe OrderProcessor contient la logique métier pour le traitement des commandes et la classe OrderRepository gère la persistance des commandes dans une base de données.

Les classes ProductService et CustomerService fournissent une interface pour récupérer respectivement des listes de produits et de clients à partir de la base de données. Ces classes pourraient être implémentées à l'aide d'un ORM comme SQLAlchemy pour gérer les interactions de base de données.

En utilisant ces modèles de domaine et ces services, nous pouvons créer des modèles précis et détaillés du système de commerce électronique, y compris le domaine commercial, l'architecture du système et le comportement, ce qui peut nous aider à mieux comprendre et communiquer les exigences du système aux parties prenantes.

A part la conception pilotée par le modèle et l’architecture en couches chez les méthodologies traditionnelles, les mises en œuvre de la conception pilotée par le domaine nécessitent principalement les éléments qui seront adoptés dans les exemples applicatifs en Python chez notre prochain article.

4 Langage ubiquitaire (UBIQUITOUS en anglais)

Cette pratique a été introduite par la conception pilotée par le comportement (Behavior Driven Design ou BDD en anglais) [8] et elle met l'accent sur l'importance de créer un langage partagé entre l'équipe de développement et les experts du domaine. Les experts du domaine doivent pouvoir utiliser les mêmes termes et concepts que les développeurs, ce qui permet d'éviter les malentendus et favorise la collaboration.

5 Contextes bornés 

Ils consistent à diviser un domaine complexe en contextes plus petits et plus gérables. Chaque contexte a son propre langage, ses modèles et ses règles métier spécifiques qui sont pertinents pour ce contexte. Cela permet de garder les modèles de domaine ciblés et maintenables, tout en facilitant le développement et la maintenance de logiciels qui reflètent les besoins du métier. En plus, on doit noter que dans DDD, l'espace de nom du problème fait référence au domaine dans lequel le problème métier existe, tandis que l'espace de noms du contexte, d'autre part, fait référence à un contexte borné spécifique dans le domaine global.

Par exemple,

Domaine du commerce électronique
Espace de nom du problème : le commerce électronique
Espace de noms de contexte : la commande

Dans le domaine du commerce électronique, l'espace de noms problématique est le commerce électronique. Dans le domaine du commerce électronique, il peut y avoir différents contextes bornés, tels que la commande, la gestion des clients ou la gestion des produits. Dans cet exemple, l'espace de noms de contexte est la commande, qui se concentre sur le processus de commande de produits.

Il faudrait remarquer qu’un contexte borné pourrait comprendre plusieurs micro-services ou sous-domaines, mais pas au contraire.


6 Carte du contexte

La carte du contexte (CONTEXT MAP en anglais) consiste à identifier les relations et les dépendances entre différents contextes bornés dont les liaisons sont classifiées. Leur implémentations seront abordées lors de la mise en œuvre.


6.1 Service Hôte Ouvert

Le service hôte ouvert (Open Host Service en anglais) est utilisé pour s'intégrer à des systèmes ou services externes qui ne correspondent pas bien au modèle de domaine. L'idée est d'exposer le système externe en tant que service au modèle de domaine, plutôt que d'essayer de l'incorporer directement dans le domaine.

Un exemple de service hôte ouvert dans une application de commerce électronique pourrait être un service de passerelle de paiement. Le service de passerelle de paiement peut avoir son propre domaine et sa propre logique commerciale liée au traitement des paiements, ce qui peut ne pas convenir au domaine du commerce électronique. Au lieu d'essayer d'intégrer la logique de la passerelle de paiement directement dans le domaine du commerce électronique, la passerelle de paiement peut être présentée comme un service avec une interface standardisée que le domaine du commerce électronique peut utiliser pour communiquer avec elle.

6.2 Noyau partagé 

Le noyau partagé (Shared Kernel en anglais) est une relation entre deux ou plusieurs contextes bornés qui partagent une base de code ou un schéma de base de données commun. Ils collaborent étroitement pour développer et maintenir le noyau partagé, qui agit comme un pont entre les contextes.


Par exemple, imaginez une plateforme de commerce électronique qui propose à la fois une boutique en ligne et une application mobile. La boutique en ligne et l'application mobile nécessitent l'accès au même compte client et aux mêmes données d'historique des commandes. Pour éviter la duplication de ces données et assurer la cohérence entre les deux plates-formes, un noyau partagé pourrait être utilisé pour gérer ces données et les rendre disponibles à la fois pour la boutique en ligne et l'application mobile.

6.3 Client-fournisseur

Le client-fournisseur (Customer Supplier Teams en anglais) est une relation entre deux ou plusieurs contextes bornés où un contexte fournit un service ou des données à un autre contexte. Le contexte fournisseur est le fournisseur et le contexte récepteur est le client.

Dans le commerce électronique, un exemple d'application des équipes client-fournisseur dans la conception pilotée par le modèle pourrait être le processus de gestion des catalogues de produits. L'équipe client serait responsable de la définition des exigences et des règles commerciales liées aux informations sur les produits et à la gestion des catalogues. Ils travailleraient en étroite collaboration avec l'équipe du fournisseur, qui serait responsable de la mise en œuvre de l'infrastructure technique de gestion du catalogue de produits, y compris le stockage, l'indexation et la récupération des données.

6.4 Conformiste 

Le conformiste (Conformist en anglais) est une relation entre deux ou plusieurs contextes bornés où un contexte suit les conventions et les règles d'un autre contexte. Le contexte conformiste ajuste son comportement pour correspondre aux exigences du contexte dirigeant.

Un exemple de conformiste dans un contexte de commerce électronique pourrait être une équipe chargée de gérer l'inventaire des produits dans un entrepôt. Cette équipe utiliserait le même modèle de domaine que les autres équipes dans le contexte borné responsable de la gestion des commandes, des clients et des produits.

6.5 Couche anti-corruption (ACL) 

La couche anti-corruption (Anti-corruption Layer en anglais) est un mécanisme qui protège un contexte borné des complexités et des incohérences d'un système externe. Il agit