miércoles, 5 de enero de 2011

Clean Code

Durante los últimos meses he estado leyendo y releyendo el libro “Clean Code: A Handbook of Agile Software Craftmanship” de Robert Martin. En este libro, el autor trata por qué hemos de escribir código limpio y cómo hacerlo.


Código Limpio. Este tema es algo que me interesa particularmente en estos momentos. Cuando eres un desarrollador con responsabilidades puntuales dentro de un proyecto y sin relevancia ninguna en cuanto a las decisiones que toma el arquitecto de turno, lo único importante es quitarte el muerto de encima cuanto antes para evitar las típicas preguntas de: ¿Cómo vamos (¿Vamos? Pero si lo estoy haciendo sólo yo…)? ¿En qué estado está esta tarea? ¿Qué pongo en el Project, 80%, 90%...? ¿Podemos hacer la entrega ya?

Sin embargo, cuando tienes cierto grado de responsabilidad sobre un proyecto y te interesa que su evolución no se complique es importante hacer entender a los miembros del equipo que la responsabilidad de cada uno va más allá de quitarse el muerto de encima, sino que cada uno debe hacer lo mejor que pueda por tener un código lo más comprensible posible. Al código comprensible es a lo que Robert Martin llama código limpio.

Este libro está dividido en tres partes:
  • En una primera parte, se explica qué es el código limpio y cómo llegar a él.
  • La segunda parte son ejemplos más complejos de refactorización de código en busca de código más limpio.
  • La tercera parte es una recopilación de pistas que ayudan a encontrar código que no está todo lo bien que debería estar y que debería ser refactorizado.

En este resumen del libro me voy a centrar en la primera parte de él, puesto que la segunda y la tercera parte son bastante directas.

Capítulo 1 - NOMBRES

La lectura del código es la actividad a la que se dedica más tiempo, incluso para escribir nuevo código hay que leer lo que ya hay escrito. Un mal código puede llegar a arruinar un proyecto, dificultando las modificaciones y alargando los plazos de entrega. El único responsable de ello es el programador que no se toma en serio su trabajo y no es profesional.

Para evitar estas situaciones se debe hacer un código limpio, pero, ¿qué es código limpio?. Según varios autores conocidos, las características del código limpio son:
  • Fácil de leer, expresivo y sencillo.
  • Probado mediante tests automáticos.
  • Hace una única cosa.
  • No existe duplicidad en él.
  • Utiliza el menor número de elementos posibles.
  • Realiza abstracciones de elementos similares.

Para conseguir un código limpio se debe prestar atención a cada uno de los elementos que componen un sistema a nivel de código: variables, funciones, clases, comentarios, tests y el propio sistema. Además existen una serie de aspectos, como son la diferenciación entre objetos y estructuras de datos, el comportamiento en los límites del sistema, el manejo de errores y la concurrencia que afectan directamente al código y a su comprensión. Capítulo a capítulo se va analizando cómo mejorar en cada uno de estos elementos

Tembién habla de la regla del Boy Scout “Hay que dejar el código mejor de cómo lo encontraste”. Esto va en la línea de la mejora continua del código y de la imposibilidad de hacerlo perfecto a la primera.

Capítulo 2 - NOMBRES

Un buen nombre da mucha más información que cualquier otra cosa. Para conseguir buenos nombres hay que usar nombres descriptivos y claros. Deben ser legibles y evitar codificaciones complejas.

Capítulo 3 - FUNCIONES

Una buena función es aquella de la que se puede inferir su comportamiento de un solo vistazo. Para ello deben ser cortas, hacer una única cosa y mantenerse dentro del mismo nivel de abstracción.

Es importante usar buenos nombres y reducir al mínimo el número de argumentos. Pero lo principal es eliminar toda la duplicidad.

Capítulo 4 - COMENTARIOS

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.
  • Javadocs en APIs públicas.

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

Capítulo 5 - FORMATEO

El formateo afecta directamente a la legibilidad del código. El código se lee de arriba abajo y de izquierda a derecha. Los espacios verticales y horizontales permiten separar ideas y conceptos distintos.

