La Programación no es trabajo ni para cobardes ni para desmemoriados. La máquina es una bestia salvaje atada por una cadena: si queremos ganarnos la vida con ella, tenemos que despojarnos del miedo irracional. Pero también tenemos que recordar, en todo momento, cuál es la longitud de la puñetera cadena.
Cuando hablo de "cadena", usted debe entender "contrato": cada pieza de software debe dejar bien claro qué es lo que hace por nosotros, siempre que le proporcionemos lo que especifica el contrato. Math.Sqrt nos calculará la raíz cuadrada de un número, siempre que le proporcionemos un número no negativo. El indexador de la clase genérica Dictionary nos devolverá el valor asociado a una clave, aunque sólo cuando la clave se haya almacenado antes. Ignorar la existencia de estos contratos nos puede llevar a uno de dos problemas:
- Pedimos la raíz cuadrada de menos uno, y el león nos come una oreja.
- El problema anterior suele pasar una sola vez. El aprendiz de domador suele quedar aterrorizado, y de ahí en adelante no se acerca al león, excepto cuando su abogado está presente (para que se coma al león en caso necesario). El abogado, por desgracia, termina siendo un problema mayor que el propio león.
A continuación, voy a presentarle varias joyas que muestran los problemas que se pueden presentar cuando el programador decide ignorar, por excesivo terror, las garantías que le ofrecen los contratos. El problema con estas "joyas" es que, cuando uno se las encuentra, pierde un tiempo valioso intentando decidir si:
- Se trata de una genialidad.
- O se trata de una simple cagada.
El 99% de los casos corresponde a la segunda opción, pero uno se niega a perder tan pronto la fe en el género humano.
La primera perla negra tiene que ver, como no podía ser de otro modo, con la ya mencionada clase Dictionary. Estoy harto de ver este fragmento de código en el software de cierta empresa imaginaria:
if (dict.ContainsKey(key))
dict[key] = value;
else
dict.Add(key, value);
Esto podría pasar como buena programación en
Ankh-Morpork, pero aquí solemos ser un poco más exigentes. Un buen día, uno de los leones, el método
Add, se adelantó al gato y le comió la lengua al programador, cuando éste intentó insertar una clave duplicada en un diccionario. De ese día en adelante, el programador pincha primero el diccionario con una vara larga, para ver si ya existe la clave. En caso afirmativo,
actualiza el valor asociado a la clave; de lo contrario, inserta el nuevo par clave/valor. El problema es que el mismo resultado se obtiene con menos charla:
dict[key] = value;
En efecto, cuando se realiza una asignación sobre el indizador un diccionario, si exista la clave, se actualiza el valor; de lo contrario, se inserta. Hay dos posibilidades: o el programador lo sabía, y se le olvidó... o nunca se tomó la molestia de leerse la documentación. Prefiero pensar lo primero. Al fin y al cabo, la memoria viene genéticamente determinada, y uno no debe reñirle al prójimo por sus genes.
Por cierto, y si me permite el paréntesis, ¿sabía usted que podemos inicializar un diccionario genérico como si se tratase de una vulgar colección? ¡Es que un diccionario genérico es una colección! El quid está en saber qué tipo de colección: sus elementos son instancias de la clase genérica KeyValuePar. Esta es la técnica:
private Dictionary<int, string> d =
new Dictionary<int, string>()
{
{1, "Uno"},
{2, "Dos"},
{3, "Tres"},
};
Preste atención a la coma, aparentemente superflua, que sigue al tercer par de valores. Se trata de un sencillo, pero poco conocido, truco sintáctico que permite añadir o quitar pares, en cualquier momento, sin preocuparnos por el carácter especial del último par. Este mismo truco se aplica a los inicializadores de objetos, e incluso a la declaración de tipos enumerativos. Si le parece una barbaridad y una concesión (hay gente para todo), recuerde que en C# el punto y coma es un terminador, no un separador. ¿Qué hay de malo en querer usar la coma también como terminador, en estos casos?
Vamos ahora a otra garantía que solemos olvidar... o simplemente desconocer. Esta vez, la garantía tiene que ver con el propio lenguaje:
double d = 0.0;
if (cond)
d = 1.0;
else
d = 2.0;
Naturalmente, la inicialización de la variable local sobra:
double d;
if (cond)
d = 1.0;
else
d = 2.0;
No pasa nada porque se deje d sin inicializar... aparentemente. La instrucción que sigue lo garantiza, sin importar cuál de sus ramas se ejecute. Por supuesto, en este caso sencillo, habría sido más fácil escribir todo el código de esta manera:
double d = cond ? 1.0 : 2.0;
El programador que escribió el primer fragmento ha olvidado la sección de la especificación de C# que explica el concepto de asignación definida. La ha olvidado, o nunca se ha tomado la molestia de leerla.
Otro disparate frecuente debido al temor a lo desconocido:
private double f()
{
double d = 0.0;
OpenXXX();
try {
if (cond)
d = f1();
else
d = f2();
}
finally {
CloseXXX();
}
return d;
}
En realidad, el código anterior combina varios disparates debido al olvido o desconocimiento de las garantías de los contratos. Para empezar, vuelve a sobrarnos la inicialización de la variable local: la rama try del bloque try/finally nos garantiza la asignación definida de la variable, y la rama finally es irrelevante, si no utilizamos d en su interior. Tenga en cuenta que, si se ejecuta el finally, nunca llegaremos a la instrucción return.
Pero la zarpa que realmente aterra al programador del ejemplo es la posibilidad de ejecutar un return dentro de la rama try, por desconocimiento (u olvido) del funcionamiento de esta instrucción. Podemos ejecutar ese return sin problemas, y el código se simplifica entonces enormemente:
private double f()
{
OpenXXX();
try {
return cond ? f1() : f2();
}
finally {
CloseXXX();
}
}
El código simplificado es más corto, más comprensible y, probablemente, algo más eficiente, y ya no hablemos de la elegancia. ¿Moraleja? Conozca sus deberes y derechos, tal y como los especifican los contratos de su software, y así será mejor programador. O, mejor aún:
RTFM (again).
Etiquetas: C#, Design by Contract, disparates