16. Modélisation et design#

La conception architecturale ne se limite pas à choisir un style d’architecture ou à appliquer des patterns. Elle requiert des techniques de découverte du domaine, des outils de communication visuelle et des mécanismes de vérification continue. Ce chapitre présente les techniques qui permettent d’aller de la compréhension du problème à une conception défendable et vérifiable.

Event Storming — atelier de découverte#

L”Event Storming est un atelier de modélisation collaborative inventé par Alberto Brandolini. En quelques heures, il permet à une équipe mixte (développeurs, experts métier, testeurs, product managers) de modéliser un domaine complexe à l’aide de post-its colorés sur un grand mur.

Les éléments de base#

Chaque type d’élément a une couleur conventionnelle :

  • Orange — Domain Event : quelque chose qui s’est passé dans le domaine, nommé au passé (OrderPlaced, PaymentFailed)

  • Bleu — Command : une intention d’action, déclenchée par un acteur ou un système (PlaceOrder, ProcessPayment)

  • Jaune — Aggregate : la structure métier qui reçoit les commandes et émet les événements

  • Lilas — Policy (processus/réaction) : « quand cet événement se produit, alors cette commande est déclenchée »

  • Rose — External System : systèmes tiers (Stripe, Carrier API)

  • Vert — Read Model : données nécessaires à une décision ou affichage

  • Rouge — Hotspot : zone de confusion, de conflit ou d’incertitude à résoudre

Les trois niveaux d’Event Storming#

Big Picture : vue d’ensemble du domaine en quelques heures. On identifie les flux majeurs, les frontières naturelles, les zones d’incertitude. Participants : toutes les parties prenantes.

Process Level : on zoome sur un flux spécifique, on ajoute les commandes, les acteurs, les policies. On commence à voir les agrégats se dessiner.

Design Level : on modélise les agrégats avec leurs commandes et événements, prêt pour l’implémentation. On peut directement lire les interfaces du code.

Simulation d’un Event Storming : flux de commande e-commerce#

import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.0)

fig, ax = plt.subplots(figsize=(16, 9))
ax.set_facecolor("#2B2B3B")

# Définition : (label, type, x, y)
elements = [
    # Ligne 1 : déclenchement commande
    ("AddToCart",       "command",   0.04, 0.80),
    ("ItemAddedToCart", "event",     0.14, 0.80),
    ("Checkout",        "command",   0.24, 0.80),
    ("CartCheckedOut",  "event",     0.34, 0.80),
    ("PlaceOrder",      "command",   0.44, 0.80),
    ("OrderPlaced",     "event",     0.54, 0.80),
    ("Cart",            "aggregate", 0.19, 0.63),
    ("Order",           "aggregate", 0.49, 0.63),

    # Ligne 2 : paiement
    ("ProcessPayment",  "command",   0.04, 0.45),
    ("PaymentProcessed","event",     0.17, 0.45),
    ("PaymentFailed",   "event",     0.17, 0.32),
    ("Stripe",          "external",  0.30, 0.38),
    ("Payment",         "aggregate", 0.10, 0.28),

    # Ligne 3 : fulfilment
    ("ReserveStock",    "command",   0.44, 0.45),
    ("StockReserved",   "event",     0.57, 0.45),
    ("OutOfStock",      "event",     0.57, 0.32),
    ("CreateShipment",  "command",   0.68, 0.45),
    ("ShipmentCreated", "event",     0.80, 0.45),
    ("Inventory",       "aggregate", 0.50, 0.28),
    ("Shipping",        "aggregate", 0.74, 0.28),

    # Policies (lilas)
    ("On OrderPlaced\n→ ProcessPayment",  "policy", 0.04, 0.62),
    ("On PaymentProcessed\n→ ReserveStock", "policy", 0.44, 0.62),
    ("On StockReserved\n→ CreateShipment",  "policy", 0.62, 0.62),

    # Hotspots (rouge)
    ("Comment gérer\nles remboursements ?", "hotspot", 0.28, 0.18),
    ("Délai de\nréservation stock ?",        "hotspot", 0.55, 0.18),

    # Notifications
    ("SendConfirmation", "command",  0.88, 0.80),
    ("OrderConfirmed\nEmailSent",    "event", 0.88, 0.63),
    ("Notification",    "external",  0.88, 0.45),
]

