sábado, 8 de agosto de 2015

Regla N°9 de la Ingeniería de Software: Los warnings de hoy son los errores de mañana (programa sin warning)


Los warning (advertencias) son mensajes que los compiladores nos muestran al generar los ejecutables de un proyecto que indican que debemos poner atención en un detalle en particular de nuestro código.

Los warning son sentencias sintácticamente correctas para el compilador, pero que tiene un significado semántico dudoso y posiblemente incorrecto. Por ejemplo, la frase "Mi tarea se comió a mi perro", es sintácticamente correcta, pero carece de sentido.

Una versión más avanzada de warnings pueden incluso advertirnos que nos estamos saltando estándares o buenas prácticas de programación.

Cuando me enfrento al mantenimiento de código realizado por diversos equipos de trabajo (y he de reconocer que no con menos frecuencia al creado por mi), me encuentro con que al compilar, existe un excesivo número de advertencias (excesivo son más de 200 o incluso mas). Advertencias que se han ido creando a lo largo del tiempo y que nadie se ha molestado en revisar jamás, acumulándose más y más cuanto más codificadores mueven el sistema. Generalmente nadie toca dichos warning debido al temor que entraña "mover" algo cuyo sentido desconocemos (el hecho que lo desconozcamos ya debiera hacer saltar la alarma). Bueno, si existe el warning, seguramente no tenga ningún significado oculto y simplemente es una omisión no intencional que deba ser corregida.

Los warnings se acumulan a lo largo de los años y el número dificulta entender claramente lo que nos están indicando, cuanto más warnings haya, menos casos les haremos, hasta que uno de esos warnings realmente representen un potencial problema en producción que se nos pase completamente desapercibido, convirtiéndose en un dolor de cabeza (con suerte) o en algo más grave, porque "los warnings de ahora son los errores de mañana".

