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()
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éponseMessage asynchrone (
→>) : envoi sans attente de réponseMessage de retour (
-->) : réponse à un message synchroneFragment
alt: alternative (if/else)Fragment
loop: répétitionFragment
opt: condition optionnelleActivation 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()
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()
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 serviceInterface 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()
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 :
Un contexte métier (2-3 paragraphes)
Des contraintes (budget, scalabilité, réglementaire)
Des attributs de qualité priorisés (disponibilité, performance, maintenabilité)
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.