colors = {
    "event":     "#E67E22",
    "command":   "#2E86C1",
    "aggregate": "#F1C40F",
    "policy":    "#8E44AD",
    "external":  "#E74C3C",
    "hotspot":   "#E74C3C",
    "readmodel": "#27AE60",
}
text_colors = {
    "event":     "white",
    "command":   "white",
    "aggregate": "#2B2B3B",
    "policy":    "white",
    "external":  "white",
    "hotspot":   "white",
    "readmodel": "white",
}

w, h = 0.09, 0.09

for label, typ, x, y in elements:
    color = colors.get(typ, "#888888")
    tcolor = text_colors.get(typ, "white")
    rotation = 45 if typ == "hotspot" else 0
    box = mpatches.FancyBboxPatch(
        (x, y), w, h,
        boxstyle="round,pad=0.01",
        facecolor=color, edgecolor="white",
        linewidth=1.2, alpha=0.92, zorder=3
    )
    ax.add_patch(box)
    ax.text(x + w/2, y + h/2, label,
            ha="center", va="center",
            fontsize=6.2, color=tcolor,
            fontweight="bold" if typ in ("aggregate",) else "normal",
            rotation=rotation, zorder=4)

# Flèches de flux
flux = [
    (0.13, 0.845, 0.24, 0.845),
    (0.33, 0.845, 0.44, 0.845),
    (0.53, 0.845, 0.58, 0.70),  # OrderPlaced → policy
    (0.58, 0.66, 0.58, 0.495),  # policy → ProcessPayment
    (0.26, 0.495, 0.44, 0.495), # PaymentProcessed → policy
    (0.66, 0.495, 0.68, 0.495), # StockReserved → policy
    (0.77, 0.495, 0.88, 0.845), # ShipmentCreated → Notification
]
for x1, y1, x2, y2 in flux:
    ax.annotate(
        "", xy=(x2, y2), xytext=(x1, y1),
        arrowprops=dict(
            arrowstyle="-|>", color="white",
            lw=1.0, connectionstyle="arc3,rad=0.0"
        ),
        zorder=2
    )

# Légende
legend_handles = [
    mpatches.Patch(color=colors["event"],     label="Domain Event (orange)"),
    mpatches.Patch(color=colors["command"],   label="Command (bleu)"),
    mpatches.Patch(color=colors["aggregate"], label="Aggregate (jaune)"),
    mpatches.Patch(color=colors["policy"],    label="Policy (lilas)"),
    mpatches.Patch(color=colors["external"],  label="External System / Hotspot"),
]
ax.legend(handles=legend_handles, loc="lower left",
          fontsize=8, framealpha=0.85,
          facecolor="#3A3A4A", labelcolor="white")

ax.set_xlim(0, 1)
ax.set_ylim(0.08, 1.0)
ax.set_title("Event Storming — flux commande e-commerce (Process Level)",
             fontsize=13, fontweight="bold", color="white")
ax.title.set_position([0.5, 0.97])
ax.axis("off")
plt.savefig("event_storming_ecommerce.png", dpi=120,
            bbox_inches="tight", facecolor="#2B2B3B")
plt.show()
_images/94e9fe8dbcaa87686757400d15c91a3fad1110cb4358aec4aa4e67be4fe85b39.png

Conditions de réussite d’un Event Storming

L’Event Storming fonctionne uniquement si les experts métier sont présents et actifs. Un atelier réalisé uniquement par des développeurs produit un modèle technique, pas un modèle du domaine. Prévoir 4 à 8 heures pour un Big Picture, avec des pauses fréquentes.

Example Mapping — règles métier avant le code#

L”Example Mapping est une technique de découverte des règles métier avant l’implémentation, créée par Matt Wynne. Elle prépare la phase de BDD (Behaviour-Driven Development) en clarifiant les règles, exemples et questions avant d’écrire le moindre test.

Structure#