Algunos warnings comunes, en este caso de C#, son los siguientes:

  • Variables que no se usan: Generalmente es una variable privada que se definió en alguna parte del código, y después de ser definida, no es usada en ninguna otra parte. Si no es usada en ninguna parte, ¿Por qué se define?, y si es necesaria, ¿Por qué no se usa?, generalmente lo correcto aquí, es eliminar la variable sin más.

  • Variables que solo se inicializan: Es un caso parecido al anterior, una variable privada que se define y a la que se asigna un valor, pero jamás se usa. Si no se usa ¿Para qué se instancia?, y si debe instanciarse ¿Por qué no se usa? La solución es borrar la variable y la cláusula de creación.

  • Parámetros que no son usados: Una función define un parámetro en su firma que no se usa en ninguna parte de su cuerpo, aquí hay un doble problema porque estamos obligando a que todas las consumidores de dicha función consideren dicho parámetro (que no sirve para nada), y por otro lado nos encontramos en un caso semejante a los anteriores, una variable que no se usa. La solución puede ser compleja en base al uso que se le da a la función, si es posible eliminar el parámetro y corregir todas las llamadas a dicha función, seria nuestra primera opción, si no podemos (y aquí debemos estar completamente seguros de no poder), se puede sobrecargar la función con los parámetros correctos, y marcar la opción con el parámetro que no se usa, como obsoleta, de esa forma los sistemas consumidores notaran dicho cambio, y deberán modificar su código, en el momento adecuado.

  • Fragmentos de código que nos son usados: Un conjunto de varias líneas de código, que nunca será ejecutado, porque el flujo de ejecución se interrumpe antes, por ejemplo si pongo código debajo de una sentencia return, nunca se ejecutara, de forma semejante si lanzo una excepción con throw. En estos casos lo mejor es tener claro el significado del código y donde debiera ubicarse apropiadamente. Si realmente no se va a ejecutar nunca, simplemente se elimina (si es un código heredado de un sistema productivo, lo más seguro es que no sirva para nada dicho código y se pueda eliminar con tranquilidad).

  • Métodos públicos que no son comentados: Tenemos un método público, que no tiene comentarios (en la cabecera del método, con la cláusula summary). Cuando un método es público, es porque deben ser consumidos por entidades ajenas a nuestro modulo, clase o sistema, esto significa que alguien que no somos nosotros necesita comprenderlo, y para comprenderlo necesita la documentación. La solución es sencilla, simplemente hay que documentar la función.

  • Métodos de una clase heredada que se llaman igual a un método de una clase base pero sin embargo no la sustituyen: En este caso tenemos dos clases, una clase base y otra heredada de la anterior. En un esquema tradicional la clase heredada podría sobrescribir métodos virtuales de la clase base, simplemente llamándose igual e indicando que se desea sobrescribir con la palabra override. ¿Qué ocurre si no especificamos a clausula override?, que el compilador no sabe que estamos queriendo sobrescribir la función o si estamos definiendo una nueva. Esto ultimo tiene extraños resultados, por ejemplo si nos referimos al objeto como la clase base se ejecutar siempre el método de la clase base y si los referenciamos por la clase heredara ejecutar el método de la clase heredada, en cambio si indicamos que deseamos sobrescribir el método, con la palabra override, siempre se ejecutara el de la clase base. El sistema lanza un warning indicándonos que debemos adornar el método con el atributo new o con el atributo override, de forma que de forma que mostramos explicita que es lo que queremos hacer (porque explicito es mejor que implícito):

    internal class ClaseA
    {
     public void Metodo1()
     {
      Console.WriteLine("Soy el metodo 1 de la Clase A");
     }
     
     public virtual void Metodo2()
     {
      Console.WriteLine("Soy el metodo 2 de la Clase A");
     }
    }
     
    internal class ClaseB : ClaseA
    {
     public new void Metodo1()
     {
      Console.WriteLine("Soy el metodo 1 de la Clase B");
     }
     
     public override void Metodo2()
     {
      Console.WriteLine("Soy el metodo 2 de la Clase B");
     }
    }

  • Condiciones que siempre va a ser verdadero o falso: Esto ocurre cuando escribimos una condición que siempre va da el mismo resultado, por ejemplo si estamos comprobando si un entero es nulo, siempre será falso puesto que un entero no puede ser nunca nulo (si un entero “Nulleable”, pero no un entero “normal”). En este caso hay que revisar cual es nuestra intención puesto que la condición siempre se ejecutara, y si tuviera una contra condición (como un else) nunca será ejecutada, lo cual no tiene sentido (tenemos validaciones que no hacen realmente nada en nuestro código).

    int i = 0;
    if (i == null)
    {
     //Nunca se ejecuta este codigo puesto que un entero no puede ser nulo
     Console.WriteLine("i es mulo");
    }
    else
    {
     //Siempre se ejecutar este codigo
     Console.WriteLine("i no es mulo");
    }
     

  • Métodos obsoletos: Este warning indica que un método que consume nuestro sistema, fue marcado como obsoleto, es decir que no va a mantenerse más y existe un nuevo método que cumple mas apropiadamente la funcionalidad de este. Puede ser que el método obsoleto se mantenga en las versiones próximas del software o que se elimine, en cualquier caso dependemos de un software que nos coloca en una situación incierta para nosotros. Necesitamos actualizar lo antes posibles a las nuevas versiones de las funciones que estamos consumiendo.

    [Obsolete("Esta función esta obsoleta")]
    public void FuncionObsoleta()
    {
     
    }
     

Actualmente hemos implementado una política de “cero warning”, esto es, cualquier modulo al que se deba realizar un mantenimiento, debe entregarse con cero warnings independientemente si los warning son preexistente a nuestro manteniendo. Una vez que se han eliminado los warning se configura el compilador para tratar los warning como errores, de forma que nuestro sistema no compilara si tiene warning pendiente de revisar, esto es útil para que no se generen nuevos warning, incluso si el equipo que mantiene el código es otro en el futuro.

Ahora bien ¿Qué pasa si realmente el warning de nuestro código es correcto? Es decir nuestro código debe programarse tal y no puede evitarse que se genere el mensaje del warning. Lo primero que hay que preguntarse si la afirmación anterior es realmente correcta, porque en nueve de cada diez ocasiones no lo será. Una vez que estemos seguro podemos limitar el código dentro de bloques pragma, que indican que no nos notifique de dicho warning (es decir que lo ignore), de esta forma no se nos mezclaran los warning que si consideramos correctos (los cuales ignoraremos) de los que no. El siguiente es un ejemplo de la directiva pragma:

// Desativa la notificacion de warning de tipo 0168 (variable no usada)
#pragma warning disable 0168
 
//Esta variable debiera generar un Warning, pero no lo hace debido a la setencia pragma
int variableSinUsar;
 
Console.WriteLine("Hola mundo");
 
 
// Vuelve a activar la notificacion de warning de tipo 0168 (variable no usada)
 
#pragma warning restore 0168
 
 
Para finalizar incluyo un enlace con descripciones de diversas referencias y warnings para Visual Studio .NET: