# Compte rendu — TD2 : complexité cyclomatique **R5.A.04 · Qualité Algorithmique** **Groupe :** Ryad MEHIRI · Luka PLOUVIER · Jimmy COLOMB-RAVAT Fichiers analysés : [`exercices.c`](exercices.c) (C1 à C6), [`Bibliotheque.java`](Bibliotheque.java) (J1 à J6) et [`exercices.py`](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](#6-refactorisation-de-p5-analyser_mot_de_passe)). 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](#5-cas-particuliers)). --- ## 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](graphes/graphes_a_la_main.webp) 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` ```c 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` ```c 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` ```java 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` ```java 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` ```python 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` ```python 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** ```c 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** ```c 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** ```java 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** ```java 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** ```java 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 ```python 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`](exercices_refactorise.py)) ```python 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`.