27 KiB
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, doncM = 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)
- Un nœud est un bloc d'instructions sans branchement. Les instructions qui précèdent une condition sont regroupées avec elle.
- Un nœud de décision a deux sorties (une sortie par
casepour unswitch). Une boucle ajoute une arête de retour. - Tous les
returnmènent à un seul nœud de fin. - 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 :
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_sommedu 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 ≤ 0etn > 0est 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 ✔. Ledefaultest le « sinon » duswitchet 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 (
!empruntevrai), 1→4 (sinon : renvoiefalse), 2→3 (!reservevrai), 2→4 (sinon : renvoiefalse), 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).
- descente principale : 1→2, 2→3 (ligne suivante), 3→4 (sinon), 4→5 (sinon), 5→7 (sinon), 7→8 (pas d'exception), 8→9 (
- 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 = −3etx = 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+andimplicite + 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 : unswitchqui associe un numéro à un nom se lit d'un coup d'œil, mais il obtient le même M quecompter_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
ifqui ajoutent chacun un message. C'est le cas typique de «ifen cascade ». - Les valeurs
12et2sont des nombres magiques.
6.2 Techniques appliquées (diapo 38 du cours)
- Extraire des fonctions pour donner une seule responsabilité à chacune :
_categorie,_categories,_contient_login,_trois_identiques,_lister_problemes,_niveau. - Remplacer les
ifen cascade par un tableau : les six règles deviennent un tuple de paires(échec ?, message), filtré par une seule compréhension. - Remplacer les drapeaux par un ensemble :
_categories(mdp)renvoie l'ensemble des catégories présentes, par exemple{"majuscule", "chiffre"}. - Supprimer la décision cachée :
mdp[i:i + 3] == mdp[i] * 3remplace la comparaison chaînée par une seule comparaison. - Nommer les constantes :
LONGUEUR_MIN = 12etMAX_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_passen'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 commandepython exercices_refactorise.pycompare 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 tableauJOURS_PAR_MOIS[12]et extraire une fonctionjours_dans_mois(mois, annee), qui ne garde que le cas de février (&&et ternaire, M = 3).date_validene garde alors que la validation des bornes, et la cascademois == 4 || mois == 6 || …disparaît. - J5
rechercher(11) : extraire deux méthodes,respecteFiltres(l, genre, anneeMin, disponiblesSeulement)etcontientMotCle(l, motCle). La boucle n'aurait plus qu'unif.
