El arte del código limpio – I

«La honestidad por las cosas pequeñas no es algo menor»

proverbio danés

Éste será mi primer post que escriba, y voy a comenzar con lo que considero que son las bases que todo buen programador necesita o por lo menos debe de conocer para que el fruto de su trabajo sea lo más maduro y sólido posible. Con ésto voy a comenzar detallando algunas de las técnicas o buenas prácticas que se conocen en el mundo del desarrollo del software para mantener un código lo más legible posible y de una calidad superior.

Puede que estés pensando que la legibilidad del código no es una de las partes importantes dentro de la programación, que antes de nada se encuentra el uso de principios SOLID, patrones de diseño, patrones arquitecturales … etc. Que una buena arquitectura de base hace que el proyecto en sus etapas finales sea robusto y muy tolerante a fallos, y si, en eso estoy de acuerdo contigo, pero también hay que tener en cuenta que la programación rara vez es una tarea solitaria, y que seguramente donde más se necesite aplicar los conceptos que antes mencionaba sea en proyectos grandes donde trabajarán codo con codo un número variable de personas, en mayor cantidad según la magnitud del proyecto.

Es por esto que el código que hoy escribas, mañana seguramente será leído o refactorizado por un tercero, o por lo que es peor y con mayor probabilidad suceda, por ti mismo… . Y te puedo asegurar que da igual que seas tu el autor de esa tarea, que pasados 3 meses o más, ese código será tan extraño que incluso es posible que le preguntes a algún compañero por esas líneas tan extrañas que ves en la pantalla y por las que maldecirás a la persona que no haya tenido cuidado en la correcta claridad y sencillez del código.

Uso de Nombres

Comenzamos con lo más sencillo, el uso de los nombres en el código. Como bien sabrás cualquier variable que sea declarada será identificada bajo un nombre único e inequívoco, y esto es importante básicamente porque un nombre de variable bien descrito es capaz de representar con total claridad un concepto específico y no otro, debe de revelar su intención claramente, nada de segundas intenciones o conceptos ambiguos.

var x;
foreach(var i; i < x.lenght; i++){
    if(x[i] == 4)
       return true;
}   
return false;

En el código anterior a simple vista se puede deducir que hacemos un recorrido sobre el contenido ubicado en la variable ‘x’, y en cuanto se encuentre un valor de la colección en ‘x’ que coincida con el valor 4 entonces se devolverá el valor booleano ‘true’, y en caso de no encontrarse dicho valor se obtendrá ‘false’. Es imposible saber sólo con estas líneas de código y fuera de contexto que es lo que se está intentando realizar en estas líneas, pero en cambio si el código se redacta de una forma más legible podría quedar de la siguiente forma.

var StatusFlags;
const PAYMENT_STATUS = 4;
foreach(var index; index < StatusFlags.lenght; index++){
    if(StatusFlags[index] == PAYMENT_STATUS)
       return true;
}   
return false;

Ahora parece que se puede vislumbrar un poco de contexto inherente en el código, tan sólo haciendo que nuestras variables o constantes sean descriptivas en sus nombres estamos añadiendo un valor en el código que ayuda al programador a ubicarse dentro de un contexto y a entender con mayor facilidad que es lo que se intenta hacer. Simplemente estableciendo que la variable anteriormente ‘x’, ahora se denomine ‘StatusFlags’, y extrayendo el número mágico 4 con el que se utilizaba para encontrar el estado, ahora entendemos que este trozo de código es parte de un sistema de pagos en el que se busca el estado actual de la transacción. De otra forma el programador debería de bucear por el código hasta encontrar la raíz del valor de ‘x’ o preguntarle a algún compañero más versado en dicho contexto para poder ver la luz al final del túnel.

Reitero la importancia de ser descriptivo y claro con el código que escribimos, puesto que al final de la jornada lees más lineas de código de las que escribes, y si no me crees te invito a que tomes nota y me demuestres que estoy equivocado.

Es clave en ésta tarea que los nombres que utilicemos sean descriptivos, nada de acrónimos o abreviaturas. En un proyecto en el que trabajé utilizaban esta técnica con los nombres de variables y métodos, todo empezaba con las siglas del nombre del proyecto, si suponemos que el proyecto se llamaba <<Gestión de Embalses>>, todas las variables y métodos principales siempre seguían la siguiente dinámica GDE_GetAllEmbalses(), GDE_STATUS, GDE_AddNewEmbalse()… ¿Ves ya el problema?

Se le está añadiendo a todo el código el valor del proyecto, cuando realmente éste valor debería de estar en el nombre de proyecto. Cuando intentas buscar con el inspector de código en el IDE, de nada sirve filtrar por ‘GDE’, te saldrán 500 resultados, y además el ser humano aprende a ignorar los conceptos repetitivos, por lo que al final hacer esto añadía un valor inútil en todo el código. En un libro el nombre de éste tan solo aparece en la portada y las primeras páginas, nuestro código debe de mostrar la misma estructura, puesto que buscamos una legibilidad que se asemeje a la de un libro, tu código debe de ser fácil de leer y de entender, haz que sea así.

Un último apunte a añadir en cuanto al nombre de los conceptos que debe manejar un programador en su código, es que somos programadores y escribimos código para programadores, por lo que si en el contexto añades vocablos que sean de algún concepto técnico propio del gremio es muy probable que el siguiente programador que le toque escudriñar tu código sea capaz de entender que si a una clase la denominas BankTransferStrategy, sea que estas aplicando el Patrón Estrategia (Strategy Pattern), y si además dicha clase se encuentra en la misma carpeta con otras clases como MobileTransferStrategy o ManualTransferStrategy, blanco y en botella.

Uso de Funciones

Cualquiera que lleve algo de tiempo programando se habrá dado cuenta de lo difícil que resulta entender lo que hace una función (Obviamente no desarrollada por ti y si eres tu el creador después de mucho tiempo) de tamaño considerable, hablamos de esas funciones que pasan de las 20 líneas de código, si si a esas me refiero. A mi en concreto y supongo que a vosotros también se os habrá dado el caso de que ni siquiera sabes en que punto empieza la función o termina, ya que el IDE en cuestión no da más de si.

Y te preguntarás ¿Como puedo evitar que ésto suceda? la solución te dejará boquiabierto, hacer funciones de tamaño pequeño, y cuando digo pequeño es todo lo pequeño que lleguéis a pensar, cuanto más mejor. Pero claro, del dicho al hecho hay un trecho, y es que es muy sencillo que te digan que tus funciones tienen que ser minúsculas cuando en realidad el algoritmo que tienes que implementar es sumamente complejo, como todo en la vida no existen blancos o negros, siempre hay excepciones. No obstante si continuamente intentamos seguir esta filosofía, a la larga nuestro código será mucho más legible y por lo tanto mantenible.

Para conseguirlo tienes que hacer que «Tus funciones hagan una sola cosa, y esa cosa la hagan bien, y sea lo único que hagan«, si seguimos esta filosofía con ello estaremos cumpliendo el primero de los principios SOLID, Single Responsability o principio de responsabilidad única. Otra ayuda que nos puede servir para identificar si nuestras funciones siguen éste principio, y que además esta considerado un code smell, es cuando el nombre de tu función es del tipo SetNewPricesAndDiscount(), aún no hemos visto el contenido del método pero ya te puedo decir que es muy probable que no cumpla dicho principio.

Lo normal es que nuestras funciones si están bien definidas, su nombre siga la estructura de Verbo + Nombre, por ejemplo AddPrices(), SetDiscount() …etc. Pongamos algunos ejemplos:

private double _price;
private double _discount;

public void SetNewPricesAndDiscount(double price, double discount, bool addDiscount){
   _price = price;
   if(addDiscount) 
      _discount = discount;
}

public double ApplyDiscount(){
   return _price * _discount;
}

Ya se que el ejemplo anterior es demasiado simple, pero no por simple significa que deban hacerse las cosas de dicha forma. Como vemos es un simple método donde se establecen dos propiedades en un sólo método, y sólo en el caso de que se especifique con el valor booleano addDiscount se aplica un descuento, entonces se establecerá esa propiedad. Esta acción es totalmente un indicativo de que nuestra función realiza más de una cosa, todos hemos visto métodos a los que se les pasa un valor booleano, y hace que dicho método aplique una lógica u otra, pues bien este es un caso que debemos evitar puesto que estamos rompiendo el principio de responsabilidad única anteriormente comentado.

Nuestro código quedaría mejor del a siguiente manera:

private double _price;

public void SetNewPrice(double price){
   _price = price;
}

public double ApplyDiscount(double discount){
   return _price * discount;
}

En definitiva se trata de separa la funcionalidad, si tu función aplica opcionalmente o cuando tu se lo indiques una lógica diferente, es síntoma de que necesitas otra función que realice tal acción requerida.

