14 marzo 2015

Clean Code

A lo largo de la vida no hay muchos libros que cambien realmente tu manera de pensar y hacer las cosas. En mi caso puedo contar con los dedos de una mano los libros de ese tipo que me he encontrado: "Computer Networking: a Top Down Approach" de Kurose y Ross, "Security Engineering: A Guide to Building Dependable Distributed systems" de Ross J. Anderson, y el libro del que trata este artículo "Clean Code: a Handbook of agile Software Craftmanship" de Robert C. Martin.

Supe de este libro en una de las conferencias de la PyConEs de 2013. En aquella conferencia hablaron de como hacer código sostenible en el tiempo. El tema me interesaba porque estaba preocupado por un fenómeno que todo programados se encuentra tarde o temprano: incluso con Python, cuando tu código crece se hace más difícil de mantener. Había programado aplicaciones que me resultaban difíciles de entender cuando tenía que revisarlas apenas unos meses después. Muchos años antes me había pasado a Python para evitar ese mismo problema en Java y Perl. En la conferencia aseguraban que los principios explicados en el libro ayudaban a prevenir ese problema. Esa es la razón por la que lo leí y he de admitir que tenían razón.

La lectura de este libro puede ser chocante. Hay tantas prácticas que sumimos correctas pero que sin embargo está terriblemente equivocadas que lo normal es leer por primera vez ciertos pasajes con una mezcla de sorpresa e incredulidad. Cosas como afirmar que los comentarios en el código son un reconocimiento de tu fracaso para hacer comprensible tu código suenan extrañas la primera vez que se leen pero luego se acaba comprendiendo que el autor está en lo cierto.

