jueves, 21 de marzo de 2013

Working Effectively With Legacy Code

Después de estudiar la forma de crear software de la mejor manera posible, usando Patrones de Diseño, evolucionando el diseño mediante TDD, cubriendo el código de pruebas y refactorizándolo cuando se presenta una oportunidad de mejora, me sigo topando con la cruda realidad. Ésta es que la mayor parte del tiempo lo paso trabajando con código ya hecho, por mí o por otras personas, y que no está desarrollado siguiendo las buenas prácticas que ahora intento seguir. Este código es difícil de entender y de mantener y, aunque quiero llevarlo a un estado parecido al código nuevo que desarrollo, esto me parece en ocasiones prácticamente imposible.

Una solución al problema de cómo mejorar este código legado es seguir los consejos que Michael C. Feathers presenta en su libro “Working Effectively With Legacy Code”. En él se presenta su visión sobre qué es el software legado, con que mentalidad enfrentarnos a él y qué técnicas podemos usar para combatir las malas prácticas que hay en él. Este artículo es un resumen de lo que este libro cuenta.

  1. Cambiando el software: El software se puede cambiar por varias razones: añadir funcionalidad, corregir un error, mejorar el diseño u optimizar el uso de los recursos del sistema. En todos los casos se quiere cambiar algún funcionamiento, pero se desea mantener la mayor parte del ya existente.
  2. Trabajando con realimentación: La forma de cambiar el sistema sin introducir errores es trabajar con la realimentación que proporcionan los tests unitarios que cubren el código. Estos tests permiten detectar cualquier comportamiento erróneo que se introduzca en las modificaciones que realicemos. El problema del código legado es que dichos tests no existen, y hay que hacer cambios para introducirlos rompiendo dependencias entre componentes, siendo extremadamente cuidadoso.
  3. Detección y separación: Existen dos motivos para romper las dependencias, detectar cómo afecta un componente a otro, o separar los componentes entre sí y poder ejecutarlos de forma independiente. De todas formas, los componentes deben colaborar entre sí y, para aliviar las dependencias en estas colaboraciones se pueden usar dobles de prueba que sustituyan al colaborador real.
  4. El modelo de costuras: El software no se puede tratar como un listado porque esto dificulta su comprensión y la reutilización de sus componentes. Para poder probarlo es necesario buscar formas de cambiar su comportamiento sin realizar ningún cambio sobre lo que se va a probar, puntos que el autor llama costuras. Estas costuras pueden ser habilitadas de distintas formas y, en función de ello, existen los siguientes tipos: de preprocesado, de enlazado y de objeto.
  5. Herramientas: A la hora de realizar cambios sobre el código legado hay una serie de herramientas que nos van a ayudar. Estas son las herramientas de refactorización automática, los frameworks de objetos “mock” (imitación), y los frameworks de prueba (xUnit, CppUnit, CppUnitLite, Fit, Fitnesse)
  6. No tengo mucho tiempo y tengo que cambiarlo: A veces no se dispone de tiempo para poner bajo tests la clase que tenemos que cambiar. En esos casos se pueden usar una serie de técnicas que, por lo menos, permiten tener bajo tests el código que introduzcamos al hacer los cambios. Estas técnicas consisten en crear una nueva clase o método, o crear una clase o método que envuelva el código existente.
  7. Hacer un cambio se eterniza: Cuando se están haciendo cambios es importante tener realimentación rápida sobre lo que está ocurriendo. Para ello es importante que los proyectos se puedan recompilar rápidamente de forma que no se invierta demasiado tiempo esperando. Para conseguir esta rapidez se deben usar interfaces y paquetes para separar las dependencias entre clases.
  8. Cómo añadir funcionalidad: Una vez que tenemos código bajo test podemos usar las técnicas descritas en este capítulo para añadir nueva funcionalidad de forma controlada. Estas técnicas son TDD y la programación por diferencia. La programación por diferencia, basada en la herencia de implementación, es bastante discutida, pero se puede utilizar de forma inicial a la hora de introducir nuevas funcionalidades y luego, gracias a los tests, ir mejorando el diseño de forma que se elimine su uso.
  9. No puedo incluir esta clase en un test: A veces no es fácil instanciar una clase en un test por distintos motivos. Estos pueden ser la dificultad de crear ciertos objetos debidos a los parámetros de su constructor o las dependencias de éste, que el test no se pueda ejecutar con una determinada clase que depende de una base de datos o parecido, que el constructor tenga efectos colaterales dañinos o que no seamos capaces de comprobar el trabajo que se hace en el constructor. Se presentan una serie de ejemplos donde se solucionan estos problemas a partir de unas útiles técnicas.
  10. No puedo ejecutar este método en un test: Una vez se puede instanciar una clase en un test sólo hemos empezado con lo que tenemos que hacer. Ahora hay que ejecutar alguno de sus métodos y a veces hay que superar problemas como que el método no sea accesible, que se necesiten parámetros difíciles de construir en su llamada, que tenga efectos colaterales perjudiciales o que no se pueda apreciar lo que hace en alguno de los objetos que utiliza. Se presentan una serie de ejemplos donde se solucionan estos problemas a partir de unas útiles técnicas.
  11. Necesito hacer un cambio. ¿Qué métodos debo probar?: Cada cambio en el software tiene una serie de efectos que es necesario considerar. Una técnica útil es hacer un boceto para ver de forma gráfica lo que se ve afectado por cada cambio. Un buen código creará estructuras simples. Es importante saber que los efectos se pueden propagar de tres formas: los valores de retorno de un método, los objetos pasados como parámetros a un método y las variables estáticas o globales. Se pueden simplificar estos bocetos mediante la eliminación de duplicación, y esto lleva a decisiones más sencillas sobre los tests a realizar.
  12. Necesito hacer muchos cambios en un área. ¿Tengo que romper las dependencias para todas las clases implicadas?: A veces es conveniente buscar un lugar donde probar vario cambios a la vez en lugar de poner bajo tests todas las clases implicadas en un cambio. Para ello, a través de los bocetos de efectos se pueden buscar puntos de intercepción, lugares donde se puede probar un determinado cambio. Algunos puntos, donde el boceto de efectos se estrecha, son especialmente indicados para probar varios cambios a la vez.
  13. Necesito hacer un cambio pero no se qué tests escribir: En código legado necesitamos tener tests que permitan preservar el comportamiento del sistema tras las modificaciones. Esos tests se llaman tests de caracterización, y son tests sencillos que simplemente reflejan lo que el sistema hace en este momento. A partir de estos tests se puede refactorizar el código para mejorarlo.
  14. Las dependencias en librerías me están matando: Es importante no confiar demasiado en una librería concreta y llenar el código de llamadas a métodos de esa librería. El uso de forma directa de una librería no favorece la creación de tests.
  15. Mi aplicación está llena de llamadas al API: La mayoría de los sistemas contienen una lógica principal que va más allá de llamadas a la API de una o más librerías. Es importante que el diseño preserve esta lógica para poder hacer buenos tests. A esta separación se puede llegar mediante dos aproximaciones: envolver el API o hacer una extracción basada en responsabilidades.
  16. No entiendo el código suficientemente bien para cambiarlo: Para entender el código existente de cara a hacer un cambio, se pueden usar algunas técnicas de bajo nivel, como tomar notas, hacer bocetos, marcar listados de código para resaltar los aspectos importantes del mismo, hacer una refactorización informal y luego deshacerlo o borrar el código que no se usa.
  17. Mi aplicación no tiene estructura:Las aplicaciones tienden al desorden cuando todo el equipo no es consciente de dicha arquitectura. Para asegurarse que el equipo entiende la arquitectura se puede recurrir a contar la historia del sistema para obtener una visión simple del sistema en su conjunto, usar CRC o “naked CRC” para comunicar el diseño de la aplicación o escrutar las conversaciones sobre el sistema para comprobar que cuando se hace un abstracción al hablar esta tenga una correspondencia en el sistema. Es importante resaltar que el diseño es una tarea que no termina mientras siga habiendo cambios.
  18. Mi código de test está en medio: Tener el código de test mezclado con el código puede no ser bueno por razones de claridad o de recursos. Para separarlo se pueden usar convenciones de nombrado o situar uno y otro en archivos separados.
  19. Mi proyecto no es orientado a objetos. ¿Cómo hago cambios seguros?: Principalmente se habla de lenguajes procedurales. Para estos lenguajes se pueden usar las costuras de procesado o de enlazado, pero si el lenguaje tiene una extensión que sea orientada a objetos, se puede hacer modificaciones que permitan usar las costuras de objeto, que son mucho más potentes.
  20. Esta clase es muy grande y no quiero que crezca más: Las clases grandes generan confusión, dificultad a la hora de planificar las tareas de los desarrolladores y son difíciles de probar. Estas clases tienden a atraer dentro de ellas las nuevas modificaciones y no paran de empeorar. La solución es refactorizar estas clases para hacer que cumplan con el Principio de Única Responsabilidad (SRP). Esta refactorización se debe hacer separando las responsabilidades de cada clase. Para ver estas responsabilidades se pueden agrupar sus métodos, buscar métodos ocultos, buscar las decisiones sobre la misma que puedan cambiar, buscar relaciones internas usando bocetos de propiedades para observar las agrupaciones internas, buscar la responsabilidad principal, hacer refactorizaciones de prueba o centrarse en la tarea que tenemos entre manos. La estrategia a seguir cuando se han identificado las distintas responsabilidades es separarlas a medida que sea necesario en vez de hacer todo a la vez. La mejor táctica para hacerlo sería aplicar el principio de responsabilidad única a nivel de implementación en vez de a nivel de interfaz.
  21. Estoy cambiando el mismo código por todos los sitios: Muchas veces hacemos un cambio y creemos que hemos terminado, pero nos damos cuenta de que debemos hacer este mismo cambio en otras partes del sistema donde el código está duplicado. A través de la refactorización podemos ahorrarnos esta molestia eliminando las duplicidades, consiguiendo además que emerja un mejor diseño y favoreciendo la ortogonalidad del sistema, consiguiendo partes del sistema independientes entre sí.
  22. Necesito cambiar un método monstruoso y no puedo escribir tests para él: Para abordar los cambios en estos métodos monstruosos podemos usar las herramientas de refactorización automáticas, evitando en este paso todo cambio adicional que no soporten. Cuando se refactoriza de forma manual debemos ser mucho más cuidadosos y, para ello, introducir variables que nos permitan sentir los cambios en el sistema, extraer sólo aquello que conocemos, separar el código principal de lo secundario o extraer un objeto método. Se pueden introducir cambios estructurales mientras se refactoriza haciendo que los métodos sean un esqueleto de la lógica que contienen, buscando secuencias de código, extrayendo a la clase actual en un primer paso en vez de llevar el código a otra clase y extrayendo en pequeñas piezas de código.
  23. ¿Cómo sé que no estoy rompiendo nada?: Se puede reducir el riego al editar los cambios si se es plenamente consciente de lo que se está editando, editando con un único objetivo, manteniendo las firmas de los métodos y apoyándose en el compilador para identificar los cambios que se deben hacer.
  24. Nos sentimos abrumados. Esto no va a mejorar: La programación puede ser divertida, aunque trabajar con código legado sea difícil. Rehacer el proyecto desde cero tampoco es una solución porque hay que mantener el mismo avance que el proyecto y esto es muy difícil. Hay que buscar la motivación para trabajar sobre nuestro sistema, solucionando los problemas más inmediatos y haciendo evidente la mejora que podo a poco se produce en el sistema.
  25. Técnicas para romper las dependencias: En este capítulo se definen técnicas para romper las dependencias, pero que no son exactamente refactorizaciones, puesto que están pensadas para hacerse sin tests, con el fin de poder introducir estos tests que nos faltan.

1 comentario:

  1. Muchas Gracias por el resumen Jorge. buen aporte. Se consigue el pdf tradicido al español?

    ResponderEliminar