Files
2026-10-14 12:33:53 +02:00

27 KiB
Raw Permalink Blame History

Compte rendu — TD2 : complexité cyclomatique

R5.A.04 · Qualité Algorithmique

Groupe : Ryad MEHIRI · Luka PLOUVIER · Jimmy COLOMB-RAVAT

Fichiers analysés : exercices.c (C1 à C6), Bibliotheque.java (J1 à J6) et exercices.py (P1 à P5). Comme demandé, les fonctions main et les constructeurs ne sont pas analysés.


Synthèse

Ex. Fonction Décisions M Classe
C1 carre 0 1 Simple
C2 moyenne 2 3 Simple
C3 est_bissextile 2 3 Simple
C4 nom_jour 7 8 Simple
C5 compter_mots 7 8 Simple
C6 date_valide 10 11 Modéré
J1 nombreDeLivres 0 1 Simple
J2 estDisponible 2 3 Simple
J3 peutEmprunter 8 9 Simple
J4 calculerPenalite 8 9 Simple
J5 rechercher 10 11 Modéré
J6 importer 7 8 Simple
P1 celsius_vers_fahrenheit 0 1 Simple
P2 signe 2 3 Simple
P3 notes_valides 3 4 Simple
P4 mention 2 3 Simple
P5 analyser_mot_de_passe 16 17 Modéré

La fonction la plus complexe est P5 analyser_mot_de_passe (M = 17). Nous l'avons découpée en 7 fonctions qui ont toutes M ≤ 4 (voir la partie 6).

Pour P3 et P5, nous comptons la comparaison chaînée de Python (0 <= n <= 20) comme une décision. Un comptage par mots-clés ne la voit pas et donne 3 et 16 (voir la partie 5).


1. Méthode

Nous avons utilisé deux méthodes et vérifié qu'elles donnent le même résultat :

  • Graphe de flot de contrôle : M = E − N + 2P, avec E le nombre d'arêtes, N le nombre de nœuds et P le nombre de composantes connexes. On a P = 1 pour une fonction, donc M = E − N + 2.
  • Comptage des décisions : M = décisions + 1.

Ce qui compte comme une décision

Ajoute +1 Ajoute 0
if, else if / elif else, default
for, for-each, while, do … while return, break, continue
chaque case try, finally
chaque catch / except appel d'une autre fonction (ses décisions comptent dans son propre M)
chaque &&, ||, and, or négation ! / not
ternaire a ? b : c / b if a else c
for et if d'une compréhension Python
comparaison chaînée Python a <= b <= c (un and implicite)

Règles de construction des graphes (diapo 31 du cours)

  1. Un nœud est un bloc d'instructions sans branchement. Les instructions qui précèdent une condition sont regroupées avec elle.
  2. Un nœud de décision a deux sorties (une sortie par case pour un switch). Une boucle ajoute une arête de retour.
  3. Tous les return mènent à un seul nœud de fin.
  4. Chaque && / || est une décision séparée, à cause de l'évaluation paresseuse.

2. Graphes de flot de contrôle (C2, C4, J2, J6, P2, P3)

Nous avons dessiné les six graphes à la main :

Graphes de flot de contrôle de C2, C4, J2, J6, P2 et P3, dessinés à la main

Les calculs ne sont pas sur la feuille : ils sont détaillés ci-dessous, graphe par graphe. Chaque sous-partie donne :

  • le code, avec à droite de chaque ligne le numéro de son nœud sur le dessin ;
  • la liste des arêtes, avec entre parenthèses la condition qui fait suivre chaque arête sortant d'une décision ;
  • le calcul M = E − N + 2P (avec P = 1), puis la vérification par le nombre de décisions.

Récapitulatif

Graphe Fonction N (nœuds) E (arêtes) M = E − N + 2 Vérification : décisions + 1
C2 moyenne 7 8 3 2 + 1 = 3 ✔
C4 nom_jour 10 16 8 7 + 1 = 8 ✔
J2 estDisponible 4 5 3 2 + 1 = 3 ✔
J6 importer 12 18 8 7 + 1 = 8 ✔
P2 signe 6 7 3 2 + 1 = 3 ✔
P3 notes_valides 6 8 4 3 + 1 = 4 ✔