Quatre types de cartes :

  • Jaune — Story : la fonctionnalité à spécifier (Passer une commande)

  • Bleu — Règle : une règle métier qui gouverne la story (Une commande doit avoir au moins un article)

  • Vert — Exemple : un scénario concret qui illustre une règle (Étant donné un panier vide, quand je confirme, alors j'obtiens une erreur)

  • Rouge — Question : une incertitude à résoudre (Que se passe-t-il si le stock est épuisé entre l'ajout au panier et la confirmation ?)

Exemple : story « Passer une commande »#

STORY (jaune) : En tant que client, je veux confirmer ma commande

RÈGLE 1 (bleu) : La commande doit contenir au moins un article
  → Exemple 1 (vert) : Panier vide → erreur "Commande vide"
  → Exemple 2 (vert) : Panier avec 1 article → commande créée

RÈGLE 2 (bleu) : Le stock doit être disponible
  → Exemple 3 (vert) : Stock disponible → réservation + commande
  → Exemple 4 (vert) : Stock insuffisant → erreur "Stock insuffisant"
  → Question (rouge) : Délai entre réservation et confirmation ?

RÈGLE 3 (bleu) : Le client doit avoir une adresse de livraison valide
  → Exemple 5 (vert) : Adresse manquante → erreur
  → Exemple 6 (vert) : Adresse invalide (pas de code postal) → erreur
  → Question (rouge) : Validation des adresses étrangères ?

L’Example Mapping transforme des règles floues en spécifications exécutables (Gherkin/Cucumber) et révèle les questions à résoudre avant de coder.

Durée et cadence

Une session d’Example Mapping dure 25 minutes maximum (technique Pomodoro). Si les cartes rouges s’accumulent, c’est que la story n’est pas prête — elle doit être reportée jusqu’à ce que les questions soient résolues. Mieux vaut ne pas coder que de coder la mauvaise chose.

Diagrammes de séquence UML#

Les diagrammes de séquence UML représentent les interactions entre participants (lifelines) au fil du temps. Ils sont particulièrement utiles pour documenter les flux complexes et les protocoles de communication entre services.

Notation essentielle#

  • Lifeline : participant vertical (acteur, service, composant)

  • Message synchrone () : appel bloquant, attente de réponse

  • Message asynchrone (→>) : envoi sans attente de réponse

  • Message de retour (-->) : réponse à un message synchrone

  • Fragment alt : alternative (if/else)

  • Fragment loop : répétition

  • Fragment opt : condition optionnelle

  • Activation box : rectangle sur une lifeline pendant qu’un participant est actif

Diagramme de séquence : flux de paiement e-commerce#

import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.0)

fig, ax = plt.subplots(figsize=(15, 11))
ax.set_facecolor("#FAFAFA")

participants = [
    ("Client",         0.08),
    ("API Gateway",    0.22),
    ("OrderService",   0.38),
    ("PaymentService", 0.55),
    ("Stripe API",     0.70),
    ("InventoryService", 0.85),
]

y_top = 0.95
y_bot = 0.04
lw_line = 1.2

# Lignes de vie verticales
for name, x in participants:
    ax.plot([x, x], [y_bot, y_top - 0.06],
            color="#888888", lw=lw_line, ls="--", zorder=1)
    box = mpatches.FancyBboxPatch(
        (x - 0.055, y_top - 0.065), 0.11, 0.055,
        boxstyle="round,pad=0.005",
        facecolor="#4C72B0", edgecolor="white",
        linewidth=1.5, zorder=3
    )
    ax.add_patch(box)
    ax.text(x, y_top - 0.038, name,
            ha="center", va="center",
            fontsize=7.5, color="white", fontweight="bold", zorder=4)

def _xp(name):
    return dict(participants)[name]

# Messages : (de, vers, label, y, async_, retour)
messages = [
    ("Client",       "API Gateway",     "POST /orders/checkout",     0.85, False, False),
    ("API Gateway",  "OrderService",    "PlaceOrderCommand",          0.79, False, False),
    ("OrderService", "InventoryService","CheckAvailability(items)",   0.73, False, False),
    ("InventoryService", "OrderService","AvailabilityConfirmed",      0.67, True,  True),
    ("OrderService", "PaymentService",  "ProcessPayment(order, card)",0.61, False, False),
    ("PaymentService","Stripe API",     "charge(token, amount)",      0.55, False, False),
    ("Stripe API",   "PaymentService",  "PaymentResult{ok, tx_id}",   0.49, True,  True),
    ("PaymentService","OrderService",   "PaymentProcessed(tx_id)",    0.43, True,  True),
    ("OrderService", "InventoryService","ReserveItems(order_id)",      0.37, True,  False),
    ("OrderService", "API Gateway",     "OrderConfirmation{order_id}",0.30, True,  True),
    ("API Gateway",  "Client",          "HTTP 201 Created",           0.24, True,  True),
]

for src, dst, label, y, is_async, is_return in messages:
    x1, x2 = _xp(src), _xp(dst)
    color = "#888888" if is_return else "#2C3E50"
    ls = "--" if is_return else "-"
    ax.annotate(
        "", xy=(x2, y), xytext=(x1, y),
        arrowprops=dict(
            arrowstyle="-|>",
            color=color, lw=1.4,
            linestyle=ls
        ),
        zorder=2
    )
    mx = (x1 + x2) / 2
    ax.text(mx, y + 0.018, label,
            ha="center", va="bottom",
            fontsize=7, color=color,
            bbox=dict(boxstyle="round,pad=0.1",
                      facecolor="white", edgecolor="#DDDDDD",
                      alpha=0.9, linewidth=0.5))

# Fragment ALT : succès / échec paiement
frag_x1, frag_x2 = _xp("PaymentService") - 0.10, _xp("Stripe API") + 0.07
frag_y1, frag_y2 = 0.44, 0.59
rect = mpatches.FancyBboxPatch(
    (frag_x1, frag_y1), frag_x2 - frag_x1, frag_y2 - frag_y1,
    boxstyle="square,pad=0.0",
    facecolor="none", edgecolor="#C44E52",
    linewidth=1.5, linestyle=":", zorder=5
)
ax.add_patch(rect)
ax.text(frag_x1 + 0.005, frag_y2 - 0.008, "alt",
        fontsize=8, color="#C44E52", fontweight="bold")
ax.text(frag_x1 + 0.03, frag_y2 - 0.022, "[payment_ok]",
        fontsize=7, color="#C44E52", style="italic")

ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
ax.set_title(
    "Diagramme de séquence — Flux de paiement e-commerce",
    fontsize=13, fontweight="bold", pad=10
)
ax.axis("off")

# Légende
legend_items = [
    mpatches.Patch(color="#2C3E50", label="Message synchrone"),
    mpatches.Patch(color="#888888", label="Message retour / async"),
    mpatches.Patch(color="#C44E52", label="Fragment alt"),
]
ax.legend(handles=legend_items, loc="lower right",
          fontsize=8, framealpha=0.9)

plt.savefig("sequence_paiement.png", dpi=120,
            bbox_inches="tight", facecolor="#FAFAFA")
plt.show()
_images/720aa797446b051268d50bd9d46ae7f1fefe5932aa47911e59a48028bfa36b2b.png

Diagrammes d’état (State Machine)#

Un diagramme d’état modélise le comportement d’un objet ou d’un système en termes d’états et de transitions déclenchées par des événements. Il est idéal pour modéliser le cycle de vie d’une entité (commande, ticket, paiement).

Éléments de notation#

  • État : rectangle arrondi (situation dans laquelle l’objet peut se trouver)

  • Transition : flèche étiquetée événement [garde] / action

  • État initial : point plein noir

  • État final : point plein entouré d’un cercle

  • État composite : état contenant d’autres états (sous-états)

  • Historique : reprise au dernier sous-état connu

Machine à états d’une commande e-commerce#

import networkx as nx
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.0)

