Clean code et refactoring - 11 - Complexite cyclomatique

Mesurer la complexité du code, la réduire, et comprendre son impact sur la testabilité

  1. 01 Clean code et refactoring - 00 - Pourquoi le clean code est un investissement, pas un luxe
  2. 02 Clean code et refactoring - 01 - Nommage : la competence la plus sous-estimee
  3. 03 Clean code et refactoring - 02 - Fonctions : courtes, claires, responsables
  4. 04 Clean code et refactoring - 03 - Conditions et lisibilité : sortir de la pyramide
  5. 05 Clean code et refactoring - 04 - Commentaires et documentation : quand le code ne suffit pas
  6. 06 Clean code et refactoring - 05 - Immutabilite et effets de bord : moins de surprises, moins de bugs
  7. 07 Clean code et refactoring - 06 - Gestion des erreurs propre : fail fast, fail loud
  8. 08 Clean code et refactoring - 07 - Programmation defensive vs offensive : valider aux frontieres, faire confiance a l'intérieur
  9. 09 Clean code et refactoring - 08 - SOLID en pratique avec TypeScript
  10. 10 Clean code et refactoring - 09 - DRY, KISS, YAGNI
  11. 11 Clean code et refactoring - 10 - Couplage et cohesion
  12. 12 Clean code et refactoring - 11 - Complexite cyclomatique
  13. 13 Clean code et refactoring - 12 - Abstractions prematurees vs tardives
  14. 14 Clean code et refactoring - 13 - Code smells
  15. 15 Clean code et refactoring - 14 - Techniques de refactoring
  16. 16 Clean code et refactoring - 15 - Refactoring legacy sans tout casser
  17. 17 Clean code et refactoring - 16 - Tests comme filet de sécurité pour le refactoring
  18. 18 Clean code et refactoring - 17 - Structurer un projet — feature-based vs layer-based
  19. 19 Clean code et refactoring - 18 - Constantes, configuration et magic numbers
  20. 20 Clean code et refactoring - 19 - Linting et formatting — ESLint, Biome, automatiser la qualité
  21. 21 Clean code et refactoring - 20 - Conventions d'équipe et ADR
  22. 22 Clean code et refactoring - 21 - Dette technique — quand elle est acceptable, quand elle tue le projet
  23. 23 Clean code et refactoring - 22 - Code review — donner et recevoir du feedback
  24. 24 Clean code et refactoring - 23 - Glossaire — tous les termes de la serie

11 - Complexite cyclomatique -- mesurer, réduire, testabilité

Ce que tu vas apprendre

  • Ce que la complexité cyclomatique mesure concrètement
  • Comment la calculer et quels outils l'automatisent
  • Le lien direct entre complexité et difficulte de test
  • Des techniques pour réduire la complexité sans perdre de fonctionnalité
  • La différence entre complexité cyclomatique et complexité cognitive

Prerequisites

Cet article fait suite a 10 - Couplage et cohesion. Les notions de SOLID et de KISS seront utiles.


Un jour, j'ai ouvert une fonction de 200 lignes avec 14 niveaux d'imbrication. Elle gerait les permissions d'acces. Chaque if avait un else qui avait un if qui avait un switch. J'ai essaye d'écrire un test. Au bout de 45 minutes, j'avais couvert 3 des 47 chemins possibles. La complexité cyclomatique de cette fonction etait de 47. Autrement dit : 47 tests minimum pour une couverture complète des branches.

Qu'est-ce que la complexité cyclomatique ?

Thomas McCabe a introduit cette metrique en 1976. L'idee est simple : on compte le nombre de chemins lineairement independants dans une fonction. Chaque point de décision (if, else, while, for, case, &&, ||, ternaire) ajoute un chemin.

typescript// Complexite = 1 (aucune branche)
function greet(name: string): string {
  return `Bonjour ${name}`;
}

// Complexite = 3 (deux if = deux branches supplementaires)
function getDiscount(user: User): number {
  let discount = 0;               // chemin de base = 1
  if (user.isPremium) {           // +1
    discount = 10;
  }
  if (user.orderCount > 50) {     // +1
    discount += 5;
  }
  return discount;                // total = 3
}

La formule : M = E - N + 2P, ou E est le nombre d'aretes du graphe de contrôle, N le nombre de noeuds, et P le nombre de composantes connexes. En pratique, on compte les points de décision et on ajoute 1.

Pourquoi ca compte pour les tests

Une fonction de complexité N a au minimum N chemins a tester pour une couverture de branche complète. En pratique, les combinaisons explosent.

typescript// Complexite = 7 -- un monstre courant
function processOrder(order: Order, user: User): Result {
  if (!order.items.length) {                    // +1
    return { status: "empty" };
  }
  if (!user.isActive) {                         // +1
    return { status: "inactive_user" };
  }

  let total = 0;
  for (const item of order.items) {             // +1
    if (item.quantity <= 0) {                   // +1
      return { status: "invalid_quantity" };
    }
    total += item.price * item.quantity;
  }

  if (total > 10000 && !user.isPremium) {       // +1, +1 (&&)
    return { status: "limit_exceeded" };
  }

  return { status: "ok", total };
}

Sept chemins. Sept cas de test minimum. Et c'est une fonction relativement simple. Imagine ce que donne une fonction avec des try/catch imbriques, des switch de 15 cases, et des conditions combinees.