Otro de los aspectos que debemos eludir es la modificación de objetos pasados por parámetros, si un objeto ha de ser modificado internamente, debe de ser él mismo el encargado de tal acción y delegar desde el exterior cuando se necesite aplicar dicha función. De otra forma estamos ocasionando lo que se llama como efectos secundarios, puesto que no es normal el modificar objetos pasados por parámetro, si es lo que realmente se quiere, el funcionamiento correcto sería el de crear un clonado del objeto por parámetro y el mismo objeto modificado devolverlo en la función.

De esta manera evitaríamos una modificación no deseada de nuestro objeto, porque recordemos que la programación es una tarea de equipo, y tu puedes ser plenamente consciente de lo que realizan tus funciones, pero es muy probable que alguno de tus compañeros no lo sepa, y sea presa de estos efectos no deseados. De hecho muchas librerías de las estructuras primitivas de código, como String y por ejemplo la función string Concat(string a, string b), funcionan de la forma que os he comentado, utilizan los objetos recibidos por parámetro para devolver uno nuevo.

En definitiva si separamos nuestras funciones como funciones de consulta simples y funciones de acciones o de comandos, el código resultante (lo normal…) sera limpio. Además teniendo en mente el denominado principio DRY (Dont Repeat Yourself) o no repitas código, si observamos que existen varias líneas de código que se repiten continuamente, significa que existe una funcionalidad común en todas esas secciones que aún no hemos extraído. Como he dicho antes existen muchos casos y situaciones en los que quizá tengas que darle una patada al libro de buenas prácticas para conseguir tu objetivo, pero si intentas siempre mantener estas normas tus funciones serán de una calidad superior, y lo mejor de todo, cualquiera que las lea las entenderá fácilmente.

Uso de Comentarios

Más bien esta sección debería de llamarse «El no uso de comentarios«, puesto que al final lo ideal sería que nuestro código no tuviera comentarios y fuese tan expresivo como leer un periódico, los métodos principales de nuestras clases deben de encontrarse en primer lugar, así como en un periódico lo primero que encontramos es el título y un subtitulo, junto con una breve descripción. Si conseguimos que nuestro código adquiera tal expresividad que con tan solo leerlo se pueda deducir su intención, no necesitaremos de comentarios que puedan confundir o que simplemente sean innecesarios.

Y no es que yo sea un loco o una de estas personas que grita a los cuatro vientos que lo odia todo sin dar motivo alguno o sin razones de peso, al contrario, te explicaré a continuación porqué no es buena idea llenar tu código de comentarios. Nosotros somos programadores, nuestra tarea es la de expresarnos a través del código, cualquier cosa que se encuentre entre el código pero no influya en la trazabilidad de éste lo ignoramos ¿Porqué? porque la gran mayoría de veces no aportan valor alguno, y con el tiempo aprendemos instintivamente a ignorarlos.

Recientemente me ha sucedido un caso donde el código se mostraba con una estructura de la siguiente forma:

List<Ticket> Tickets {get; set;}

public List<Ticket> GetFilteredTickets(int STATE, DateTime searchDate){
   if(STATE == 0){ // IF STATE == 0 (Closed)
       return Tickets.Where(t => t.InDate >= searchDate).ToList()
   }else if (STATE == 1){ // IF STATE == 1 (Calculated)
       return Tickets.Where(t => t.CalcDate >= searchDate).ToList()
   }else if (STATE == 2){ // IF STATE == 2 (All)
       return Tickets
              .Where(t => t.InDate >= searchDate || 
                         t.CalcDate >= searchDate)
              .ToList()
   }else if (STATE == 3){ // IF STATE == 3 (Pending)
       return Tickets.Where(t => t.CalcDate >= searchDate).ToList()
   }
}

Para poneros en contexto de la función anterior, en la vista aparecía un selector de estados y otro por fecha, dependiendo del valor establecido, el cual mostraba los distintos valores posibles en el siguiente orden Todos > Calculados > Cerrados > Pendientes, se realizaba un filtrado de tiquet dependiendo del estado indicado y fecha superiores de creación. El fallo que me tocaba solucionar era uno en el que el usuario seleccionaba el estado «Todos» junto con una fecha, y en el listado que aparecía a continuación le aparecían todos los tiquet filtrados pero no coincidía la fecha que establecía de filtro con la fecha de creación en la que se mostraban los tiquet, ya que en el listado solo aparecía la fecha de creación de estos, es decir, «t.InDate«.

No se si ya, con la información que os he contado del problema que tenía el usuario veis el fallo, al usuario le salían varios tiquet con otras fechas que no coincidían en el filtro, y esto es porque cuando el filtro indicado es el de «Todos«, el código filtrará por fecha de creación «InDate» como por fecha de cálculo «CalcDate» del tiquet, debido a esto, como sólo se mostraba la fecha de creación, el filtro por fechas parecía no funcionar.

