Cuántas veces habéis heredado código de alguien para hacer algún evolutivo, resolver algún bug y cuántas veces habéis pensado... “¡Uff! ¿Y esto? No sé ni por dónde cogerlo”.

Seguro que alguna vez os habéis encontrado con clases de más de 800 líneas, nombres de clases y variables que no representaban lo que realmente hacían, switch/case enormes con switch/case dentro de cases, etc. ¡Nosotros también y no hace tanto!

Pero, claro, ¿cuánto cuidamos nuestro código para evitar que otros tengan esos mismos pensamientos? Las presiones, las prisas... suelen hacer que olvidemos esos importantes detalles, pero como profesionales debemos evitar en lo posible que ocurra.

Así que hemos decidido publicar este post aprovechando que en Paradigma llevamos a cabo una iniciativa con la impartición de sesiones sobre código limpio a los equipos de desarrollo. Aquí puedes ver el ebook recopilatorio con nuestras mejores prácticas de Clean Code. En este post veremos qué es código limpio y cómo tú también puedas ser un guardián del código. ¡Empecemos!

¿A qué llamamos clean code?

Podemos encontrar diferentes definiciones, pero nos quedamos con las siguientes:

“Cualquier idiota puede escribir código que compila, pero sólo un buen programador puede hacer código que otros entiendan”, Martin Fowler.

Dave Thomas, fundador de OTI, padrino de la estrategia de Eclipse, define código limpio como:

“Código que otro desarrollador puede leer y mejorar sin ser el desarrollador original, que contiene test, nombres significativos, con mínimas dependencias que son explícitamente definidas y proporcionadas a través de una clara y mínima API.”

En el libro de Clean Code, de James O. Coplien y Robert C. Martin, encontramos:

“El código que estás tratando de escribir será tan fácil o difícil de escribir, dependiendo de lo duro o fácil que sea de leer. Si quieres generar código de forma rápida, si quieres que sea fácil de escribir, haz que sea fácil de leer.”

A nosotros nos gusta decir que nuestro código será código limpio cuando cumpla el siguiente conjunto de características:

Las definiciones anteriores ya nos dan pistas de cosas con las que debemos ser cuidadosos para generar código limpio, pero veamos más en detalle.

Las reglas básicas de la programación

Principios del arte clean code

Hay dos reglas o principios con los que deberíamos partir a la hora de ponernos a programar:

La regla del Boy Scout: “Deja el campamento más limpio de lo que lo has encontrado".

Cuando nos toca modificar código existente, deberíamos intentar siempre dejarlo un poco mejor de lo que lo hemos encontrado.

No debemos conformarnos con no dejar nuestra “basura”, sino que si vemos algo que podemos mejorar, ¡hagámoslo! De tal modo, que pequeños gestos ayuden a que el código no “degenere”.

Teoría de las ventanas rotas: “Imaginad un edificio que tiene una ventana rota. Si no se repara, los vándalos tenderán a romper unas cuantas más. Finalmente, quizás hasta entren en el edificio y, si está abandonado, es posible que hasta sea ocupado o le prendan fuego”.

Con el código pasa igual, el mal código termina contagiando y provocando que las personas dejen de intentar hacer bien su trabajo, si hay otros que no lo hacen o que pierdan el interés por hacerlo mejor. Debemos evitar los malos diseños, código no reutilizable y difícil de mantener.

Code Style

Se llama code style a las convenciones de código utilizadas en cada lenguaje de programación.

La forma en la que esté formateado el código no deja de ser comunicación, y la comunicación es de las primeras reglas de un buen profesional, por lo que el equipo debería establecer qué code style, qué reglas seguir y aplicarlas siempre.

A la hora de distribuir el código, lo más importante debería estar al principio y lo menos importante abajo, de tal modo que la importancia vaya de arriba a abajo, sería equiparable a cómo se escribe un artículo de un periódico.

Con respecto al número de líneas máximo y ancho, depende a quién le preguntes puede tener una opinión diferente, pero lo habitual es que tengan 200 líneas con un límite de 500 como máximo (¡y ya me parecen muchísimas!) y 80 caracteres por línea o 120. Lo suyo es evitar tener que hacer scroll a la derecha para ver la línea completa.

Podemos encontrar diferentes convenciones como: Java Code Style, Apache Coding Standard, Google Styles Guides o Python.

Nombres significativos

“El código es para humanos y las máquinas entienden binario”. Los nombres en software es algo muy importante, debemos elegir nombres para todo: damos nombres a las variables, funciones, clases, paquetes, directorios, a los artefactos que generamos...

Por lo que no es ninguna locura reflexionar para elegir el nombre más adecuado, que ayude al lector a entender qué es lo que representa y qué hace.

A la hora de buscar el nombre podemos ayudarnos respondiendo a estas tres preguntas:

No es lo mismo encontrarnos en una función:

int d; // número de días trabajados

Que encontrar:

int workedDays;

Debemos evitar nombres que, en lugar de dar información, confunda al lector. Por ejemplo no usar accountList cuando el tipo no sea un List, porque nos puede confundir.

Si se añade contexto a los nombres, nos debe permitir distinguir las diferencias. Por ejemplo moneyAmount no nos permite discernir qué le diferencia de utilizar money o customerInfo de customer.

Si un método es llamado como firstName, ¿qué hace, retorna un valor? Sin embargo, addFirstName nos deja mucho más claro las intenciones de éste.

Nuestro cerebro entiende las “palabras”, si no utilizamos nombres pronunciables es muy difícil discutir sobre ellos sin parecer bobos (esto va muy en relación con prácticas actuales como BDD).

A parte de esto, los desarrolladores nuevos que no hayan participado en la decisión de esos nombres impronunciables, van a tener que invertir tiempo para aprender lo que representan. Por el contrario, eligiendo nombres más legibles, es mucho más fácil y rápido de comprender.

 private Date genymdhms;
 private Date generationTimestamp;

Deberíamos utilizar nombres de variables de una o pocas letras, a su uso en métodos pequeños o bucles. Si una variable se va a utilizar en varios lugares del código, es mucho más fácil de localizar si utilizamos nombres más amigables.

La gente suele obviar muy rápidamente los sufijos o prefijos que se utilizan para nombrar clases o funciones, así que es mucho mejor que los evites. Nombrar interfaces adornándolas con una I por delante, es más una distracción que una aclaración, en la clase de implementación ya incluiremos el sufijo Impl.

No se deberían usar verbos para nombrar clases. Sin embargo, para los métodos sí que se deben utilizar verbos o nombres de frases verbales.

Sigue la regla de una palabra por concepto. Es confuso tener fetch, retrieve y get como métodos equivalentes para diferentes clases. Establece una convención y sé consistente a lo largo de todo el código.

La diferencia entre un programador inteligente y un programador profesional, es que el profesional entiende que la claridad es importante, se preocupa por escribir código bueno que otros puedan entender.

Comentarios

Cuántas veces nos ha pasado que hemos escrito comentarios que nunca hemos vuelto a actualizar a pesar de haber cambiado el código. O encontramos comentarios que realmente no aportan ninguna información porque el código ya se explica por sí mismo.


i++; // incrementamos en 1

Otras veces vemos entre los comentarios con fechas, nombres evidentes, menciones a usuarios ¿realmente aportan algo este tipo de comentarios? En otras ocasiones, nos encontramos los diarios de todos los cambios que se han ido haciendo sobre una clase, función... ¿por qué escribirlo ahí si tenemos un sistema de control de versiones que nos puede proporcionar esa información?

Con todo esto no queremos dar la impresión de que no se debe comentar el código, sino que se debe hacer un uso correcto de los comentarios y utilizarlos para compensar aquellos puntos en los que el código no sea lo suficiente explicativo. Deben ser claros y estar correctamente escritos.

No dejes código comentando ¡bórralo! Siempre podrás recuperarlo con el sistema de control de versiones. Y ¡no comentes el código malo, mejor reescríbelo!

Cuando documentes las APIs, hazlo pensando en que la persona que lo va a leer debe entenderlo rápido y claro, en estos casos se suele usar Javadoc.

Funciones