Se debe mantener un estilo uniforme, y para ello se debe consensuar con todo el equipo las reglas de formateo. Las reglas elegidas no son tan importantes como el hecho de que todo el mundo se atenga a ellas.

Capítulo 6 - OBJETOS Y ESTRUCTURAS DE DATOS.

No todo se puede solucionar usando sólo objetos o sólo estructuras de datos. Hay que aprender a diferenciar en qué situaciones son más convenientes unos u otros. Los objetos esconden sus datos y exponen funciones que permiten manipularlos, mientras que las estructuras de datos exponen datos pero no tiene funciones que operen sobre ellos.

Capítulo 7 - MANEJO DE ERRORES

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.

Para no complicar el manejo de errores hay que escribir los bloques try-catch-finally en primer lugar permite identificar los puntos del programa en los cuáles se puede producir una excepción. Se deben usar excepciones en lugar de códigos de retorno. Las excepciones deben proporcionar suficiente información sobre el error y el momento en que se ha producido. Además, las excepciones no comprobadas son menos invasivas a nivel de código.

También se debe evitar pasar null como parámetro de una llamada a un método o devolverlo como valor de retorno. Esto introduce complejidad adicional debido a las comprobaciones necesarias.

Capítulo 8 - FRONTERAS

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.

Los tests ayudan a experimentar con el código externo viendo cómo puede cubrir nuestras necesidades. También permiten comprobar que las nuevas versiones de la librería siguen cumpliendo con nuestras necesidades.

Encapsular el conocimiento adquerido a través de los tests en un interfaz entre nuestro sistema y el código de terceros permite localizar en ún único punto las modificaciones debidas a posibles cambios en código que está fuera de nuestro control.

Capítulo 9 - TESTS UNITARIOS

En TDD 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.

Capítulo 10 – CLASES

Las clases se deben organizar situando en primer lugar las constantes públicas, después las variables estáticas privadas, variables de instancia privadas y, a continuación, los métodos. Los métodos privados se encuentran junto a los métodos públicos que los usan.

Se debe mantener la encapsulación de las clases. Las clases deben ser pequeñas y con un número reducido de responsabilidades. Se consigue una alta cohesión si todos los métodos de una clase hacen uso de todas sus variables privadas.

Las nuevas funcionalidades se deben de introducir extendiendo el sistema, no modificando el código existente. Las clases concretas contienen detalles de implementación y las clases abstractas expresan conceptos. La dependencia en clases concretas es un riesgo en caso de cambio. Según el principio de inversión de dependencia las clases deben depender de abstracciones.

Capítulo 11 – SISTEMAS

Es importante reconocer y separar las distintas responsabilidades de un sistema.

En primer lugar, se deben separar el proceso de construcción de un sistema de su uso. El proceso de construcción se encarga de crear y conectar entre sí los objetos necesarios para la ejecución de la aplicación. Se debe modularizar y separar de la lógica de ejecución, permitiendo una estrategia independiente para resolver las dependencias de la aplicación.

La inyección de dependencias permite que los objetos sólo se encarguen únicamente de la lógica de negocio. Un elemento contenedor se encarga de inyectar las dependencias de cada objeto de forma externa.

No se puede construir sistemas de forma correcta a la primera hay que ir implementando las historias de que se dispone, y después refactorizar y expandir el sistema para seguir implementando nuevas historias. TDD, refactorización y código limpio permiten esto a nivel de código.

A nivel de sistema, es difícil crecer de sistemas simples a sistemas complejos debido a las dependencias en la arquitectura que se usa. Para evitar estos problemas, se deben separar las distintos responsabilidades de un sistema. Para lograr esta separación se pueden usar proxys, frameworks AOP Java puros o aspectos AspectJ.

Se debe construir la lógica de la aplicación en base a POJOs a través de tests, e ir evolucionando de lo simple a lo complicado, interconectando los distintos aspectos necesarios.

Capítulo 12 – EMERGENCIA

