Nombre magique.
Un nombre magique est un littéral sans nom dont le lecteur doit deviner le sens ; une constante nommée rend le code plus clair et plus sûr à modifier.
En programmation, un nombre magique est un littéral sans nom — le plus souvent un nombre, parfois une chaîne — écrit directement dans le code source et porteur d'une signification particulière qui n'est pas claire pour le lecteur. Comme la valeur n'est ni nommée ni expliquée, le lecteur doit deviner ce qu'elle représente, et pour la modifier sans risque il faut retrouver tous les endroits où elle a été répétée.
Le terme a aussi d'autres sens : une constante placée au début d'un fichier pour identifier son format (une signature de fichier, comme %PDF au début des documents PDF), et une valeur reconnaissable comme 0xDEADBEEF, écrite en mémoire pour faciliter le débogage. Ces usages ne sont pas des anti-patrons et sortent du cadre de cet article.
Constantes numériques sans nom
Une telle valeur est aussi appelée constante magique. Son utilisation est considérée comme un anti-modèle et enfreint l'une des plus anciennes règles de la programmation, qui remonte aux manuels de COBOL, FORTRAN et PL/1 des années 1960.
Par exemple, dans le code suivant qui calcule un prix après taxe, 1.05 est un nombre magique car cette valeur encode le taux de taxe sur les ventes, 5 %, d'une manière qui n'a rien d'évident.
price_after_tax = 1.05 * price
L'utilisation de nombres magiques dans le code masque l'intention des développeurs ayant choisi ce nombre, multiplie les occasions d'erreurs subtiles et rend plus difficile l'adaptation et l'extension du programme par la suite. Par exemple, il est difficile de dire si chaque chiffre de 3.14159265358979323846 est correctement saisi, ou si cette constante représentant pi peut être tronquée à 3.14159 sans nuire au fonctionnement du programme malgré sa précision réduite. Remplacer tous les nombres magiques significatifs par des constantes nommées (aussi appelées variables explicatives) rend les programmes plus faciles à lire, à comprendre et à maintenir.
L'exemple ci-dessus peut être amélioré en ajoutant une variable au nom descriptif :
TAX = 0.05 price_after_tax = (1.0 + TAX) * price
Un bon nom peut produire un code plus facile à comprendre pour un mainteneur qui n'est pas l'auteur d'origine, voire pour l'auteur d'origine après un certain temps. Un exemple de constante au nom peu informatif est int SIXTEEN = 16, tandis que int NUMBER_OF_BITS = 16 serait sans doute plus utile.
Les données non numériques peuvent posséder les mêmes propriétés magiques, et donc présenter les mêmes problèmes que les nombres magiques. Ainsi, déclarer const string testUserName = "John" et utiliser testUserName peut être préférable à l'utilisation directe du littéral "John".
Exemple : mélanger un jeu de cartes
Par exemple, s'il faut mélanger aléatoirement les valeurs d'un tableau représentant un jeu standard de cartes à jouer, ce pseudo-code remplit la tâche à l'aide de l'algorithme de mélange de Fisher-Yates :
for i from 1 to 52
j := i + randomInt(53 - i) - 1
a.swapEntries(i, j)
où a est un objet tableau, la fonction randomInt(x) choisit un entier aléatoire compris entre 1 et x inclus, et swapEntries(i, j) échange les i-ième et j-ième entrées du tableau. Dans l'exemple précédent, 52 et 53 sont des nombres magiques, dont le lien mutuel n'est d'ailleurs pas clair. Il est considéré comme un meilleur style de programmation d'écrire ce qui suit :
int deckSize:= 52
for i from 1 to deckSize
j := i + randomInt(deckSize + 1 - i) - 1
a.swapEntries(i, j)
Cette forme est préférable pour plusieurs raisons :
- Meilleure lisibilité. Un programmeur qui lit le premier exemple peut se demander : Que signifie le nombre 52 ici ? Pourquoi 52 ? Le programmeur peut en déduire le sens après une lecture attentive du code, mais ce n'est pas évident. Les nombres magiques deviennent particulièrement déroutants lorsque le même nombre est utilisé à des fins différentes dans une même portion de code.
- Plus facile à maintenir. Il est plus simple de modifier la valeur du nombre, car elle n'est pas dupliquée. Modifier la valeur d'un nombre magique est source d'erreurs, car la même valeur est souvent utilisée plusieurs fois à différents endroits d'un programme. De plus, lorsque deux variables ou nombres sémantiquement distincts ont la même valeur, ils peuvent être accidentellement modifiés ensemble. Pour adapter le premier exemple afin de mélanger un jeu de Tarot, qui comporte 78 cartes, un programmeur pourrait naïvement remplacer chaque occurrence de 52 dans le programme par 78. Cela poserait deux problèmes. Premièrement, on manquerait la valeur 53 à la deuxième ligne de l'exemple, ce qui ferait échouer l'algorithme d'une manière subtile. Deuxièmement, on remplacerait probablement les caractères « 52 » partout, qu'ils se rapportent ou non à la taille du jeu ou à tout autre chose, comme le nombre de semaines dans une année du calendrier grégorien, ou plus insidieusement qu'ils fassent partie d'un nombre comme « 1523 », ce qui introduirait des bogues. À l'inverse, modifier la valeur de la variable
deckSizedans le second exemple serait un changement simple, sur une seule ligne. - Encourage la documentation. L'endroit unique où la variable nommée est déclarée constitue un bon emplacement pour documenter ce que la valeur signifie et pourquoi elle a cette valeur. Avoir la même valeur dans une multitude d'endroits conduit soit à des commentaires en double (avec les problèmes qui en découlent lorsqu'on en met à jour certains en en oubliant d'autres), soit à l'absence d'un endroit unique où il soit à la fois naturel pour l'auteur d'expliquer la valeur et probable que le lecteur cherche une explication.
- Regroupe l'information. Les déclarations des variables de type « nombre magique » peuvent être placées ensemble, généralement en tête d'une fonction ou d'un fichier, ce qui facilite leur examen et leur modification.
- Détecte les fautes de frappe. Utiliser une variable (plutôt qu'un littéral) tire parti des vérifications du compilateur. Taper accidentellement « 62 » au lieu de « 52 » passerait inaperçu, tandis que taper «
dekSize» au lieu de «deckSize» entraînerait un avertissement du compilateur indiquant quedekSizen'est pas déclaré. - Réduit la saisie. Si un EDI prend en charge la complétion de code, il complétera la majeure partie du nom de la variable à partir de ses premières lettres.
- Facilite le paramétrage. Par exemple, pour généraliser l'exemple ci-dessus en une procédure qui mélange un jeu d'un nombre quelconque de cartes, il suffirait de transformer
deckSizeen paramètre de cette procédure, alors que le premier exemple nécessiterait plusieurs modifications.
function shuffle (int deckSize)
for i from 1 to deckSize
j := i + randomInt(deckSize + 1 - i) - 1
a.swapEntries(i, j)
Les inconvénients sont les suivants :
- Rompt la localité. Lorsque la constante nommée n'est pas définie près de son utilisation, cela nuit à la localité, et donc à la compréhensibilité, du code. Placer le 52 à un endroit éventuellement éloigné signifie que, pour comprendre entièrement le fonctionnement de la boucle « for » (par exemple pour estimer le temps d'exécution de la boucle), il faut retrouver la définition et vérifier qu'il s'agit bien du nombre attendu. Cela est facile à éviter (en rapprochant la déclaration) lorsque la constante n'est utilisée que dans une seule portion du code. En revanche, lorsque la constante nommée est utilisée dans des portions disparates, l'emplacement distant signale au lecteur que la même valeur apparaît à d'autres endroits du code, qui méritent peut-être aussi un coup d'œil.
- Augmente la verbosité. La déclaration de la constante ajoute une ligne. Lorsque le nom de la constante est plus long que la valeur, en particulier si plusieurs constantes de ce type apparaissent sur une même ligne, cela peut rendre nécessaire le fractionnement d'une instruction logique du code sur plusieurs lignes. Une augmentation de la verbosité peut se justifier lorsqu'il existe un risque de confusion au sujet de la constante, ou lorsqu'il est probable que la constante doive être modifiée, comme la réutilisation d'une routine de mélange pour d'autres jeux de cartes. Elle peut tout autant se justifier comme un gain d'expressivité.
- Considérations de performance. Il peut être plus lent d'évaluer l'expression
deckSize + 1à l'exécution que la valeur « 53 ». Cela dit, la plupart des compilateurs modernes recourent à des techniques telles que le repliement de constantes et l'optimisation de boucle pour résoudre l'addition lors de la compilation, de sorte qu'il n'y a généralement aucune ou qu'une pénalité de vitesse négligeable par rapport à l'utilisation de nombres magiques dans le code. Le coût du débogage et le temps nécessaire pour tenter de comprendre un code non explicite doivent en particulier être mis en balance avec le coût de calcul infime.
Usage admis
Lorsqu'un littéral numérique n'a pas de signification particulière, son utilisation n'est pas qualifiée de magique, même si ce qui constitue une signification « particulière » reste subjectif. Parmi les littéraux qui ne sont souvent pas considérés comme magiques figurent :
- L'utilisation de 0 et de 1 comme valeurs initiales ou d'incrément dans une boucle for, comme dans
for (int i = 0; i < max; i += 1)
- L'utilisation de 2 pour vérifier si un nombre est pair ou impair, comme dans
isEven = (x % 2 == 0), où%est l'opérateur modulo
- L'utilisation de littéraux simples, par exemple dans des expressions telles que
circumference = 2 * Math.PI * radius, ou pour calculer le discriminant d'une équation du second degré sous la formed = b^2 − 4*a*c
- L'utilisation de puissances de 10 pour convertir des valeurs métriques (par exemple entre grammes et kilogrammes) ou pour calculer des pourcentages et des valeurs en pour mille
- Les exposants dans des expressions telles que
(f(x) ** 2 + f(y) ** 2) ** 0.5pour√(f(x)² + f(y)²)
- Les littéraux 1 et 0 sont parfois utilisés pour représenter les valeurs booléennes vrai et faux. On pourrait soutenir qu'attribuer ces valeurs à des noms tels que TRUE et FALSE serait préférable.
- En C et C++, 0 est souvent utilisé pour signifier pointeur nul, même si la bibliothèque standard du C définit une macro
NULLet que le C++ moderne comprend un mot-clénullptr.
Exemple
Dans la méthode ci-dessous, 1.2, 30 et 0.015 encodent des règles métier — la TVA, un délai de grâce et des pénalités de retard — et le lecteur doit deviner lequel est lequel. Pire encore, 30 apparaît deux fois avec deux sens différents.
class Billing {
static double total(double net, int daysLate) {
double total = net * 1.2;
if (daysLate > 30) {
total += total * 0.015 * (daysLate / 30);
}
return total;
}
}
Chaque valeur porte désormais un nom, et le délai de grâce et la période de facturation peuvent évoluer indépendamment (voir Remplacer un nombre magique par une constante symbolique).
class Billing {
static final double VAT_RATE = 0.20;
static final int GRACE_PERIOD_DAYS = 30;
static final int DAYS_PER_BILLING_PERIOD = 30;
static final double LATE_FEE_RATE_PER_PERIOD = 0.015;
static double total(double net, int daysLate) {
double total = net * (1 + VAT_RATE);
if (daysLate > GRACE_PERIOD_DAYS) {
int overduePeriods = daysLate / DAYS_PER_BILLING_PERIOD;
total += total * LATE_FEE_RATE_PER_PERIOD * overduePeriods;
}
return total;
}
}
Adapté de l’article Wikipédia “Magic number (programming)” (révision 1355218563), disponible sous licence CC BY-SA 4.0 ; le texte a été traduit et modifié, et des exemples ont été ajoutés.