Ir al contenido principal

Introducción al Código limpio

Clean code

Clean Code es el título de un libro escrito por Robert C. Martin (Uncle Bob) donde nos habla de cómo escribir «código limpio», ese código bien estructurado, fácil de comprender, robusto y, a su vez fácil de mantener. Está dirigido para todos aquellos desarrolladores de software que deseen mejorar el diseño y la calidad de su código. No importa el nivel que se tenga; incluso los desarrolladores más experimentados aprenderán algo nuevo en cada capítulo. Para los desarrolladores más noveles, será una ayuda más que interesante para que empiecen a forjar su estilo.

Podemos decir que el código limpio es aquel código que es fácil de entender, y fácil del modificar.

public void fixTheCondition(Object condition, Object criteria) {
  if(!condition.equals(criteria)) {
    doSomethingToFix(condition)
  }
}
public void doSomethingToFix(Object condition) {
  ...
}

Seguramente la mayoría de la gente esté de acuerdo en que el código anterio es fácil de entender:

  • Es fácil de entender el flujo de ejecución de la función
  • Es fácil de entender la forma de colabrorar de los diferentes objetos
  • Es fácil entender la responsabilidad de cada objeto
  • Es fácil de entender lo que hace cada método
  • Es fácil de entender el motivo de cada expresión

El código limpio es simple y directo. Cuando se escribe código limpio se debe hacer con la idea en mente de que en el futuro otra gente tendrá que leer y entender ese código. Un punto al que se da una enorme importancia es a la construcción de tests. No sólo a la necesidad de disponer de tests, si no ha utilizarlos de la mejor manera posible. No es suficiente con hacer tests, hay que hacerlos correctamente y ejecutarlos constantemente.

Para empezar a escribir código limpio hay una serie de recomendaciones, que comentaremos a continuación un poco por encima.

Elegir nombres con significado

El nombre de una entidad lo es todo, define su razón de ser, su esencia, su propósito. Es importante llamar a las cosas por su verdadero nombre. Cada vez que se ponga el nombre a una entidad es necesario escoger con cuidado, y aun así posteriormente, Cambiarlo cada vez que encuentres uno mejor para ella. Un buen nombre debe responder a las preguntas básicas sobre la entidad a la que representa:

  • ¿Por qué existe esta entidad?
  • ¿Qué hace? ¿Cuál es su propósito?
  • ¿Cómo es usada?
private int a = 24;
private int totalNumberOfPayments = 24;

La primera variable no indica nada sobre que es lo que está representado en ella. Un algoritmo complejo que necesite hacer varias operaciones matemática sobre la variable "a" va a ser mucho más dificil de entender si se utilizan variables con ese nombre "a". Sin embargo si vemos el nombre de "totalNumberOfPayments", nos resultará mucho más fácil recordar sobre que se estan haciendo los calculos.

Cosas a tener en cuenta:

  • Los nombres deben estar diferenciados: Si un nombre representa un significado, y como es poco probable que dos variables tengan el mismo significado, es poco recomendable tener dos variables con nombres dificiles de distingir. Va a ser muy dificil distinguir por el nombre dos variables que sean "SessionBean", y "SessionDataBean".
  • Deben ser pronunciables: Debemos recordar que estamos escribiendo código para personas, y como tales intentaremos leer el código escrito. Un nombre que no sea pronunciable será muy dificil de recordar, y aunque pueda estar diseñado para aportar significado... nuestro cerebro no será capaz de hacer la asociación. Si intentamos hacer referencia a un metodo "amortXZdupVarCul" tendremos que buscar su nombre una y otra vez
  • Que no incluya metadatos: Hoy en día trabajar con un editor de código completo es casi imprescindible, y dicho editor de código nos va a aportar los metadatos de las variables o métodos. No tiene sentido ensuciar el nombre de la variable indicado marcas para conocer su tipo, su pertenencia a una clase, etc... "m_strPrimerApellido" es mucho más dificil de recordar que "primerApellido".

Se fiel al principio de responsabilidad única

Va muy de la mano del anterior principio de nombres con significado. Cuando diseñemos una función, es importante darle primero un nombre con significado, pero despues debemos asegurarnos de que la implementación de la misma sea fiel al nombre escogido. Debemos evitar que una función tenga efectos secundarios, para evitar acoplamientos y errores aleatorios. Si tenemos una función cuyo nombre es parseDocument y que retorna una estructura a partir de un nombre de fichero... nadie esperaría que dentro de dicha función se realicen operaciones de inserción en bases de datos que requieran de transaccionalidad, o menos aún que se inicialicen variables de contexto (lo que haría imprescindible invocar a esta función de primera para tener un contexto de ejecución estable).

A tener en cuenta:

  • El manejo de errores es algo que rompe el flujo normal de ejecución. Por ello, los cuerpos de los try - catch deberían estar agrupados en una función para evitar distraer. Además, una función encargada de gestionar los errores, debería hacer solo eso: empezar con el try, invocar al código que esperamos que pueda fallar, y tratar la captura del error
  • Las condiciones lógica deberían tener un único parámetro, y debería ser un parámetro puramente booleano. Cuando para resolver una condición lógica son necesarios más de un parámetro, o el parámetro lo expresamos como una operación lógica; entonces estamos ante un cadidato a ser extraido como una función separada, que será quien tenga la responsabilidad de evaluar la condición... por ejemplo, en lugar de tener un if(eventType.eqals(AdminEventType.REBOOT) || eventType.eqals(AdminEventType.FORZE_UPDATE)) queda mucho más claro un if(isRestartNeeded(eventType))
  • No se deben mezclar niveles de abstracción en una misma función. Cuando una función está haciendo al mismo tiempo operaciones de alto nivel (ordenar la compra de un producto, ordenar el envío de un mail, etc), y de bajo nivel (manipular cadenas de caracteres, operaciones aritméticas, etc), es muy probable que se estén mezclando responsabilidades. Además durante la lectura de esas funciones, es necesario obligar a la mente a hacer cambios de contextos para entener lo que está pasando en cada parte.

