Aller au contenu

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.