C2 — moyenne

int moyenne(const int *t, int n)
{
    int somme = 0;                       // 1
    for (int i = 0; i < n; i++)          // 1 (i = 0) · 2 (i < n) · 3 (i++)
        somme += t[i];                   // 3
    return n > 0 ? somme / n : 0;        // 4 (n > 0) · 5 (somme / n) · 6 (0) · 7 (return)
}
  • Nœuds (7) : 1 à 7. Les nœuds de décision sont 2 (i < n) et 4 (n > 0).
  • Arêtes (8) : 1→2, 2→3 (i < n), 3→2 (retour de boucle), 2→4 (sinon), 4→5 (n > 0), 4→6 (sinon), 5→7, 6→7
  • Calcul : M = E − N + 2P = 8 − 7 + 2 × 1 = 3
  • Vérification : 2 décisions (for, ?:) + 1 = 3 ✔

Remarque : comme dans l'exemple signe_somme du cours, un des 3 chemins de base est impossible à l'exécution.

  • Chemin 1 : 1→2→4→6→7, testé avec n = 0 (résultat 0).
  • Chemin 2 : 1→2→3→2→4→5→7, testé avec t = {12}, n = 1 (résultat 12).
  • Chemin 3 : 1→2→4→5→7, impossible : si on n'entre pas dans la boucle, alors n ≤ 0 et n > 0 est forcément faux.

Les deux décisions dépendent l'une de l'autre, ce que le graphe ne voit pas. Deux tests suffisent donc à couvrir toutes les arêtes.

C4 — nom_jour

const char *nom_jour(int jour)
{
    switch (jour) {                      // 1
    case 1: return "lundi";              // 2
    case 2: return "mardi";              // 3
    case 3: return "mercredi";           // 4
    case 4: return "jeudi";              // 5
    case 5: return "vendredi";           // 6
    case 6: return "samedi";             // 7
    case 7: return "dimanche";           // 8
    default: return "inconnu";           // 9
    }
}                                        // 10 (fin)
  • Nœuds (10) : 1 à 10. Le seul nœud de décision est 1 (le switch), qui a 8 sorties.
  • Arêtes (16) : 1→2 (case 1), 1→3 (case 2), 1→4 (case 3), 1→5 (case 4), 1→6 (case 5), 1→7 (case 6), 1→8 (case 7), 1→9 (default), puis 2→10, 3→10, 4→10, 5→10, 6→10, 7→10, 8→10, 9→10
  • Calcul : M = E − N + 2P = 16 − 10 + 2 × 1 = 8
  • Vérification : 7 case + 1 = 8 ✔. Le default est le « sinon » du switch et ne compte pas.

J2 — estDisponible

return !livre.emprunte          // 1
    && !livre.reserve           // 2
    && !livre.abime;            // 3 (valeur renvoyée) · 4 (fin)

Il n'y a pas de if, mais chaque && crée un branchement : si !livre.emprunte est faux, la suite n'est pas évaluée et la méthode renvoie directement false. On construit le graphe comme celui de est_bissextile dans le cours.

  • Nœuds (4) : 1 à 4. Les nœuds de décision sont 1 (!livre.emprunte) et 2 (!livre.reserve).
  • Arêtes (5) : 1→2 (!emprunte vrai), 1→4 (sinon : renvoie false), 2→3 (!reserve vrai), 2→4 (sinon : renvoie false), 3→4 (renvoie !abime)
  • Calcul : M = E − N + 2P = 5 − 4 + 2 × 1 = 3
  • Vérification : 2 && + 1 = 3 ✔

J6 — importer