Pues bien este problema que a simple vista es directo, me llevo más tiempo de la cuenta, ya que el contexto o significado de los distintos estados del tiquet se encuentra en los comentarios (Si os habéis fijado…), mientras que el código no es expresivo por si mismo, el código anterior sin los comentarios sería una locura entenderlo, carecería de contexto alguno y tan solo depurando sería posible entender que es lo que esta pasando ahí. A lo que vengo a decir es que yo me fijé en el orden de estados que antes comentaba para deducir por cual de los if-else estaba actuando mi código, mi mente entendió a su forma que el orden de los estados que aprecia en la interfaz, sería el mismo en el código y por lo tanto el estado «0» sería el estado por defecto o «Todos» en éste caso, cuando la solución la tenía todo el tiempo en los comentarios pero no la veía.

El valor real y definitivo del código esta en él mismo, no tenemos que depender de comentarios para aportarle contexto a nuestro código, nuestro código debe ser lo suficientemente expresivo para aportar valor al programador sin necesidad de comentarios. Y digo esto puesto que los comentarios no evolucionan de la misma forma que el código, cuando un proyecto avanza en el tiempo y van pasando distintos desarrolladores en él, es posible que los primeros pusieran varios comentarios aclarando que partes del código hacían qué cosa, pero te puedo asegurar que cuando transcurran unos años tendrás un código que te está diciendo una cosa y habrán comentarios que te estén engañando o diciendo algo totalmente opuesto al código.

Siempre que tengas la necesidad de escribir un comentario porque consideras que tu código no es suficientemente claro, para un segundo y piensa en alguna forma de conseguir que tu código se exprese mejor, cambia nombres a métodos o variables, modifica estructuras ambiguas, refactoriza hasta que consideres que es lo más legible posible y que no hay posibilidad a duda alguna, y si aun así después de todo eso crees que no es expresivo, entonces ahí, utiliza un comentario, ya que esto no es la Navaja de Ockham, no todo lo simple es siempre lo correcto.

Al final del día el código lo refactoricé de la siguiente forma, y vosotros mismos decidiréis cual es mejor y si mereció la pena el cambio o no:

List<Ticket> Tickets { get; set;}

public enum TICKET_STATE {
   CLOSED = 0, CALCULATED = 1, ALL = 2, PENDING = 3
}

public List<Ticket> GetFilteredTickets(int state, DateTime searchDate){
   switch(state){
      case TICKET_STATE.CLOSED:
      case TICKET_STATE.ALL:
         return Tickets.Where(t => t.InDate >= searchDate).ToList();
      case TICKET_STATE.CALCULATED:
      case TICKET_STATE.PENDING:
         return Tickets.Where(t => t.CalcDate >= searchDate).ToList();
      default:
         return null;
   }
}

El código anterior todavía tiene posibilidad de mejora, ya que una estructura switch-case rompe por completo el Principio de Abierto – Cerrado, pero por lo menos esa parte del código ya está refactorizada de tal forma que, yo por lo menos, considero que es mucho más expresiva que la anterior. Tan solo queda esperar al siguiente héroe que le toque modificar la misma parte y se ponga el sombrero de limpiador de código o code-cleaner, en inglés siempre queda mejor.

Para terminar acabo diciendo una cosa que en los tiempos que corren creo que no hace falta recalcarla y que creo que es incluso peor que poner comentarios que le den contexto al código, y es código comentado. El código comentado es totalmente inútil, puede ser que en su momento algún programador lo comentase bajo el miedo de «Bueno por si luego me vuelven a pedir que lo modifique...», no caigas en esa tentación, tu código debe de hacer lo que dice, no tener vestigios entre sus líneas de código muerto que nadie se ha atrevido a eliminar o que se deja comentado temporalmente para no tener que pensar en la lógica de nuevo. ¡BORRA ESOS COMENTARIOS! No dejes que nadie te meta miedo diciendo que es lógica futura importante, o que es demasiado complejo que costaría replicarlo.

Si no lo sabes ya, existen varias herramientas de control de código como puede ser Git, que se asegurarán de que tu código no se pierda.

Creo que por hoy vamos a ir dejando esta primera parte acerca de escribir código de calidad, más adelante escribiré un segundo Post donde continuaré contando buenas prácticas a la hora de crear objetos y clases, y la importancia de las pruebas automatizadas en el código, que siempre se nos olvidan o cuesta ponerse a implementarlas. Espero que vuestro código siga mejorando día tras día.