Fitness functions¶
Automatiser la vérification des propriétés architecturales — tests continus qui transforment les intentions en garanties mesurables.
Pourquoi des fitness functions¶
Une architecture non validee est une hypothèse. Elle peut sembler cohérente sur un diagramme et se révéler fragile en production — couplage invisible, latence sous-estimée, sécurité contournee par pragmatisme. Les fitness functions transforment les intentions architecturales en garanties mesurables. Sans elles, la dette technique s'accumule silencieusement jusqu'au moment où le système ne répond plus aux exigences qui ont guide sa conception.
Les fitness functions sont des tests automatises qui verifient les propriétés non-fonctionnelles de l'architecture. Elles jouent le même rôle que les tests unitaires pour le comportement du code : elles rendent les violations visibles et immédiates, avant qu'elles n'atteignent la production.
Le terme vient de Neal Ford, Rebecca Parsons et Patrick Kua dans Building Evolutionary Architectures. L'analogie avec les fonctions de fitness en algorithmique evolutionnaire est délibérée : on définit ce qu'on optimise, et on mesure en continu si le système évolue dans la bonne direction.
Catégories de fitness functions¶
Les fitness functions couvrent l'ensemble des propriétés architecturales. On les organisé en catégories pour structurer leur mise en place progressive.
Deployabilite¶
La capacité a déployer rapidement et en confiance.
| Fitness function | Seuil | Outil |
|---|---|---|
| Taille image Docker | < 500 MB | docker inspect |
| Temps de build CI | < 10 minutes | CI metrics |
| Nombre de dépendances directes | < 50 | pip-audit, npm ls |
| Ratio tests / code (coverage) | > 80% | pytest-cov, istanbul |
| Zero secret en clair dans le code | 0 occurrence | gitleaks, trufflehog |
Scalabilité¶
La capacité a absorber la charge sans dégradation.
| Fitness function | Seuil | Outil |
|---|---|---|
| Latence p99 API | < 200 ms | Prometheus histogram |
| Throughput sous charge | > N req/s | k6, locust |
| Nombre de connexions DB en pic | < pool max * 0.8 | pg_stat_activity |
| Temps de scale-out | < 2 minutes | Kubernetes HPA metrics |
Testabilité¶
La capacité a tester le système de manière isolee et reproductible.
| Fitness function | Seuil | Outil |
|---|---|---|
| Dépendances cycliques | 0 cycle | analyse statique |
| Couplage entre bounded contexts | 0 import interdit | custom fitness function |
| Ratio tests intégration / unitaires | < 1:5 | test runner metrics |
| Temps d'exécution suite de tests | < 5 minutes | CI metrics |
Sécurité¶
La capacité a résister aux menaces et a protéger les données.
| Fitness function | Seuil | Outil |
|---|---|---|
| Vulnérabilités critiques (CVE) | 0 | Trivy, Grype |
| Headers sécurité HTTP | 100% endpoints | OWASP ZAP baseline |
| Certificats proches expiration | > 30 jours | cert-manager metrics |
| Conformité RBAC | 0 rôle sur-privilege | OPA / Kyverno |
Implémentation en CI¶
Les fitness functions n'ont de valeur que si elles s'exécutent automatiquement. L'intégration en CI est non negociable.
Architecture du pipeline¶
flowchart LR
COMMIT["Commit"] --text--> UNIT["Tests unitaires"]
UNIT --text--> FITNESS["Fitness functions"]
FITNESS --text--> INTEG["Tests integration"]
INTEG --text--> DEPLOY["Deploiement"]
FITNESS --"echec"--> BLOCK["Build bloque"] Les fitness functions se positionnent après les tests unitaires (rapides) et avant les tests d'intégration (coûteux). Une violation architecturale bloque le pipeline avant même de lancer les tests lents.
Exemple : test de dépendances inter-modules¶
# fitness/test_dependencies.py
import ast
import os
import pytest
from pathlib import Path
# Regles de dependance entre bounded contexts
FORBIDDEN_DEPS = {
"catalog": ["order", "payment", "notification"],
"order": ["catalog"],
"payment": ["catalog", "order", "notification"],
"notification": ["catalog", "order", "payment"],
}
def get_imports(filepath: str) -> list[str]:
"""Parse les imports Python d'un fichier."""
with open(filepath) as f:
tree = ast.parse(f.read())
imports = []
for node in ast.walk(tree):
if isinstance(node, ast.Import):
imports.extend(alias.name for alias in node.names)
elif isinstance(node, ast.ImportFrom):
if node.module:
imports.append(node.module)
return imports
def get_module_context(filepath: str) -> str | None:
"""Determine le bounded context d'un fichier selon son chemin."""
parts = Path(filepath).parts
for i, part in enumerate(parts):
if part == "src" and i + 1 < len(parts):
return parts[i + 1]
return None
@pytest.mark.parametrize("py_file", list(Path("src").rglob("*.py")))
def test_no_cross_context_dependency(py_file):
context = get_module_context(str(py_file))
if context not in FORBIDDEN_DEPS:
return
imports = get_imports(str(py_file))
forbidden = FORBIDDEN_DEPS[context]
for imp in imports:
for forbidden_ctx in forbidden:
assert not imp.startswith(f"src.{forbidden_ctx}"), (
f"{py_file}: le contexte '{context}' importe depuis "
f"'{forbidden_ctx}' (import interdit: {imp})"
)
Exemple : taille des images Docker¶
# fitness/test_docker.py
import subprocess
import pytest
MAX_IMAGE_SIZE = 500 * 1024 * 1024 # 500 MB
IMAGES = ["ecommerce-api:latest", "ecommerce-worker:latest"]
@pytest.mark.parametrize("image", IMAGES)
def test_docker_image_size(image):
result = subprocess.run(
["docker", "image", "inspect", image,
"--format", "{{.Size}}"],
capture_output=True, text=True
)
size_bytes = int(result.stdout.strip())
assert size_bytes < MAX_IMAGE_SIZE, (
f"Image {image} trop lourde: {size_bytes / 1e6:.0f} MB "
f"(max: {MAX_IMAGE_SIZE / 1e6:.0f} MB)"
)
Fitness functions composites¶
Certaines propriétés architecturales ne se mesurent pas avec un seul test. Une fitness function composite agrégé plusieurs signaux pour produire un score global.
# fitness/composite_health.py
from dataclasses import dataclass
@dataclass
class FitnessScore:
name: str
score: float # 0.0 a 1.0
weight: float
threshold: float
def compute_architecture_health(scores: list[FitnessScore]) -> float:
"""Score de sante architecturale pondere."""
total_weight = sum(s.weight for s in scores)
weighted_sum = sum(s.score * s.weight for s in scores)
return weighted_sum / total_weight
def check_all_thresholds(scores: list[FitnessScore]) -> list[str]:
"""Retourne les violations de seuil."""
return [
f"{s.name}: {s.score:.2f} < {s.threshold:.2f}"
for s in scores
if s.score < s.threshold
]
# Utilisation
scores = [
FitnessScore("couplage", 0.95, weight=3.0, threshold=0.90),
FitnessScore("latence_p99", 0.88, weight=2.0, threshold=0.85),
FitnessScore("couverture_tests", 0.82, weight=1.0, threshold=0.80),
FitnessScore("securite_cve", 1.0, weight=3.0, threshold=1.0),
]
health = compute_architecture_health(scores)
violations = check_all_thresholds(scores)
Évolution des fitness functions dans le temps¶
Les fitness functions ne sont pas statiques. Elles evoluent avec le système — et cette évolution doit être délibérée.
Maturation progressive¶
Au démarrage d'un projet, on commence avec des fitness functions basiques (dépendances cycliques, taille des artefacts). Au fur et à mesure que le système murit, on ajoute des fitness functions plus sophistiquees.
flowchart TD
V1["V1 — Fondations"] --text--> V2["V2 — Consolidation"]
V2 --text--> V3["V3 — Maturite"]
V1 --- F1["Zero dependance cyclique\nTaille image < 500 MB\nCouverture tests > 60%"]
V2 --- F2["Latence p99 < 200 ms\nZero CVE critique\nCouplage inter-modules"]
V3 --- F3["Score sante composite\nDrift detection\nCompliance as code"] Ajustement des seuils¶
Les seuils doivent être ajustes en fonction des mesures réelles. Un seuil trop lâche ne protégé pas. Un seuil trop strict généré du bruit et pousse les équipes a ignorer les alertes.
La bonne approche : mesurer pendant 2-4 semaines, fixer le seuil a un ecart raisonnable au-dessus de la valeur observee, puis resserrer progressivement.
Deprecation et remplacement¶
Quand une fitness function devient obsolète (le risque qu'elle couvrait n'existe plus), on la supprimé explicitement plutôt que de la laisser trainer. On documente la raison dans un ADR — la suppression d'une fitness function est une décision architecturale.
Tip
Integrez les fitness functions dans la CI. Une propriété architecturale non testée finira par être violee — c'est une question de temps. Et quand elle sera violee, personne ne s'en apercevra avant que les conséquences soient coûteuses.
Pieges a éviter¶
| Piege | Conséquence | Remede |
|---|---|---|
| Trop de fitness functions trop tôt | Pipeline lent, équipe frustrée | Commencer par 3-5, ajouter par vagues |
| Seuils arbitraires | Faux positifs, perte de confiance | Mesurer d'abord, fixer ensuite |
| Pas de responsable par fitness function | Personne ne réagit aux violations | Assigner un owner par catégorie |
| Fitness functions sans remédiation | On sait qu'il y a un problème, rien ne bouge | Chaque alerte a un runbook associe |
| Ignorer les fitness functions en staging | Violations découvertes trop tard | Même pipeline en staging et production |
Note
Les fitness functions ne remplacent pas le jugement humain. Elles automatisent la détection des dérivés connues. Les dérivés inconnues — les risques emergents, les changements de contexte — necessitent des revues d'architecture structurées.
Chapitre suivant : Revues d'architecture — ATAM — la méthode ATAM, les revues légères, et le rôle du board de revue.