Mantener estructuras sencillas

La unidad fundamental de procesamiento dentro de la mayoría de lenguajes es la función. Con independencia de si son lenguajes funcionales u orientados a objetos, el grueso del procesamiento se lleva a cabo dentro de funciones. Es por ello que este elemento reciba una atención especial en cuanto a recomendaciones.

Para ello:

  • Siguiendo siempre el ideal por el cual nuestro código debe estar enfocado, cada una de las funciones que lo componen debe tener un objetivo claro. Hacer una cosa, pero hacerla muy bien. Las funciones deben ser pequeñas, para permitir su comprensión. Si una función tiene un propósito muy claro y está correctamente codificada, puede escribirse en menos de 20 líneas de código (¡¡Para la mayoría de los casos se necesitan menos de 5!!). Si necesita más, plantéate extraer parte del código a otra función, con un nombre descriptivo. Seguro que se mejora la legibilidad.
  • Una función no debería tener más de dos o tres parámetros de entrada. Por encima de este número, lo ideal es agrupar parámetros en un objeto y darle un nombre que represente el significado de esa agrupación. Se pueden agrupar argumentos en un solo objeto que represente mejor la semántica y relación de esos datos. Un ejemplo: public void insert(long id, String name, String description, long quantity, long price) frente a public void insertProduct(Product product)
  • No se debén exponer las dependencias de los objetos. Una clase sólo puede llamar a métodos de las clases con las que se relaciona, y no profundizar en su estructura recorriendo dependencias para buscar un método. Las clases deben comportarse como cajas negras, y debemos evitar tener invocaciones del estilo: product.getProvider().getRegion().getRepresentant().sendNotify(). En su lugar esa funcionalidad "sendNotify" debería estar expuesta ya en la clase de producto producto.sendNotifyToRepresentant()

Evitar los comentarios en el código

Los comentarios no pueden maquillar el mal código. La necesidad de comentarios para aclarar algo es síntoma de que hay código mal escrito que debería ser rediseñado. Es preferible expresarse mediante el propio código.

Hay situaciones en las que los comentarios son apropiados, como cuando tratan de:

  • Aspectos legales del código.
  • Advertir de consecuencias.
  • Comentarios TODO.
  • Remarcar la importancia de algo.
  • Documentación en APIs públicas.

Sin embargo, en el resto de los casos pueden llevar a confusión y deberían ser evitados.

Utiliza excepciones, no códigos de error

Se deben usar excepciones en lugar de códigos de retorno. Las excepciones son objetos complejos, y proporcionar información sobre el error y el momento en que se ha producido. Además, si utilizamos códigos de error estamos obligando a verificar siempre las condiciones de error en el código que nos invoca, forzandolo a incluir la responsabilidad de hacer un manejo de errores. Recordemos que el código de manejo de errores oculta la verdadera funcionalidad del código. Hay que mantenerlo lo más separado posible de la lógica de negocio para no dificultar la comprensión de ésta última. En este aspecto las excepciones no comprobadas no son invasivas a nivel de código.

Relacionado con este punto también podemos hablar de la necesidad de evitar el uso del valor null; tanto como parámetro de una llamada a un método, o cómo valor de retorno de una función. Un valor nulo carece de significado, y obliga a introducir lógica adicional para comprobar su presencia. Existen estructuras en los lenguajes para evitar la necesidad de este valor (por ejemplo en java la clase Optional), sepamos cuales son, y usemoslas.

Diseña las fronteras de la aplicación

Los sistemas dependen de paquetes de terceros o de componentes desarrollados por otros equipos. Hay que definir de forma clara la frontera entre el código y el exterior para poder acomodar de forma sencilla los futuros cambios, minimizando las partes de nuestro código que dependan de elementos externos.

Evita en todo lo posible que tus clases de lógica importen paquetes de terceros, y también trata de evitar que las importaciones de un paquete concreto esten desperdigadas por diferentes clases de la aplicación.

Diseña un completo juego de tests

Cuando desarrollamos guiados por los test, los tests de seben escribir en primer lugar. Las 3 reglas de TDD son:

  • No se debe escribir código de producción hast que no se tenga un test unitario que falle.
  • No se debe escribir más de un test unitario que lo necesario para que éste falle.
  • No se debe escribir más código de producción uqe el necesario para que pase un tests unitario que fallaba.

Se debe dar la misma importancia al código de test que al de producción. Los tests permiten que el código de producción se pueda modificar sin temor a introducir nuevos errores, asegurando su mantenibilidad.

El número de assert por cada test debe ser lo más bajo posible. Se debe testear un único concepto en cada test, lo que permite ir aclarando los distintos conceptos progresivamente, mejorando el conocimiento sobre el código.

Las reglas FIRST sobre el código de test son:

  • Fast: Se deben ejecutar rápido y muy a menudo.
  • Independent: Las condiciones de un test no deben depender de un test anterior.
  • Repeteable: Se deben poder ejecutar en cualquier entorno.
  • Self-Validating: El propio test debe decir si se cumple o no, no debe hacer falta realizar comprobaciones posteriores al test.
  • Timely: Los tests se deben escribir en el momento adecuado, que es justo ante de escribir el código de producción, lo que permite escribir código fácilmente testeable.

Comentarios