Files
2026_TD2_DEV51_Qualite_Algo/COMPTE_RENDU.md
T
2026-10-14 12:33:53 +02:00

553 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`.