Cuando implementamos una función deberíamos seguir el Principio de Responsabilidad Única (es uno de los conceptos más importantes en la programación orientada a objetos) y es que el método sólo debería hacer una única cosa, ¡y hacerla bien!

Para que sea fácil de seguir y entender, además de las consideraciones que hemos visto de los nombres, deberíamos cuidar que cada línea no supere los 150 caracteres y las 100 líneas (como muchísimo!).

Más caracteres hará que el código sea más difícil de seguir y es muy probable que estemos perdiendo de vista la premisa de hacer una sola cosa, para pasar a hacer varias. Las funciones no deberían contener más de uno o dos niveles de anidamiento.

Cuando tenemos varias condiciones, _if/else o _while que ocupan más de una línea, es probable que sea mucho más adecuado cambiarlo por una llamada a un método. No sólo porque la función nos quedará más pequeña, sino también porque si utilizamos un nombre descriptivo, será mucho más entendible que tener que ver condición a condición qué se está evaluando.

Al igual que cuando leemos un documento o texto lo hacemos de arriba abajo, la lectura de código es similar y además existe la regla: the Stepdown rule, que viene a decir que cada función estará seguida de otra función, que corresponde con el siguiente nivel de abstracción, de tal modo, que podremos leer el programa, descendiendo de nivel de abstracción al mismo tiempo que leemos hacia abajo la lista de funciones.

O lo que es lo mismo, en la parte más alta de nuestro código tendremos la primera función de todas, que será la que inicia todo, la que mayor importancia tiene. Si esta función llama a la función B, la función B debería ser la siguiente función a poder leer.

Ahora me gustaría detenerme en los switch: los switch, por su naturaleza, siempre hacen “n” cosas, no siempre los vamos a poder evitar, pero para no romper con el principio de responsabilidad única deberíamos evitarlos y la forma de conseguirlo es con polimorfismo, con patrones, como puede ser el patrón de creación Abstract Factory.

¡Importante! Qué patrón aplicar, qué solución implementar, dependerá del problema y de las necesidades.

¿Cuántos argumentos son los recomendables para una función? Lo ideal sería ninguno o uno, pero nunca más de tres. Los argumentos son duros de leer, hay que interpretar qué representan cada vez que se ve un argumento y si son varios, con el nombre de la función no va a ser suficiente.

Además, cuantos más argumentos haya en una función más difícil de testear será. Se deben tener en cuenta todas las posibles combinaciones. Una posible solución a este problema es la encapsulación, usar objetos para agrupar argumentos cuando el número de argumentos es mayor que uno.

Estamos acostumbrados a que los datos de entrada se pasen a una función como argumentos y que la salida sea a través de return, por lo que utilizar argumentos de salida suele confundir al que lee el código. Si la función está haciendo una transformación en un dato de entrada, entonces debería retornarlo.

También es una mala práctica utilizar flags como argumentos, ya que rompen el principio de responsabilidad única: si el valor es true, hará una cosa y si está a false hará otra, es decir, estará haciendo dos cosas diferentes.

Evita duplicar código, principio Dry (dont repeat yourself), si tenemos un código que ya hace algo similar, no lo repitas: abstrae, generaliza y refactoriza, pero nunca dupliques.

Tratamiento de errores

Chequear excepciones es algo muy útil y necesario que debemos hacer. El manejo y tratamiento de excepciones nos permiten asegurar que nuestro código se mantiene estable pase lo que pase.

El manejo de excepciones por sí mismo ya es hacer una única cosa, por lo que una buena práctica es la de separar la lógica de negocio del manejo de errores. De este modo nuestro código se mostrará más limpio y será mucho más fácil de mantener.

Para asegurar que nuestro código es estable, lo ideal sería seguir TDD: empezamos escribiendo los test, incluidos aquellos que fuercen el lanzamiento de excepciones, continuamos escribiendo el código y lanzamos los test para asegurar que pasan.

Cada excepción que lanzamos en nuestro código debería proporcionar suficiente contexto para determinar cuál es la fuente del error y dónde está localizado.

Una buena práctica para este problema es definir una clase de excepciones, de tal modo que ya sólo nos debemos preocupar de cómo capturarlas y tratarlas. Además es mucho mejor lanzar excepciones que códigos de error, ya que éstos nos obligan a tratarlos, mientras que con las excepciones se puede delegar para más adelante o capturarlas con un handler.