Los ejemplos del libro no están en Python sino en Java, sin embargo no creo que ningún programador de Python tenga problema alguno en comprender los conceptos que allí se explican. Algunos de los conceptos son un poco "javeros" pero muchos otros son útiles para desarrolladores en Python. Algunos de los conceptos principales son:

  • Los nombres de tus funciones deberían explicar claramente lo que hace la función. No se permiten abreviaturas en dichos nombres.
  • Una función sólo debería hacer una única cosa. Una función sólo debería tener un propósito. Por supuesto, una función puede tener muchos pasos pero el conjunto de ellos debería estar enfocado a conseguir el objetivo de la función y cada paso debería implementarse en su propia función. Una consecuencia de este enfoque es que es más fácil desarrollar tests unitarios para este tipo de funciones.
  • Las funciones deberían ser cortas: 2 líneas es estupendo, 5 está bien, 10 regular y 15 es muy pobre.
  • Los comentarios de código debería utilizarse únicamente para explicar decisiones de diseño pero nunca para explicar lo que hace el código.
  • No se deben mezclar niveles de abstracción en la misma función, lo que significa que no se debe llamar directamente a la API de Python mientras que otros pasos de la función llaman a tus propias funciones. En vez de eso es mejor meter tu llamada a la API dentro de una de tus propias funciones.
  • Ordena el código de manera que se pueda leer el conjunto de las implementaciones desde arriba en sentido descendente.
  • Hay que reducir todo lo pasible la cantidad de argumentos que se pasan a las funciones. Funciones con 1 argumento están bien, con 2 regular y con 3 probablemente deberías darle otra vuelta.
  • No te repitas (DRY, Don't Repeat Yourself, aunque este concepto ya lo conocía antes de leer este libro).
  • Las clases deberían ser pequeñas.
  • Las clases sólo deberían tener una razón para cambiar (Principio de Responsabilidad Única). En mi opinión este principio es una extensión lógica del de "un único propósito" de las funciones.
  • Idealmente, los atributos de las clases deberían ser utilizados por todos los métodos de la clase. Si nos encontramos atributos que sólo utilicen un pequeño subconjunto de métodos, deberíamos preguntarnos si esos atributos y esos métodos podrían ir en una clase separada.
  • Las clases deberían estar abiertas a extensiones pero cerradas para modificaciones. Eso significa que deberíamos incorporar nuevas funcionalidades heredando las clases existentes no modificándolas. De esa manera se reduce el riesgo de romper las programas cuando incluimos nuevas funcionalidades.
  • Haz TDD o condénate a ti mismo al infierno de meter modificaciones en tu código con la incertidumbre de si no romperás algo sin darte cuenta.
Hay muchos más conceptos, todos completamente explicados con ejemplos, pero estos que he explicado son los que tengo en la cabeza cuando escribo código.

Para probar si los principios de este libro estaban en lo cierto, desarrollé una aplicación llamada Geolocate, siguiendo estos conceptos y los de TDD. Al principio fue difícil cambiar mis hábitos a la horas de escribir código, pero conforme mi código fue creciendo me fui dando cuenta resultaba mucho más fácil encontrar errores y corregirlos de lo que me había resultado en proyectos anteriores. Además, cuando mi aplicación adquirió un tamaño respetable, la dejé aparte durante cinco meses para ver cómo de fácil resultaba retomar el desarrollo después de que pasase tanto tiempo. Fue asombroso. Aunque en mis anteriores proyectos habría necesitado varios días para comprender del todo un código tan grande, esta vez comprendí el funcionamiento del código en apenas una hora.

Mi conclusión es que este libro es imprescindible, dado que te permite mejorar dramáticamente la calidad de tu código y tu paz mental a la hora de mantenerlo después.

08 enero 2015

Herramientas de cobertura de tests en Python

Supongo que hay muchas metricas para saber como de efectivos son nuestras pruebas unitarias a la hora de cubrir todas las casuísticas posibles en nuestros desarrollos. En el futuro tengo pensado formalizar mis conocimientos en TDD, pero por ahora sólo estoy jugando con los conceptos por lo que sólo sigo una métrica muy simple: si mis test ejecutan todas las líneas de código de mi programa, entonces debo estar enfocando bien mis tests.

Pero ¿cómo saber si tus tests ejecutan todo tu código?. Conforme crece el tamaño de tu código, también lo hace la cantidad de tus tests, por lo que es fácil dejar por olvido un fragmento de código sin probar. Ahí es donde entran en juego las herramientas de cobertura de tests.

Esas herramientas hacen seguimiento de la ejecución de tus tests y toman nota de las líneas de código visitadas. De esa manera, tras la ejecución de los tests se pueden ver las estadísticas sobre qué porcentage del código dse prueba en realidad y cual no.

Vamos a ver dos maneras de analizar la cobertura de tests de tus proyectos: desde la consola y desde tu IDE.

En caso de estar en un equipo Ubuntu, deberíamos instalar el paquete "python3-coverage" para obtener la herremienta de consola:

dante@Camelot:~/project-directory$ sudo aptitude install python3-coverage


Una vez instalado, python3-covergae debe ser ejecutado para llamar a tus pruebas unitarias:

dante@Camelot:~/project-directory$ python3-coverage run --omit "/usr/*" ./run_tests.py

Utilizo el parámetro "--omit" para mantener la carpeta "/usr" fuera de mis informes de cobertura. Antes de usar ese parámetro, las librerías externas que mi programa iba llamando se incluían en el informe. Obviamente, no creo tests para las librerías externas dado que no toco su implementación, por lo que incluirlas en el informe de cobertura sólo haría al mismo más difícil de leer. El script "run_test.py" es el mismo que expliqué en mi artículo sobre unittest.

Supongamos que todos nuestros tests se ejecutan correctamente. Se puede generar un informe de cobertura tanto en html como en xml. Para generar un informe en formato html, sólo hay que
ejecutar:

dante@Camelot:~/project-directory$ python3-coverage html


Este comendo genera una carpeta llamada "htmlcov" donde el se guarda el informe generado en html (su página de entrada es index.html). Lo único un poco molesto es que la carpeta htmlcov debe ser vorrada manualmente antes de generar un nuevo informe. Además, es bastante aburrido tener que buscar el fichero index.html para pichar en él y abrir el informe. Esa es la razón por la que prefiero usar un script que automatiza todas esas aburridas tareas.

#!/usr/bin/env bash

python3-coverage run --omit "/usr/*" ./run_tests.py
echo "Removing previous reports..."
rm -rf htmlcov/*
rmdir htmlcov
echo "Removed."
echo "Building coverage report..."
python3-coverage html
echo "Coverage report built."
echo "Let's show report."
(firefox htmlcov/index.html &> /dev/null &)
echo "Bye!"

Al ejecutar ese script (lo suelo llamar "run_tests_and_coverage.sh") consigo que el navegador Firefox se habra mostrando el informe de cobertura recién creado.

Si usas un IDE, lo más probable es que ya incluya algún tipo de herramienta de análisis de cobertura. PyCharm incluye un analizador de cobertura en su versión profesional. En realidad, con PyCharm se consigue más o menos lo mismo que con las herramientas de consola que vimos antes, pero  de manera integrada con el editor y por tanto mucho más cómoda de utilizar.

A nivel de aplicación, la configuración por defecto debería ser suficiente:



Supongo que si no se tiene instalado el paquete de sistema "python3-coverage" se debería marcar la opción "Use bundled coverage.py" para usar así la herramienta que trae integrada de manera nativa PyCharm. En mi caso, no he notado diferencia alguna en marcar o desmarcar dicha opción (aunque obviamente yo ya tengo el paquete "python3-coverage" instalado).

El único truco a recordar es que ejecutar los tests con soporte para análisis de cobertura implica usar el botón dedicado a esa tarea. Ese butón está localizado junto a los de "Run" y los de "Debug":



Tras usar ese botón, aparecerá un panel resumen a la derecha del editor principal con los porcentajes de cobertura de cada una de las carpetas del proyecto. El panel esquemático de la izquierda mostrará los niveles de cobertura de cada uno de los ficheros de código fuente. Además, el panel principal de edición marcará con color verde las líneas de código cubiertas y en rojo las que no.

Hacer click en la imagen para mostrarla a tamaño original.

Mantener la cobertura de los códigos tan cerca como sea posible del 100% es unos de los mejores indicadores de que tus tests está bien diseñados. Llevar a cabo ese análisis desde la consola o desde el IDE es una cuestión de gustos, pero ambas opciones son lo suficientemente cómodas y potentes como para usarlas a menudo.