Files

553 lines
27 KiB
Markdown
Raw Permalink Normal View History

2026-10-14 12:33:53 +02:00
# 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`.