fig, ax = plt.subplots(figsize=(13, 9))
ax.set_facecolor("#F8F9FA")

# États et positions
etats = {
    "●\nInitial":       (0.50, 0.92),
    "PENDING":          (0.50, 0.77),
    "CONFIRMED":        (0.50, 0.60),
    "PAYMENT\nFAILED":  (0.80, 0.60),
    "PAID":             (0.50, 0.43),
    "PREPARING":        (0.25, 0.27),
    "SHIPPED":          (0.50, 0.27),
    "DELIVERED":        (0.50, 0.12),
    "CANCELLED":        (0.80, 0.27),
    "◎\nFinal":         (0.50, 0.02),
}

colors_etat = {
    "PENDING":          "#4C72B0",
    "CONFIRMED":        "#4C72B0",
    "PAYMENT\nFAILED":  "#C44E52",
    "PAID":             "#55A868",
    "PREPARING":        "#DD8452",
    "SHIPPED":          "#DD8452",
    "DELIVERED":        "#55A868",
    "CANCELLED":        "#C44E52",
}

for etat, (x, y) in etats.items():
    if etat in ("●\nInitial", "◎\nFinal"):
        ax.plot(x, y, "o", markersize=14,
                color="#2C3E50", zorder=4)
        ax.text(x + 0.04, y, etat.split("\n")[1],
                va="center", fontsize=8, color="#2C3E50")
        continue
    color = colors_etat.get(etat, "#888888")
    box = mpatches.FancyBboxPatch(
        (x - 0.075, y - 0.04), 0.15, 0.075,
        boxstyle="round,pad=0.01",
        facecolor=color, edgecolor="white",
        linewidth=2, zorder=3
    )
    ax.add_patch(box)
    ax.text(x, y - 0.003, etat,
            ha="center", va="center",
            fontsize=8.5, fontweight="bold",
            color="white", zorder=4)