Las siguientes 4 reglas dadas por Kent Beck permiten crear buenos diseños según se trabaja en ellos, conocer nuestro sistema y aplicar buenos principios de diseño:
  • Ejecutar todos los tests: Los tests verifican que el sistema se comporta según lo esperado. Al construir un sistema testeable se intenta que las clases sean simples y tengan un único propósito, y se trata de reducir el acoplamiento.
  • Eliminar la duplicación: La duplicación implica trabajo adicional, más riesgo y mayor complejidad.
  • Expresar la intención del programador: Usar un código lo más expresivo posible facilita el mantenimiento. Se deben escoger buenos nombres, funciones y clases pequeñas y tests bien escritos.
  • Minimizar el número de clases y métodos: Siguiendo las recomendaciones anteriores uno se puede exceder creando demasiadas clases pequeñas. Hay que tener cuidado y mantener un número reducido de clases.

Durante la refactorización de puede aplicar todo nuestro conocimiento para mejorar el diseño: aumentar la cohesión, reducir el acoplamiento, separar responsabilidades, reducir funciones y clase, escoger nombres mejores, etc.

Se dedicar tiempo a estas tareas después hacer que el software funcione. No se debe pasar a la siguiente tarea sin repasar lo hecho. Es imposible conseguir un código limpio a la primera, hay que repasarlo una y otra vez para conseguir mejorarlo progresivamente.

Capítulo 13 – CONCURRENCIA

La concurrencia es otro de los aspectos que pueden estar presentes en el código. Permite desacoplar lo que que ocurre de cuándo ocurre, y mejorar tanto el rendimiento como la estructura de una aplicación.

Desde el punta de vista estructural se puede percibir la aplicación como un grupo de computadoras colaborando entre sí, haciendo el sistema más fácil de entender y permitiendo separar las responsabilidades. La concurrencia permite mejorar los tiempos de respuesta y la eficiencia de una aplicación.

Se deben tener en cuenta las siguientes ideas sobre la concurrencia:
  • Introduce cierta sobrecarga.
  • Es compleja de manejar.
  • Los errores causados por ella son difícilmente reproducibles
  • Normalmente requiere cambios en el diseño.

El problema de la concurrencia es que los distintos hilos de una aplicación pueden entrelazarse siguiendo múltiples flujos de ejecución, lo que puede provocar problemas inesperados en situaciones normales.

Para defendernos de los problemas de concurrencia es importante que cada clase tenga una única responsabilidad,seperando la gestión de hilos se del resto del código. Hay que conocer las librerías que manejas y entender los distintos modelos de ejecución. Las secciones sincronizadas lo más pequeñas posibles.

Ejecutar pruebas de forma frecuente es la mejor manera de encontrar los posibles errores en el código. Sin embargo, es difícil hacer tests cuando hay concurrencia. Se deben tratar los fallos espúreos como posibles problemas de concurrencia e instrumentar el código para forzar la aparición de errores.

CONCLUSIONES FINALES

Todas estas buenas prácticas sobre cómo escribir código limpio se deben ejecitar de forma constante para adquirir buenos hábitos y ser capaces de hacerlo de forma natural. Siguiendo estas prácticas el autor nos promete conseguir un código con el que será mas fácil trabajar y hará mucho menos frustrante nuestro día a día.

Una idea que se repite en varios puntos del libro es que debemos de ser capaces de exprimir las capacidades de nuestro entorno de trabajo. Conocer nuestro entorno de trabajo nos puede simplificar enormemente nuestro trabajo, puesto que puede realizar por nosotros un montón de actividades de forma automática y nos ofrece una serie de ayudas que nos pueden ayudar en el día a día haciendo innecesarias algunas convenciones que sólo sirven para complicar el código.

Otra cosa que me ha gustado de este libro es la bibliografía en que se basa. No es excesivamente grande, pero me ha servido para sacar unas buenas recomendaciones sobre otros libros a leer:
  • Agile Software Development: Principles, Patterns and Practices, Robert Martin.
  • Implementation Patterns, Kent Beck.
  • Test Driven Development, Kent Beck.
  • Extreme Programming Explained: Embrace Change, Kent Beck.
  • Refactoring: Improving the Design of Existing Code, Martin Fowler et al..
  • Design Patterns: Elements or Reusable Object Oriented Software, Gamma et al..
  • The Pragmatic Programmer, Andrew Hunt, Dave Thomas.