Comment mesurer

Plusieurs outils calculent la complexité cyclomatique automatiquement.

En ESLint, la regle complexity :

json{
  "rules": {
    "complexity": ["error", { "max": 10 }]
  }
}

SonarQube la mesure aussi, avec des seuils configurables. La plupart des équipes mettent le seuil a 10 ou 15. Au-dela de 10, la fonction merite probablement d'etre decoupee.

Les seuils courants :

Complexite Interprétation
1-5 Simple, facile a tester
6-10 Acceptable, attention
11-20 Risque, difficile a maintenir
21+ Danger, refactoring urgent

Techniques de réduction

Early returns

La technique la plus simple et la plus efficace. Au lieu d'imbriquer, on sort tot.

typescript// Avant : imbrication profonde (complexite = 5)
function processPayment(payment: Payment): Result {
  if (payment) {
    if (payment.amount > 0) {
      if (payment.method) {
        if (isValidMethod(payment.method)) {
          return charge(payment);
        } else {
          return { error: "invalid_method" };
        }
      } else {
        return { error: "no_method" };
      }
    } else {
      return { error: "invalid_amount" };
    }
  } else {
    return { error: "no_payment" };
  }
}

// Apres : early returns (meme complexite, mais lisibilite ++)
function processPayment(payment: Payment): Result {
  if (!payment) return { error: "no_payment" };
  if (payment.amount <= 0) return { error: "invalid_amount" };
  if (!payment.method) return { error: "no_method" };
  if (!isValidMethod(payment.method)) return { error: "invalid_method" };

  return charge(payment);
}

La complexité cyclomatique est la meme (le nombre de branches n'a pas change). Mais la complexité cognitive -- celle que ton cerveau ressent -- est bien plus basse.

Extraire des fonctions

Chaque extraction réduit la complexité de la fonction d'origine.

typescript// Avant : tout dans une fonction (complexite = 8)
function createInvoice(order: Order): Invoice {
  // validation (3 branches)
  // calcul des taxes (2 branches)
  // formatage (3 branches)
}

// Apres : trois fonctions de complexite 3, 2, et 3
function createInvoice(order: Order): Invoice {
  validateOrder(order);
  const taxes = calculateTaxes(order);
  return formatInvoice(order, taxes);
}

Remplacer les conditions par du polymorphisme

Quand un switch ou une chaîne de if/else fait la meme chose a chaque ajout, le pattern Strategy le remplace.

typescript// Avant : switch qui grandit a chaque nouveau type (complexite = N+1)
function calculateShipping(method: string, weight: number): number {
  switch (method) {
    case "standard": return weight * 0.5;
    case "express": return weight * 1.2 + 5;
    case "overnight": return weight * 2.0 + 15;
    case "drone": return weight * 3.0 + 25;
    default: throw new Error(`Unknown method: ${method}`);
  }
}

// Apres : une map de strategies (complexite = 1)
const shippingStrategies: Record<string, (weight: number) => number> = {
  standard: (w) => w * 0.5,
  express: (w) => w * 1.2 + 5,
  overnight: (w) => w * 2.0 + 15,
  drone: (w) => w * 3.0 + 25,
};

function calculateShipping(method: string, weight: number): number {
  const strategy = shippingStrategies[method];
  if (!strategy) throw new Error(`Unknown method: ${method}`);
  return strategy(weight);
}

La complexité de calculateShipping est passee de 5 a 2. Chaque stratégie a une complexité de 1. Et ajouter un nouveau mode de livraison ne touche pas la fonction.

Complexite cognitive vs cyclomatique

SonarSource a introduit la complexité cognitive en 2017. Elle tente de mesurer ce que le cerveau humain ressent, pas juste le nombre de chemins.

typescript// Complexite cyclomatique = 4, complexite cognitive = 1
function getLabel(status: Status): string {
  switch (status) {
    case "active": return "Actif";
    case "pending": return "En attente";
    case "disabled": return "Desactive";
    default: return "Inconnu";
  }
}

// Complexite cyclomatique = 3, complexite cognitive = 7
function process(a: boolean, b: boolean, c: boolean): string {
  if (a) {                    // +1
    if (b) {                  // +2 (imbrication)
      if (c) {                // +3 (imbrication double)
        return "abc";
      }
    }
    return "a";               // +1
  }
  return "none";
}

Le switch est simple a lire malgre sa complexité cyclomatique de 4. Les if imbriques sont penibles malgre une complexité cyclomatique plus basse. La complexité cognitive capture mieux cette réalité.

Les deux metriques sont utiles. La cyclomatique aide pour le nombre de tests. La cognitive aide pour la lisibilité. Plus de détails sur les metriques de qualité de code sur paltemps.fr.

Résumé

  • La complexité cyclomatique compte les chemins independants dans une fonction
  • Chaque if, for, while, case, &&, || ajoute un chemin
  • Une complexité supérieure a 10 est un signal de refactoring
  • Les early returns reduisent la complexité cognitive sans changer la cyclomatique
  • Extraire des fonctions et utiliser le polymorphisme reduisent la complexité réelle
  • La complexité cognitive mesure l'effort mental, pas juste les chemins

Article précédent : 10 - Couplage et cohesion

Article suivant : 12 - Abstractions prematurees vs tardives

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.