int importes = 0;                                          // 1
for (String ligne : lignes) {                              // 2
    if (ligne.isBlank() || ligne.startsWith("#"))          // 3 (isBlank) · 4 (startsWith)
        continue;
    String[] champs = ligne.split(";");                    // 5
    if (champs.length != 4) {                              // 5
        System.err.println("Ligne ignorée : " + ligne);    // 6
        continue;
    }
    try {
        int annee = Integer.parseInt(champs[3].trim());    // 7 (peut lever l'exception)
        if (annee > 0 && annee <= 2026) {                  // 8 (annee > 0) · 9 (annee <= 2026)
            livres.add(new Livre(...));                    // 10
            importes++;                                    // 10
        }
    } catch (NumberFormatException e) {
        System.err.println("Année invalide : " + champs[3]);   // 11
    }
}
return importes;                                           // 12
  • Nœuds (12) : 1 à 12. Les nœuds de décision sont 2, 3, 4, 5, 7, 8 et 9.
  • Arêtes (18) :
    • descente principale : 1→2, 2→3 (ligne suivante), 3→4 (sinon), 4→5 (sinon), 5→7 (sinon), 7→8 (pas d'exception), 8→9 (annee > 0), 9→10 (annee <= 2026) ;
    • sorties sur le côté : 2→12 (plus de ligne), 5→6 (champs.length != 4), 7→11 (NumberFormatException) ;
    • retours vers le nœud 2 : 3→2 (isBlank(), continue), 4→2 (startsWith("#"), continue), 6→2 (continue), 8→2 (sinon), 9→2 (sinon), 10→2 (fin du tour), 11→2 (fin du tour).
  • Calcul : M = E − N + 2P = 18 − 12 + 2 × 1 = 8
  • Vérification : 7 décisions (for-each, if, ||, if, if, &&, catch) + 1 = 8 ✔. On retrouve les 7 nœuds de décision du graphe.

Le nœud 7 est une décision parce que Integer.parseInt a deux sorties : la suite normale du try ou le saut vers le catch. C'est cette arête exceptionnelle (7→11) que compte le +1 du catch.

P2 — signe

def signe(x):
    if x > 0:          # 1
        return 1       # 2
    elif x < 0:        # 3
        return -1      # 4
    return 0           # 5      (fin : 6)
  • Nœuds (6) : 1 à 6. Les nœuds de décision sont 1 (x > 0) et 3 (x < 0).
  • Arêtes (7) : 1→2 (x > 0), 1→3 (sinon), 3→4 (x < 0), 3→5 (sinon), 2→6, 4→6, 5→6
  • Calcul : M = E − N + 2P = 7 − 6 + 2 × 1 = 3
  • Vérification : if + elif + 1 = 3 ✔. Trois tests suffisent : x = 5, x = −3 et x = 0.

P3 — notes_valides

def notes_valides(notes):
    return [n for n in notes if 0 <= n <= 20]

La compréhension est une boucle for avec un filtre if, écrits sur une seule ligne. La condition 0 <= n <= 20 équivaut à 0 <= n and n <= 20 : si 0 <= n est faux, Python n'évalue pas n <= 20. Cela fait donc deux décisions, comme pour un and (règle 4).

Comme tout tient sur une ligne, voici ce que représente chaque nœud :

Nœud Rôle
1 création de la liste résultat (vide)
2 for n in notes : reste-t-il un élément ?
3 0 <= n
4 n <= 20
5 ajout de n à la liste
6 return de la liste (fin)
  • Nœuds (6) : 1 à 6. Les nœuds de décision sont 2, 3 et 4.
  • Arêtes (8) : 1→2, 2→3 (élément suivant), 2→6 (plus d'élément), 3→4 (0 <= n), 3→2 (sinon), 4→5 (n <= 20), 4→2 (sinon), 5→2
  • Calcul : M = E − N + 2P = 8 − 6 + 2 × 1 = 4
  • Vérification : for + if + and implicite + 1 = 4 ✔

Si on ne découpe pas la comparaison chaînée (en fusionnant les nœuds 3 et 4), on obtient N = 5, E = 6 et M = 3. C'est la valeur que donne un comptage par mots-clés.


3. Comptage des décisions (toutes les fonctions)

3.1 exercices.c

Ex. Fonction Points de décision Décisions M
C1 carre aucun 0 1
C2 moyenne for, ?: 2 3
C3 est_bissextile &&, || 2 3
C4 nom_jour 7 case (default ne compte pas) 7 8
C5 compter_mots while, 4 ||, if, else if 7 8
C6 date_valide if + 3 ||, if, if, else if + 3 || 10 11

C3 donne le même graphe que l'exemple du cours (N = 4, E = 5, M = 3). Les deux opérateurs logiques sont des décisions, même sans if.

C5 compter_mots : M = 8

while (texte[i] != '\0') {                                                       // +1 while
    char c = texte[i];
    int separateur = (c == ' ' || c == '\t' || c == '\n' || c == ',' || c == '.'); // +4 (4 ||)
    if (separateur) {                                                            // +1 if
        dans_mot = 0;
    } else if (!dans_mot) {                                                      // +1 else if
        dans_mot = 1;
        mots++;
    }
    i++;
}

On a 1 + 4 + 1 + 1 = 7 décisions, donc M = 8. Les 4 || sont dans une affectation et non dans un if, mais ils comptent quand même.

C6 date_valide : M = 11

if (annee < 1 || mois < 1 || mois > 12 || jour < 1)              // +1 if, +3 ||
    return 0;
if (mois == 2) {                                                  // +1 if
    if (est_bissextile(annee))                                    // +1 if (l'appel lui-même : 0)
        max = 29;
    else                                                          // 0
        max = 28;
} else if (mois == 4 || mois == 6 || mois == 9 || mois == 11) {   // +1 else if, +3 ||
    max = 30;
} else {                                                          // 0
    max = 31;
}
return jour <= max;                                               // 0 : simple comparaison renvoyée

On a 4 + 1 + 1 + 4 = 10 décisions, donc M = 11. Les 3 décisions de est_bissextile sont comptées dans C3 et pas ici.

3.2 Bibliotheque.java

Ex. Méthode Points de décision Décisions M
J1 nombreDeLivres aucun 0 1
J2 estDisponible 2 && 2 3
J3 peutEmprunter 4 if, ?:, 2 else if, && 8 9
J4 calculerPenalite if + ||, if, else if, for, if, if, ?: 8 9
J5 rechercher for-each, if + &&, if, if + &&, if + ||, if + || 10 11
J6 importer for-each, if + ||, if, if + &&, catch 7 8

J3 peutEmprunter : M = 9

if (!a.cotisationAJour)                  // +1
    return "Cotisation à renouveler";
if (a.joursDeRetardCumules > 30)         // +1
    return "Trop de retards";
int maxEmprunts = a.etudiant ? 8 : 5;    // +1 (ternaire)
if (a.nbEmprunts >= maxEmprunts)         // +1
    return "Nombre maximum d'emprunts atteint";
if (l.emprunte)                          // +1
    return "Livre déjà emprunté";
else if (l.reserve && !a.etudiant)       // +1 else if, +1 &&
    return "Livre réservé";
else if (l.abime)                        // +1
    return "Livre en réparation";
return "OK";

On a 8 décisions, donc M = 9. Le ternaire est facile à oublier parce qu'il est caché dans une affectation.

J4 calculerPenalite : M = 9

if (joursRetard <= 0 || nbLivres <= 0)   // +1 if, +1 ||
    return 0;
if (joursRetard <= 7)                    // +1
    tarif = 0.20;
else if (joursRetard <= 30)              // +1
    tarif = 0.50;
else                                     // 0
    tarif = 1.00;
for (int i = 0; i < nbLivres; i++) {     // +1
    penalite += joursRetard * tarif;
    if (i >= 2)                          // +1
        penalite += 1.0;
}
if (a.etudiant)                          // +1
    penalite *= 0.5;
return penalite > 50 ? 50 : penalite;    // +1 (ternaire)

On a 8 décisions, donc M = 9.

J5 rechercher : M = 11

for (Livre l : livres) {                                         // +1 for-each
    if (genre != null && !l.genre.equals(genre))                 // +1 if, +1 &&
        continue;
    if (l.annee < anneeMin)                                      // +1
        continue;
    if (disponiblesSeulement && !estDisponible(l))               // +1 if, +1 && (l'appel : 0)
        continue;
    if (motCle == null || motCle.isEmpty()) {                    // +1 if, +1 ||
        resultats.add(l);
    } else {                                                     // 0
        String m = motCle.toLowerCase();
        if (l.titre.toLowerCase().contains(m)
                || l.auteur.toLowerCase().contains(m))           // +1 if, +1 ||
            resultats.add(l);
    }
}

On a 1 + 2 + 1 + 2 + 2 + 2 = 10 décisions, donc M = 11.

Le détail de J6 est donné avec son graphe dans la partie 2.

3.3 exercices.py

Ex. Fonction Points de décision Décisions M
P1 celsius_vers_fahrenheit aucun 0 1
P2 signe if, elif 2 3
P3 notes_valides for et if de la compréhension, comparaison chaînée 3 4
P4 mention for, if 2 3
P5 analyser_mot_de_passe voir le détail dans la partie 6.1 16 17

P4 mention : sans le dictionnaire, il aurait fallu une cascade de 4 if/elif, soit M = 5. Le dictionnaire parcouru dans une boucle ramène M à 3. C'est la technique « remplacer les if en cascade par un tableau » du cours (diapo 38). Attention : la fonction dépend de l'ordre d'insertion du dictionnaire (16, 14, 12, 10), qui est garanti depuis Python 3.7.


4. Classement

Nous utilisons la grille du cours : 1–10 simple, 11–20 modéré, 21–50 complexe, > 50 très risqué.

Classe Fonctions
Simple (1–10) C1 (1), C2 (3), C3 (3), C4 (8), C5 (8), J1 (1), J2 (3), J3 (9), J4 (9), J6 (8), P1 (1), P2 (3), P3 (4), P4 (3)
Modéré (11–20) C6 date_valide (11), J5 rechercher (11), P5 analyser_mot_de_passe (17)
Complexe (21–50) aucune
Très risqué (> 50) aucune

Analyse :

  • Aucune fonction ne dépasse 20. Les trois fonctions modérées dépassent le seuil de McCabe (10). Ce sont elles qu'il faudrait revoir en priorité, en commençant par P5, qui est loin devant les autres.
  • J3 et J4 (M = 9) sont juste sous le seuil. Une condition de plus suffirait à les faire passer en « modéré ».
  • Le tableau du sujet (diapo 46) indique la difficulté de l'exercice, pas le classement de la grille. J3, J4 et J6 y sont marqués « complexes » parce qu'ils contiennent des pièges (ternaires, catch, boucles), mais leur M reste ≤ 10.
  • C4 nom_jour (M = 8) montre une limite de la mesure : un switch qui associe un numéro à un nom se lit d'un coup d'œil, mais il obtient le même M que compter_mots, qui est bien plus difficile à suivre.

5. Cas particuliers

Construction Où Compte Justification
&& / || sans if C3, J2, C5 (separateur) +1 chacun Avec l'évaluation paresseuse, le second opérande n'est évalué que dans certains cas. Il y a donc un branchement dans le graphe, même si l'expression est dans un return ou une affectation (voir le graphe de J2).
Ternaire ?: / b if a else c C2, J3, J4, P5 +1 C'est un if/else à l'intérieur d'une expression, avec deux sorties.
switch / case C4 +1 par case Chaque case est une sortie du nœud de décision. Le default est le « sinon » et ne compte pas. Ici, les return remplacent les break et rejoignent tous le nœud de fin. Le cours note la limite : un switch lisible obtient un M élevé. Certaines variantes (complexité cyclomatique « modifiée », complexité cognitive) comptent le switch une seule fois, ce qui donnerait M = 2.
Compréhension Python P3 (et P4, P5 pour les boucles) +1 pour le for, +1 par if [n for n in notes if cond] est une boucle avec un filtre écrite sur une ligne. Le graphe est le même que celui d'une boucle for qui contient un if.
Comparaison chaînée P3, P5 +1 a <= b <= c équivaut à a <= b and b <= c. Si le premier test est faux, le second n'est pas évalué, ce qui crée une décision cachée. Nous la comptons pour rester cohérents avec la règle 4. Sans elle, on aurait P3 = 3 et P5 = 16.
catch / except J6 +1 par catch Le catch ajoute une sortie exceptionnelle au bloc try (arête 7→11 dans le graphe de J6). Le try ne compte pas car il n'y a pas de choix à l'entrée. Un finally ne compterait pas non plus car il est toujours exécuté.
for-each J5, J6, P4, P5 +1 À chaque tour, on teste s'il reste un élément, comme dans une boucle for.
break, continue, return J5, J6, P5, … 0 Ce sont des sauts sans choix. La décision est dans le if qui les précède.
else, default C6, J4, J5, P5 0 C'est la sortie « sinon » d'une décision déjà comptée.
Appel d'une fonction C6 (est_bissextile), J5 (estDisponible) 0 M se calcule fonction par fonction. Les décisions de la fonction appelée sont dans son propre M.
Négation ! / not J2, J3, J5, P5 0 Elle inverse une valeur sans créer de branchement.
Dictionnaire de seuils P4 — Les données remplacent des décisions. La logique reste la même, mais M passe de 5 à 3.

6. Refactorisation de P5 analyser_mot_de_passe

6.1 Version d'origine : M = 17

def analyser_mot_de_passe(mdp, login=None):
    problemes = []
    if len(mdp) < 12:                                   # +1
        problemes.append("trop court")
    majuscule = minuscule = chiffre = special = False
    for c in mdp:                                       # +1
        if c.isupper():                                 # +1
            majuscule = True
        elif c.islower():                               # +1
            minuscule = True
        elif c.isdigit():                               # +1
            chiffre = True
        else:                                           # 0
            special = True
    if not (majuscule and minuscule):                   # +1 if, +1 and
        problemes.append("mélanger majuscules et minuscules")
    if not chiffre:                                     # +1
        problemes.append("ajouter un chiffre")
    if not special:                                     # +1
        problemes.append("ajouter un caractère spécial")
    if login and login.lower() in mdp.lower():          # +1 if, +1 and
        problemes.append("ne pas contenir le login")
    for i in range(len(mdp) - 2):                       # +1
        if mdp[i] == mdp[i + 1] == mdp[i + 2]:          # +1 if, +1 comparaison chaînée
            problemes.append("éviter 3 caractères identiques")
            break                                       # 0
    if not problemes:                                   # +1
        return "fort", []
    return ("moyen" if len(problemes) <= 2 else "faible"), problemes   # +1 (ternaire)

On a 16 décisions, donc M = 17. La fonction est classée modérée et dépasse largement le seuil de 10.

Ce qui pose problème :

  • La fonction fait quatre choses à la fois : classer les caractères, vérifier six règles, détecter les répétitions et calculer le niveau.
  • Les quatre drapeaux booléens (majuscule, minuscule, chiffre, special) relient le début et la fin de la fonction. Pour comprendre un message, il faut suivre tout le code.
  • Les six règles sont écrites comme une suite de if qui ajoutent chacun un message. C'est le cas typique de « if en cascade ».
  • Les valeurs 12 et 2 sont des nombres magiques.

6.2 Techniques appliquées (diapo 38 du cours)

  1. Extraire des fonctions pour donner une seule responsabilité à chacune : _categorie, _categories, _contient_login, _trois_identiques, _lister_problemes, _niveau.
  2. Remplacer les if en cascade par un tableau : les six règles deviennent un tuple de paires (échec ?, message), filtré par une seule compréhension.
  3. Remplacer les drapeaux par un ensemble : _categories(mdp) renvoie l'ensemble des catégories présentes, par exemple {"majuscule", "chiffre"}.
  4. Supprimer la décision cachée : mdp[i:i + 3] == mdp[i] * 3 remplace la comparaison chaînée par une seule comparaison.
  5. Nommer les constantes : LONGUEUR_MIN = 12 et MAX_PROBLEMES_MOYEN = 2.

6.3 Nouvelle version (exercices_refactorise.py)

LONGUEUR_MIN = 12
MAX_PROBLEMES_MOYEN = 2


def _categorie(c):                                   # M = 4
    if c.isupper():
        return "majuscule"
    if c.islower():
        return "minuscule"
    if c.isdigit():
        return "chiffre"
    return "special"


def _categories(mdp):                                # M = 2
    return {_categorie(c) for c in mdp}


def _contient_login(mdp, login):                     # M = 2
    return bool(login) and login.lower() in mdp.lower()


def _trois_identiques(mdp):                          # M = 2
    return any(mdp[i:i + 3] == mdp[i] * 3 for i in range(len(mdp) - 2))


def _lister_problemes(mdp, login):                   # M = 3
    presentes = _categories(mdp)
    controles = (
        (len(mdp) < LONGUEUR_MIN, "trop court"),
        (not {"majuscule", "minuscule"}.issubset(presentes), "mélanger majuscules et minuscules"),
        ("chiffre" not in presentes, "ajouter un chiffre"),
        ("special" not in presentes, "ajouter un caractère spécial"),
        (_contient_login(mdp, login), "ne pas contenir le login"),
        (_trois_identiques(mdp), "éviter 3 caractères identiques"),
    )
    return [message for echec, message in controles if echec]


def _niveau(problemes):                              # M = 3
    if not problemes:
        return "fort"
    return "moyen" if len(problemes) <= MAX_PROBLEMES_MOYEN else "faible"


def analyser_mot_de_passe(mdp, login=None):          # M = 1
    problemes = _lister_problemes(mdp, login)
    return _niveau(problemes), problemes

6.4 Nouveau calcul de M

Fonction Points de décision Décisions M Classe
_categorie if, if, if 3 4 Simple
_categories for de la compréhension 1 2 Simple
_contient_login and 1 2 Simple
_trois_identiques for du générateur (une seule comparaison, plus de chaînage) 1 2 Simple
_lister_problemes for et if de la compréhension 2 3 Simple
_niveau if, ternaire 2 3 Simple
analyser_mot_de_passe aucun 0 1 Simple

Bilan :

Avant Après
M de analyser_mot_de_passe 17 (modéré) 1 (simple)
M maximal d'une fonction 17 4
Somme des M 17 17 (7 fonctions)
  • La complexité n'a pas disparu, elle est répartie. La somme des M reste 17 : il faut toujours vérifier les mêmes cas. Mais chaque fonction a au plus 4 chemins et se teste seule, avec 4 tests au maximum. analyser_mot_de_passe n'a plus qu'un chemin et se lit comme sa spécification : « lister les problèmes, puis en déduire le niveau ».
  • Ajouter une règle ne change plus M : il suffit d'ajouter une ligne dans controles.
  • Le résultat de la fonction ne change pas : mêmes messages, dans le même ordre, mêmes niveaux et même type de retour (un tuple (niveau, liste)). La commande python exercices_refactorise.py compare l'ancienne et la nouvelle version sur 5 011 mots de passe (11 cas choisis à la main et 5 000 chaînes aléatoires) avec 5 logins différents (None, "", …), soit 25 055 comparaisons.

6.5 Pistes pour les deux autres fonctions modérées

  • C6 date_valide (11) : mettre le nombre de jours de chaque mois dans un tableau JOURS_PAR_MOIS[12] et extraire une fonction jours_dans_mois(mois, annee), qui ne garde que le cas de février (&& et ternaire, M = 3). date_valide ne garde alors que la validation des bornes, et la cascade mois == 4 || mois == 6 || … disparaît.
  • J5 rechercher (11) : extraire deux méthodes, respecteFiltres(l, genre, anneeMin, disponiblesSeulement) et contientMotCle(l, motCle). La boucle n'aurait plus qu'un if.