# Transitions : (src, dst, label, rad)
transitions = [
    ("●\nInitial",      "PENDING",          "Commande créée",           0.0),
    ("PENDING",         "CONFIRMED",         "place() [items > 0]",      0.0),
    ("PENDING",         "CANCELLED",         "cancel(reason)",           0.2),
    ("CONFIRMED",       "PAYMENT\nFAILED",   "paymentFailed",            0.0),
    ("PAYMENT\nFAILED", "CONFIRMED",         "retryPayment()",           -0.3),
    ("CONFIRMED",       "PAID",              "paymentProcessed",         0.0),
    ("CONFIRMED",       "CANCELLED",         "cancel() [délai expiré]",  0.3),
    ("PAID",            "PREPARING",         "startPreparation()",       0.2),
    ("PAID",            "SHIPPED",           "shipDirectly()",           0.0),
    ("PREPARING",       "SHIPPED",           "markAsShipped(tracking)",  0.0),
    ("SHIPPED",         "DELIVERED",         "confirmDelivery()",        0.0),
    ("SHIPPED",         "CANCELLED",         "returnRequested()",        0.2),
    ("DELIVERED",       "◎\nFinal",          "",                         0.0),
    ("CANCELLED",       "◎\nFinal",          "",                         0.2),
]

for src, dst, label, rad in transitions:
    x1, y1 = etats[src]
    x2, y2 = etats[dst]
    # Ajustement pour partir du bord de la boîte
    color = "#555555"
    ax.annotate(
        "", xy=(x2, y2 + 0.04), xytext=(x1, y1 - 0.04),
        arrowprops=dict(
            arrowstyle="-|>",
            color=color, lw=1.3,
            connectionstyle=f"arc3,rad={rad}"
        ),
        zorder=2
    )
    if label:
        mx = (x1 + x2) / 2
        my = (y1 + y2) / 2
        offset = rad * 0.4
        ax.text(mx + offset, my, label,
                ha="center", va="center",
                fontsize=6.5, color="#333333",
                bbox=dict(boxstyle="round,pad=0.1",
                          facecolor="white", edgecolor="#CCCCCC",
                          alpha=0.85, linewidth=0.5))

ax.set_xlim(0.05, 1.0)
ax.set_ylim(-0.02, 1.0)
ax.set_title("Machine à états — Cycle de vie d'une commande e-commerce",
             fontsize=13, fontweight="bold", pad=10)
ax.axis("off")
plt.savefig("state_machine_order.png", dpi=120,
            bbox_inches="tight", facecolor="#F8F9FA")
plt.show()
_images/516299d562d084ceedd3ff10526d1b98d539721673eee24450bf69658e05dfb2.png

Diagrammes de composants#

Les diagrammes de composants UML représentent l’organisation physique ou logique d’un système : les composants (modules, services, bibliothèques), leurs interfaces fournies et requises, et leurs dépendances.

Interfaces fournies et requises#

  • Interface fournie (lollipop ○—) : le composant expose ce service

  • Interface requise (arc ⊂—) : le composant a besoin de ce service

Ces notations permettent de visualiser le couplage et les points d’extensibilité d’une architecture.

Lecture d’un diagramme de composants#

Un diagramme de composants bien structuré répond à plusieurs questions :

  • Quels composants puis-je remplacer sans impacter les autres ?

  • Quels composants sont des points de défaillance uniques ?

  • Les dépendances vont-elles dans la bonne direction (vers les abstractions) ?

  • Y a-t-il des cycles de dépendances ?

Pour un e-commerce :

┌─────────────────────────────────────────────────────────┐
│                     API Gateway                         │
│          Fourni: HTTP/REST    Requis: Auth, OrderSvc    │
└──────────────┬───────────────────────────────────────────┘
               │
    ┌──────────┴──────────┬──────────────────┐
    ▼                     ▼                  ▼
┌─────────┐         ┌──────────┐       ┌──────────┐
│  Order  │────────▶│Inventory │       │ Payment  │
│ Service │         │ Service  │       │ Service  │──▶ [Stripe]
└────┬────┘         └──────────┘       └──────────┘
     │
     ▼
┌──────────┐         ┌──────────┐
│  Order   │         │ Domain   │
│Repository│         │ Events   │──▶ [Kafka]
└──────────┘         └──────────┘

Fitness Functions — tests automatisés de propriétés architecturales#

Les Fitness Functions sont un concept issu du livre « Building Evolutionary Architectures » (Ford, Parsons, Kua). Ce sont des tests automatisés qui vérifient des propriétés de l’architecture elle-même — non pas le comportement fonctionnel, mais la structure du code.

Types de fitness functions#

  • Cycles de dépendances : détecter les dépendances circulaires entre modules

  • Couplage afférent/efférent : mesurer combien de modules dépendent d’un module donné

  • Violations de couches : détecter qu’un module de domaine importe un module d’infrastructure

  • Coverage de tests : s’assurer qu’aucun agrégat n’a moins de 80% de couverture

  • Taille des agrégats : alerter si un agrégat dépasse N classes internes

Simulation : détection de cycles dans un graphe de dépendances#

import networkx as nx
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.0)

# --- Graphe de dépendances d'une application (modules Python) ---
# Chaque nœud = module, chaque arête = dépendance import

deps_sains = [
    # Application → Domain
    ("app.handlers",       "domain.order"),
    ("app.handlers",       "domain.payment"),
    ("app.handlers",       "domain.inventory"),
    # Domain → (rien d'externe)
    ("domain.order",       "domain.shared"),
    ("domain.payment",     "domain.shared"),
    ("domain.inventory",   "domain.shared"),
    # Infrastructure → Domain
    ("infra.repositories", "domain.order"),
    ("infra.repositories", "domain.payment"),
    ("infra.messaging",    "domain.shared"),
    # App → Infrastructure (via injection)
    ("app.handlers",       "infra.repositories"),
    ("app.handlers",       "infra.messaging"),
]

deps_cycliques = deps_sains + [
    # Cycles introduits (violations)
    ("domain.order",   "infra.repositories"),  # domaine → infra : violation !
    ("domain.shared",  "app.handlers"),         # cycle shared → handlers
]

def detecter_cycles(graph):
    try:
        cycles = list(nx.simple_cycles(graph))
        return cycles
    except Exception:
        return []

def dessiner_graphe(deps, ax, titre, montrer_cycles=True):
    G = nx.DiGraph()
    G.add_edges_from(deps)

    cycles = detecter_cycles(G) if montrer_cycles else []
    noeuds_cycliques = set(n for cycle in cycles for n in cycle)

    # Disposition
    try:
        pos = nx.planar_layout(G)
    except Exception:
        pos = nx.spring_layout(G, seed=42, k=2.0)

    # Couleur des nœuds selon la couche
    def node_color(n):
        if n in noeuds_cycliques:
            return "#C44E52"
        if n.startswith("app"):
            return "#4C72B0"
        if n.startswith("domain"):
            return "#55A868"
        if n.startswith("infra"):
            return "#DD8452"
        return "#888888"

    ncolors = [node_color(n) for n in G.nodes()]

    # Arêtes cycliques
    aretes_cycliques = set()
    for cycle in cycles:
        for i in range(len(cycle)):
            aretes_cycliques.add((cycle[i], cycle[(i+1) % len(cycle)]))

    edge_colors = [
        "#C44E52" if (u, v) in aretes_cycliques else "#AAAAAA"
        for u, v in G.edges()
    ]
    edge_widths = [
        2.5 if (u, v) in aretes_cycliques else 1.0
        for u, v in G.edges()
    ]

    nx.draw_networkx_nodes(G, pos, ax=ax,
                           node_color=ncolors,
                           node_size=1800, alpha=0.9)
    nx.draw_networkx_labels(G, pos, ax=ax,
                            font_size=7, font_color="white",
                            font_weight="bold")
    nx.draw_networkx_edges(G, pos, ax=ax,
                           edge_color=edge_colors,
                           width=edge_widths,
                           arrows=True,
                           arrowsize=15,
                           arrowstyle="-|>",
                           connectionstyle="arc3,rad=0.1")

    if cycles:
        msg = f"⚠ {len(cycles)} cycle(s) détecté(s)"
        ax.text(0.5, -0.05, msg, ha="center", va="top",
                transform=ax.transAxes,
                fontsize=10, color="#C44E52", fontweight="bold")
        for i, cycle in enumerate(cycles):
            ax.text(0.5, -0.10 - i * 0.07,
                    " → ".join(cycle) + " → " + cycle[0],
                    ha="center", va="top",
                    transform=ax.transAxes,
                    fontsize=7.5, color="#C44E52")
    else:
        ax.text(0.5, -0.05, "✓ Aucun cycle — architecture saine",
                ha="center", va="top",
                transform=ax.transAxes,
                fontsize=10, color="#55A868", fontweight="bold")

    ax.set_title(titre, fontsize=11, fontweight="bold", pad=8)
    ax.axis("off")


fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 8))
fig.patch.set_facecolor("#F8F9FA")

dessiner_graphe(deps_sains,    ax1,
                "Architecture saine — sans cycles", montrer_cycles=True)
dessiner_graphe(deps_cycliques, ax2,
                "Architecture avec violations", montrer_cycles=True)

# Légende commune
legend_handles = [
    mpatches.Patch(color="#4C72B0", label="Couche Application"),
    mpatches.Patch(color="#55A868", label="Couche Domaine"),
    mpatches.Patch(color="#DD8452", label="Couche Infrastructure"),
    mpatches.Patch(color="#C44E52", label="Nœud / arête cyclique"),
]
fig.legend(handles=legend_handles, loc="lower center",
           ncol=4, fontsize=9, framealpha=0.9)

fig.suptitle("Fitness Function — Détection automatique de cycles de dépendances",
             fontsize=13, fontweight="bold")
plt.savefig("fitness_function_cycles.png", dpi=120,
            bbox_inches="tight", facecolor="#F8F9FA")
plt.show()
_images/150d89d87fd204ae02edd633be83716cb07ab53c5d28345747fe046b5062ef91.png

Intégration dans la CI/CD#

# fitness_functions.py — exécuté à chaque commit (pytest)
import ast
import os
from pathlib import Path
import networkx as nx
import pytest

def build_dependency_graph(src_root: str) -> nx.DiGraph:
    """Construit le graphe de dépendances en analysant les imports."""
    G = nx.DiGraph()
    root = Path(src_root)
    for py_file in root.rglob("*.py"):
        module = str(py_file.relative_to(root)).replace("/", ".")[:-3]
        tree = ast.parse(py_file.read_text())
        for node in ast.walk(tree):
            if isinstance(node, (ast.Import, ast.ImportFrom)):
                if isinstance(node, ast.ImportFrom) and node.module:
                    G.add_edge(module, node.module)
    return G

def test_no_circular_dependencies():
    """Fitness function : aucun cycle de dépendances."""
    G = build_dependency_graph("src")
    cycles = list(nx.simple_cycles(G))
    assert not cycles, (
        f"Cycles détectés :\n"
        + "\n".join(" → ".join(c) for c in cycles)
    )

def test_domain_does_not_import_infrastructure():
    """Fitness function : le domaine ne dépend pas de l'infrastructure."""
    G = build_dependency_graph("src")
    violations = [
        (src, dst)
        for src, dst in G.edges()
        if src.startswith("domain.") and "infra" in dst
    ]
    assert not violations, (
        f"Violations couches :\n"
        + "\n".join(f"  {s}{d}" for s, d in violations)
    )

def test_aggregate_test_coverage():
    """Fitness function : couverture minimale des agrégats."""
    # Intégration avec pytest-cov — récupérer les métriques de coverage
    import coverage
    cov = coverage.Coverage()
    cov.load()
    data = cov.get_data()
    for filepath in data.measured_files():
        if "aggregate" in filepath.lower():
            pct = cov.report(morfs=[filepath], show_missing=False)
            assert pct >= 80, (
                f"Coverage insuffisante pour {filepath}: {pct:.1f}% < 80%"
            )