Veamos el siguiente ejemplo, donde se muestra un trozo de código en el que se está utilizando una librería de terceros en la que tenemos cubrir todas las excepciones de la llamada:


ACMEPort port = new ACMEPort(12);
try{
      port.open();
}catch(DeviceResponseException e){
      reportPortError(e);
      logger.log(“Device response exception “, e);
}catch(ATM1212UnlockedException e){
      reportPortError(e);
      logger.log(“Device response exception “, e);
}catch(GMXError e){
      reportPortError(e);
      logger.log(“Device response exception “, e);
}...

Como vemos hay un montón de duplicidades, podríamos simplificarlo envolviendo el API del siguiente modo:


LocalPort port = new LocalPort(12);
try{
      port.open();
}catch(PortDeviceFailure e){
       reportPortError(e);
       logger.log(“Device response exception “, e);
}

Donde LocalPort es una clase que envuelve los catches y envuelve las excepciones.


public class LocalPort{
      private ACMEPort innerPort;
      public LocalPort(int portNumber){
      innerPort = new ACMEPort(portNumber);
}
public void open(){
      try{
            innerPort.open();
      }catch(DeviceResponseException e){
            throw new PortDeviceFailure(e);
      }catch(ATM1212UnlockedException e){
            throw new PortDeviceFailure(e);
      }catch(GMXError e){
            throw new PortDeviceFailure(e);
     }
     ….
}

Una buena práctica cuando tenemos que integrar APIs de terceros es hacer un recubrimiento, encapsulando el API, de tal modo, que nos permita minimizar las dependencias al respecto y blindarnos así, de cambios que se puedan realizar en el futuro sobre el API.

Veamos otro ejemplo:


try{
      MealExpenses expenses = expenseReportDAO.getMeals(employee.getId());
      total += expenses.getTotal();
}catch(MealExpensesNotFound e){
      total += getMealPerDiem();
}

En el siguiente ejemplo vemos que tenemos un caso especial, donde el empleado recibirá una comida por día si no encuentra una. ¿Ves algo raro? ¿No sería mucho más simple cambiar el código para que siempre retorne un objeto MealExpense y si no hay datos de comida que retorne un objeto con el total por día?

Esto que comentamos se conoce como Special Case Pattern (Fowler). Donde puedes crear una clase o configurar un objeto que maneje los casos especiales por ti. De este modo el cliente no tiene que lidiar con un comportamiento especial.

Para el caso de devolver null podemos seguir el mismo patrón y retornar una excepción significativa o especial. ¡Es una muy mala práctica retornar null!

Imaginemos el siguiente código:


List employees = getEmployees();
if (employees != null){
      for(Employee e: employees){
            totalPay += e.getPay();
      }
}

Puede parecer un buen código, o no estar tan mal, pero es malo, porque estamos dándonos trabajo a nosotros y generando problemas a los que nos llaman.

En lugar de retornar null desde un método, mejor lanzar una excepción o retornar un objeto especial en su lugar. Y si un método de un API de terceros retorna null, considera recubrirlo para que, como antes, lance una excepción o retorne un objeto especial.

También es una mala práctica pasar null como argumento de un método, deberíamos evitarlo, tendríamos que chequear para cada argumento si es null y en ese caso lanzar una excepción de InvalidArgumentExceptions.

En estos casos, una buena práctica es usar las aserciones, no resuelven el problema pero el código quedará mucho más limpio. Librerías como la de apache-commons proporcionan clases de utilidad para realizar estas validaciones de forma sencilla.

Clases y Objetos

Clases

Al igual que comentábamos para las funciones, las clases también deben tener un tamaño pequeño. En este caso el tamaño se mide por las responsabilidades que engloba.

El nombre de la clase debería describir qué responsabilidad cumple. Si no somos capaces de dar un nombre concreto y conciso, probablemente tendrá un tamaño demasiado grande.

A las clases también aplica el principio de Responsabilidad Única (SRP), que establece que sólo debe tener una y sólo una razón para cambiar.

Se debe reducir el acoplamiento entre las variables y funciones de una clase, se recomienda seguir el principio de Open-Closed, diseñadas para que puedan extenderse pero cerradas a la actualización.

Y por otro lado, el principio de Dependency Inversion (DIP), que dice que nuestras clases deberían depender de abstracciones y no de detalles concretos.

Objetos y estructura de datos

Una buena práctica es utilizar la abstracción para mostrar los datos en términos abstractos pero no el detalle de ellos, a nadie le tiene que interesar cómo ha quedado la implementación.

Pero esto no sólo se logra mediante el uso de interfaces y/o getters y setters, se necesita pensar seriamente para encontrar la mejor forma de representar los datos que contiene un objeto. La peor opción que podemos tomar es la de agregar alegremente getters y setters.

En este punto debemos hablar de varios conceptos:

Nuestro código debe tender a minimizar el acoplamiento y aumentar la cohesión, que es lo que viene a decirnos la ley de Demeter: un método f de una clase C, debería solo llamar a métodos de la clase C, objetos creados por f, objetos pasados por argumentos a *f *y objetos mantenidos como variables instanciadas. Es decir, nuestro código sólo debe hablar con los más cercanos.

Integración con terceros

Son muchas las veces que nos toca integrarnos con un API o librería de terceros que todavía está en desarrollo y que probablemente sufrirá cambios.

Una buena práctica, como comentábamos antes, para solventar y minimizar el impacto de los cambios futuros del API es envolverla, pero también podemos usar el patrón de ADAPTER para convertir desde nuestra interfaz a la interfaz proporcionada para la integración.

De este modo, tendremos una clara separación entre el código de integración del API, como del resto de la lógica, además deberíamos escribir los tests que chequeen el comportamiento esperado.

Es mucho mejor depender de algo sobre lo que tú tienes control que sobre algo que desconoces.

Introducción a los Tests

Al principio del post decíamos que un código es limpio si además cuenta con tests. De hecho el código de los tests es tan importante como el código de producción, requiere diseño y cuidado, debe ser legible y sin duplicidades.

Para su desarrollo se pueden utilizar técnicas como TDD (Test Driven Development), que consiste en escribir primero las pruebas unitarias, después escribir el código hasta que pasen los tests satisfactoriamente y, por último, refactorizar. De tal modo que conseguimos un código más robusto, seguro y mantenible.

Los test y el código de producción deben ser escritos juntos. Si no mantienes los test limpios, los terminarás perdiendo y sin ellos se perderá la flexibilidad del código. Los test unitarios son los que nos aseguran un código flexible, mantenible y reusable, además aseguras que un cambio no haya generado un bug.

Cada test debe seguir el patrón de BUILD-OPERATE-CHECK, es decir, estará dividido en tres partes given-when-then:

Una práctica que se suele seguir para evitar duplicidades es la de utilizar el patrón Template Method, donde el given*/*when se llevan a una clase base y la parte del then en diferentes.

También se pueden crear clases de test completamente separadas y poner el given y when en una función anotada con @Before y el when en cada función anotada con @Test. También se debería intentar que cada función de test valide un sólo concepto.

Para que los tests los podamos considerar como limpios tienen que cumplir el principio de F.I.R.S.T que está compuesto por cinco reglas:

Conclusiones

A lo largo de este post hemos intentado reflejar algunas consideraciones para hacer un código más limpio y mantenible. A modo de recopilación, debemos intentar seguir los principios de: responsabilidad única, open-closed y no olvidar lo importantísimo que es dar nombre significativos.

Hacer cambios de código y de estructura puede ser un trabajo muy duro, sobre todo si no tenemos tests que aseguren que no estamos rompiendo nada. También hay que ser consciente de que ningún código es inmune a ser mejorado.

Escribir código limpio no es algo que se consiga a la primera ¡escribe código y limpia después! Y evolucionar o corregir un código que no es nuestro también es muy duro, pero somos responsables de dejar el código un poco mejor de lo que lo hemos encontrado.

Un buen principio a seguir y que suelo aplicarme siempre es el de: “deja el codigo mejor de lo que te lo encuentras”.

Referencias

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete