Número mágico.
Un número mágico es un literal sin nombre en el código cuyo significado hay que adivinar; una constante con nombre hace el código más claro y seguro de cambiar.
En programación, un número mágico es un literal sin nombre —normalmente un número, a veces una cadena— que aparece directamente en el código fuente y tiene un significado especial que no resulta claro para quien lo lee. Como el valor no tiene nombre ni explicación, el lector debe adivinar qué representa, y para cambiarlo con seguridad hay que encontrar todos los lugares donde se repite.
El término también se usa en otros sentidos: una constante almacenada al comienzo de un archivo para identificar su formato (una firma de archivo, como %PDF al principio de los documentos PDF) y un valor reconocible, como 0xDEADBEEF, que se escribe en memoria para facilitar la depuración. Esos usos no son antipatrones y quedan fuera del alcance de este artículo.
Constantes numéricas sin nombre
Un valor así también se llama constante mágica. Usarlo se considera un antipatrón e infringe una de las reglas más antiguas de la programación, que se remonta a los manuales de COBOL, FORTRAN y PL/1 de la década de 1960.
Por ejemplo, en el siguiente código que calcula un precio después de impuestos, 1.05 es un número mágico, ya que el valor codifica la tasa del impuesto sobre las ventas, el 5%, de una forma poco evidente.
price_after_tax = 1.05 * price
El uso de números mágicos en el código oculta la intención de los desarrolladores al elegir ese número, aumenta las posibilidades de cometer errores sutiles y dificulta que el programa pueda adaptarse y ampliarse en el futuro. Por ejemplo, resulta difícil saber si todos los dígitos de 3.14159265358979323846 están escritos correctamente, o si esta constante de pi puede truncarse a 3.14159 sin que su menor precisión afecte a la funcionalidad del programa. Sustituir todos los números mágicos significativos por constantes con nombre (también llamadas variables explicativas) hace que los programas sean más fáciles de leer, comprender y mantener.
El ejemplo anterior puede mejorarse añadiendo una variable con un nombre descriptivo:
TAX = 0.05 price_after_tax = (1.0 + TAX) * price
Un buen nombre puede dar lugar a un código que resulte más fácil de entender para quien lo mantiene y no es el autor original, e incluso para el propio autor original al cabo de un tiempo. Un ejemplo de una constante con un nombre poco informativo es int SIXTEEN = 16, mientras que int NUMBER_OF_BITS = 16 podría resultar más útil.
Los datos no numéricos pueden tener las mismas propiedades mágicas y, por tanto, los mismos problemas que los números mágicos. Así pues, declarar const string testUserName = "John" y utilizar testUserName podría ser mejor que emplear directamente el literal "John".
Ejemplo: barajar un mazo de cartas
Por ejemplo, si se necesita barajar aleatoriamente los valores de un arreglo que representa una baraja estándar de naipes, este pseudocódigo realiza la tarea utilizando el algoritmo de barajado de Fisher–Yates:
for i from 1 to 52
j := i + randomInt(53 - i) - 1
a.swapEntries(i, j)
donde a es un objeto de tipo arreglo, la función randomInt(x) elige un entero aleatorio entre 1 y x, ambos incluidos, y swapEntries(i, j) intercambia las entradas i-ésima y j-ésima del arreglo. En el ejemplo anterior, 52 y 53 son números mágicos que, además, no guardan una relación clara entre sí. Se considera un mejor estilo de programación escribir lo siguiente:
int deckSize:= 52
for i from 1 to deckSize
j := i + randomInt(deckSize + 1 - i) - 1
a.swapEntries(i, j)
Esto es preferible por varias razones:
- Mayor legibilidad. Un programador que lea el primer ejemplo podría preguntarse: ¿Qué significa aquí el número 52? ¿Por qué 52? El programador podría deducir el significado tras leer el código con atención, pero no resulta evidente. Los números mágicos resultan especialmente confusos cuando el mismo número se usa con distintos fines en una misma sección de código.
- Más fácil de mantener. Resulta más sencillo modificar el valor del número, ya que no está duplicado. Cambiar el valor de un número mágico es propenso a errores, porque el mismo valor suele utilizarse varias veces en distintos lugares de un programa. Además, cuando dos variables o números semánticamente distintos tienen el mismo valor, podrían editarse ambos por accidente al mismo tiempo. Para modificar el primer ejemplo y barajar una baraja del Tarot, que tiene 78 cartas, un programador podría, de forma ingenua, sustituir cada aparición de 52 en el programa por 78. Esto provocaría dos problemas. En primer lugar, pasaría por alto el valor 53 de la segunda línea del ejemplo, lo que haría que el algoritmo fallara de forma sutil. En segundo lugar, probablemente reemplazaría los caracteres «52» en todas partes, con independencia de si se refieren al tamaño de la baraja o a algo completamente distinto, como el número de semanas de un año del calendario gregoriano o, de forma más insidiosa, si forman parte de un número como «1523», todo lo cual introduciría errores. En cambio, cambiar el valor de la variable
deckSizeen el segundo ejemplo sería una modificación sencilla de una sola línea. - Fomenta la documentación. El único lugar donde se declara la variable con nombre constituye un buen sitio para documentar qué significa el valor y por qué tiene el valor que tiene. Tener el mismo valor en multitud de lugares conduce a comentarios duplicados (con los consiguientes problemas al actualizar algunos y olvidar otros) o no deja ningún único lugar en el que resulte natural que el autor explique el valor y donde sea probable que el lector busque una explicación.
- Agrupa la información. Las declaraciones de las variables de «número mágico» pueden colocarse juntas, normalmente al principio de una función o de un archivo, lo que facilita su revisión y modificación.
- Detecta erratas. Usar una variable (en lugar de un literal) aprovecha la comprobación del compilador. Escribir accidentalmente «62» en vez de «52» pasaría inadvertido, mientras que escribir «
dekSize» en lugar de «deckSize» haría que el compilador advirtiera de quedekSizeno está declarada. - Reduce la escritura. Si un IDE admite el autocompletado de código, rellenará la mayor parte del nombre de la variable a partir de las primeras letras.
- Facilita la parametrización. Por ejemplo, para generalizar el ejemplo anterior en un procedimiento que baraje una baraja con cualquier número de cartas, bastaría con convertir
deckSizeen un parámetro de dicho procedimiento, mientras que el primer ejemplo requeriría varios cambios.
function shuffle (int deckSize)
for i from 1 to deckSize
j := i + randomInt(deckSize + 1 - i) - 1
a.swapEntries(i, j)
Las desventajas son:
- Rompe la localidad. Cuando la constante con nombre no se define cerca de donde se usa, se perjudica la localidad y, por tanto, la comprensibilidad del código. Colocar el 52 en un lugar posiblemente lejano implica que, para entender por completo el funcionamiento del bucle «for» (por ejemplo, para estimar el tiempo de ejecución del bucle), hay que localizar la definición y comprobar que es el número esperado. Esto es fácil de evitar (reubicando la declaración) cuando la constante solo se usa en una parte del código. Cuando la constante con nombre se utiliza en partes dispares, en cambio, la ubicación remota es una pista para el lector de que el mismo valor aparece en otros lugares del código, que también podría merecer la pena revisar.
- Provoca verbosidad. La declaración de la constante añade una línea. Cuando el nombre de la constante es más largo que el valor, en especial si varias de esas constantes aparecen en una misma línea, puede que sea necesario dividir una sentencia lógica del código en varias líneas. Un aumento de la verbosidad puede estar justificado cuando existe cierta probabilidad de confusión respecto a la constante, o cuando es probable que la constante deba cambiarse, como en la reutilización de una rutina de barajado para otros juegos de cartas. Igualmente puede justificarse como un aumento de la expresividad.
- Consideraciones de rendimiento. Procesar la expresión
deckSize + 1en tiempo de ejecución puede ser más lento que el valor «53». Dicho esto, la mayoría de los compiladores modernos utilizan técnicas como el plegado de constantes y la optimización de bucles para resolver la suma durante la compilación, por lo que normalmente no hay penalización de velocidad, o es insignificante, en comparación con el uso de números mágicos en el código. Sobre todo, el coste de la depuración y el tiempo necesario para intentar comprender un código no explicativo deben sopesarse frente al ínfimo coste del cálculo.
Uso aceptado
Cuando un literal numérico carece de un significado especial, su uso no se clasifica como mágico, aunque lo que se considera «especial» es subjetivo. Entre los ejemplos de literales que a menudo no se consideran mágicos se incluyen:
- Uso de 0 y 1 como valores iniciales o de incremento en un bucle for, como
for (int i = 0; i < max; i += 1)
- Uso del 2 para comprobar si un número es par o impar, como en
isEven = (x % 2 == 0), donde%es el operador módulo
- Uso de literales sencillos, p. ej., en expresiones como
circumference = 2 * Math.PI * radius, o para calcular el discriminante de una ecuación cuadrática comod = b^2 − 4*a*c
- Uso de potencias de 10 para convertir valores métricos (por ejemplo, entre gramos y kilogramos) o para calcular valores porcentuales y por mil
- Exponentes en expresiones como
(f(x) ** 2 + f(y) ** 2) ** 0.5para√(f(x)² + f(y)²)
- Los literales 1 y 0 se utilizan a veces para representar los valores booleanos verdadero y falso. Es discutible, pero asignar estos valores a nombres como TRUE y FALSE podría ser mejor.
- En C y C++, a menudo se usa 0 para significar puntero nulo, aunque la biblioteca estándar de C define una macro
NULLy el C++ moderno incluye una palabra clavenullptr.
Ejemplo
En el método siguiente, 1.2, 30 y 0.015 codifican reglas de negocio —el IVA, un periodo de gracia y un recargo por demora— y el lector tiene que adivinar cuál es cuál. Peor aún, 30 aparece dos veces con dos significados distintos.
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;
}
}
Ahora cada valor tiene nombre, y el periodo de gracia y el periodo de facturación pueden cambiar de forma independiente (véase Reemplazar número mágico por constante simbólica).
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;
}
}
Adaptado del artículo de Wikipedia “Magic number (programming)” (revisión 1355218563), disponible bajo la licencia CC BY-SA 4.0; el texto ha sido traducido y editado, y se han añadido ejemplos.