Fitness functions dans la CI

Exécuter les fitness functions à chaque pull request, pas uniquement en pré-production. Un cycle de dépendances détecté après 6 mois de développement coûte 10 fois plus cher à corriger qu’un cycle détecté dès son introduction.

Architecture kata — pratique délibérée de la conception#

Un Architecture Kata est un exercice de conception architecturale chronométré, proposé par Ted Neward. Il permet de pratiquer délibérément les décisions architecturales dans un cadre sans risque.

Format d’un kata#

Chaque kata décrit :

  1. Un contexte métier (2-3 paragraphes)

  2. Des contraintes (budget, scalabilité, réglementaire)

  3. Des attributs de qualité priorisés (disponibilité, performance, maintenabilité)

  4. Des questions ouvertes qui forcent les compromis

L’équipe dispose de 45 minutes pour proposer une architecture, puis la présente et la défend devant les autres équipes.

Exemple de kata : plateforme de streaming de cours#

CONTEXTE
Une startup EdTech veut lancer une plateforme de cours en ligne avec
vidéos, quiz et certificats. Lancement dans 3 mois. Budget limité.

UTILISATEURS
- 10 000 utilisateurs au lancement, objectif 500 000 en 18 mois
- Présence internationale (Europe, Amérique du Nord)

CONTRAINTES
- Équipe de 4 développeurs
- Infrastructure cloud (AWS ou GCP)
- RGPD obligatoire pour les données européennes
- Vidéos HD jusqu'à 4K

ATTRIBUTS DE QUALITÉ (par ordre de priorité)
1. Disponibilité (99.9% min)
2. Performance (vidéo < 2s de latence)
3. Maintenabilité (équipe petite)
4. Coût (startup, budget serré)

QUESTIONS OUVERTES
- Héberger les vidéos soi-même ou déléguer (Cloudflare Stream, Mux) ?
- Monolithe modulaire ou microservices ?
- Base de données unique ou séparée par domaine ?
- Comment gérer la montée en charge imprévue ?

Évaluation d’une proposition architecturale#

Une bonne réponse à un kata ne doit pas être « la bonne architecture » (il n’en existe pas une seule) mais doit :

  • Justifier chaque décision majeure par rapport aux attributs de qualité

  • Identifier explicitement les compromis (trade-offs)

  • Proposer un chemin évolutif (pas un big bang)

  • Lister les risques et comment les atténuer

La pratique délibérée

Un développeur senior pratique régulièrement les katas architecturaux — en groupe ou seul — même sur des domaines qu’il ne maîtrise pas. L’inconfort face à un domaine inconnu est précisément ce qui développe la capacité à poser les bonnes questions plutôt qu’à proposer des solutions prématurées.

Résumé#

Ce chapitre a présenté les techniques de modélisation et de design qui entourent l’implémentation :

  • L”Event Storming permet de découvrir le domaine en équipe avant d’écrire la moindre ligne de code, en révélant les flux, les frontières et les zones d’incertitude.

  • L”Example Mapping transforme les règles métier floues en spécifications exécutables et identifie les questions à résoudre avant de coder.

  • Les diagrammes de séquence documentent les interactions entre composants, en particulier pour les flux complexes multi-services.

  • Les machines à états modélisent explicitement le cycle de vie des entités et rendent les transitions et gardes visibles.

  • Les diagrammes de composants représentent les dépendances architecturales et les interfaces.

  • Les fitness functions automatisent la vérification des propriétés architecturales (cycles, violations de couches, couplage) dans la CI/CD.

  • Les architecture katas développent la capacité à concevoir sous contrainte, à justifier des compromis et à communiquer des décisions.

Architecture comme activité continue

L’architecture n’est pas une phase en début de projet — c’est une activité continue. Les fitness functions vérifient que l’architecture réelle ne dérive pas de l’architecture voulue. L’Event Storming peut être rejoué quand le domaine évolue. Les diagrammes de séquence et d’état vivent à côté du code, mis à jour lors de chaque modification significative. Une architecture documentée mais non vérifiée est une architecture dont on ne peut pas faire confiance.