Cómo plantarle cara al Legacy Code

Cómo plantarle cara al Legacy Code
Sin comentarios Facebook Twitter Flipboard E-mail

Aunque podemos tener una discusión muy profunda sobre lo que es y lo que no es Legacy Code, hay un aspecto concreto que para mi puede marcar la diferencia y es la confiabilidad con la que somos capaces de realizar cambios sobre nuestro producto a medida que estos son requeridos por negocio.

En una base de código en la que ciertas tarjetas de nuestro panel Kanban se eluden sistemáticamente por parte del equipo de desarrollo haciendose los disimulados, este es un claro síntoma de que hay una porción importante de nuestro sistema sobre la que nadie quiere asumir los riesgos que comporta su modificación. Cuando esto sucede, siempre se repiten algunas situación que son verdadores _spots_ a detectar: Controladores o clases muy grandes, pocos tests y/o malos tests, acoplamiento, código duplicado, etc.

Ante esta situación, es muy probable que lleguemos a sentir la presión de negocio cuando no somos capaces de incorporar nuevas funcionalidades lo suficientemente rápido o cuando, plantear un test A/B sobre el producto parece como una gran locura que llevaría semanas de planificación.

¿Que vamos a hacer entonces con nuestra base de código? ¿La tiramos a la basura y volvemos a empezar de cero? ¿Vamos a ser capaces de solucionar los problemas de la base de código existente? ¿Nos compramos el libro de Michael Feathers Working Effectively with Legacy Code y transformamos nuestro producto?

Sobre este escenario analizaremos algunos de los aspectos más relevantes a la hora de afrontar una situación de este estilo y que, gracias a la experiencia de estos últimos años con Agile@Work, hemos podido comprobar en primera persona al colaborar con distintos equipos de desarrollo de gran tamaño.

Saber ver las señales

Lo primero es lo primero, y si no somos capaces de ver el mal endémico que sufre nuestra base de código, dificilmente pensaremos en ponernos las pilas y comenzar a darle solución.

En las siguientes secciones analizaremos algunas de las áreas en las que buscar olores, sean tanto de código como de infraestructra o colaboración.

Calidad percibida

Para tener una idea de lo sana que está nuestra base de código, al margen de las metricas que podemos obtener analizando nuestro código con SonarQube o herramientas similares, también podemos fijarnos en otros heurísticos que, aunque obvios y simples, pueden resultar muy interesantes a la hora de detectar problemas.

Con el fin de que sirva de ejemplo, podemos comenzar fijándonos en si el número de bugs o historias de usuario recurrentes es muy elevado. Parece obvio, pero necesitamos algún mecanismo de control que detecte esta situación en la herramienta que utilicemos de gestión de proyectos. Un número elevado de bugs es síntoma de falta de pruebas automáticas, demasiado acoplamiento entre las distintas partes del sistema que causa efectos colaterales ante cambios o incorrecta configuración del entorno de desarrollo y/o producción.

¿Qué percepción tienen nuestros usuarios acerca de la calidad del mismo? ¿Están contentos con su desempeño? ¿Se han acostumbrado a los continuos errores de funcionamiento? Todas estas son cuestiones que debemos plantearnos antes de tomar una decisión relativa a cómo comenzar a mejorar nuestro producto.

Código

Pueden ser los olores más complicados de detectar en función de lo sutiles que estos sean. La buena noticia es que aquí tenemos la inestimable ayuda del gran Martin Fowler y de la magnífica guía de referencia que es su Refactoring: Improving the Design of Existing Code.

En este libro, al margen de explicarnos como sistematizar el proceso de refactoring de código en general, nos ofrece un catálogo de olores que podemos encontrar en nuestro código, dándonos pistas de como atajarlos (en Source Making podemos encontrar este catálogo con ejemplos ilustrativos y una presentación muy trabajada).

Si nuestro código no tiene un buen naming, tiene clases grandes, métodos largos y muchos parámetros, entre otros olores, entonces estamos en una fase muy inicial y necesitamos centrar nuestro esfuerzo en mejorar la estructura básica del código antes de afrontar otros retos de mucha más envergadura como hacer cambios de diseño o incluso de arquitura. El diseño dificilmente emergerá si el sustrato no es el adecuado.

Del análisis de este tipo de situaciones viene mi convencimiento y afirmación categórica de que nunca debemos solucionar problemas de diseño con frameworks o con una tecnología concreta, ya que en lugar de solucionarlos, acabaremos con un mal diseño incorrectamente distribuido entre las distintas capas de nuestra nueva arquitectura.

Un claro ejemplo es cuando, maravillados por el hype de los microservicios o del nuevo framework de turno, pensamos que podemos transformar nuestra base de código completa y solucionar todos nuestros problemas de un plumazo, cuando lo que conseguimos habitualmente es generar otros muchos nuevos :/

Microservices

Si estamos intentando mejorar nuestra base de código para deshacernos de los olores más básicos y pestilentes, deberíamos de hacer uso de herramientas como nuestro IDE y de los refactorings automáticos que implementa a la hora de aplicar lo aprendido en el catálogo de olores y conjunto de refactorings de Fowler.

Una vez atajado el problema, y si ya podemos considerar nuestro código como código limpio, podemos comenzar a pensar en pasar al siguiente nivel en el que podremos comenzar a distinguir nuevos olores más sutiles que ya nos hablan de diseño, acoplamiento, patrones y otros aspectos que afectan a cómo nuestras entidades de negocio colaboran entre si en nuestro modelo rico.

En este nuevo nivel, ya necesitamos herramientas más complejas y potentes como son los patrones de diseño. Para comenzar a aprender qué patrones existen y como es mejor utilizarlos, recomiendo la referencia de Head First Design Patterns y de nuevo la documentación sobre patrones que podemos encontrar en Source Making.

Siguiendo esta misma línea, un objetivo interesante a medio/largo plazo debería consistir en conseguir una arquitectura en la que nuestro modelo fuera el núcleo de todo y las fronteras y relaciones con otras áreas de negocio fueran los más claras y desacopladas posibles. Para ello, tanto Domain Driven Design (DDD) como Hexagonal Architecture o Clean Architecture son referencias que debemos tener a la vista. Si somos capaces de definir las fronteras de nuestra aplicación, seremos capaces de desacoplar sus áreas de negocio mediante servios o lo que requiera nuestra futura arquitectura, consiguiendo así aplicar técnicas como Microservices o CQRS de una forma efectiva.

Como referencia, podemos tomar el libro de Vaughn Vernon sobre Implementing Domain-Driven Design, o la última versión revisada y simplicada del mismo.

Pruebas automatizadas

La ausencia o mala calidad de las pruebas automatizadas es otro de los sintomas de que nuestro proyecto es legacy. En esta situación poco podemos refactorizar al no contar con la red de protección que nos dan los tests.

Una posible estrategia que deberíamos plantearnos en esta situación es ir añadiendo progresivamente tests de andamiaje que sustenten la funcionalidad actual (defensa del valor actual antes errores que puedan llegar a los usuarios que ya están utilizando la aplicación). Hacer un andamiaje de test consiste en construir tests lo más end-to-end posible con el fin de asegurar que no rompemos nada en esta parte mientras hacemos pequeños cambios en la estructura del código sin alterar el comportamiento observable del mimo. Ojo!!! Cualquier otra situación no es refactorizar!!!

Por otra parte, el código que vayamos extrayendo a las nuevas clases que emerjan deberá de ir ya acompañado de sus correspondientes nuevos tests, esta vez sí unitarios. Cuando finalicemos el proceso, ya podemos borrar los tests de andamiaje que sólo sirven temporalmente para asistirnos en las tareas de refactorización.

Otra situación que nos podemos encontrar es que sí que existan tests, pero que sean todos muy end-to-end y/o muy acoplados al diseño mediante el uso excesivo de mocks (ver detalles sobre qué es el mocking a la hora de hacer tests en el siguiente ariculo). Esta situación es de nuevo un olor, ya que la falta de un diseño adecuado y de una arquitectura que permita desacoplar las distintas capas de funcionalidad, hacen que el testeo unitario sea muy complejo o incluso imposible y que acabemos haciendo tests end-to-end como único mecanismo de testeo global del proyecto (todo ello apalancado con mocks que lo único que hacen es complicar posteriormente las tareas de refactorización sin ofrecer garantías de corrección).

La solución en estos casos viene por realizar un ejercicio similar a cuando no tenemos tests e ir borrando progresimente toda esta marea de mocks que tan poca confianza nos ofrece en general.

Con el fin de adentrarnos en el apasionanente mundo de los tests, podemos contar con referencias como Agile Testing de Lisa Crispin, JUnit Recipes de J.B. Rainsberger o Effective Unit Testing de Lasse Koskela.

De nuevo, para el medio/largo plazo nos podemos fijar como objetivo el aprendizaje de técnicas como TDD que nos ayuden a desarrollar nueva funcionalidad dirigida por las pruebas, forzando de alguna forma la testeabilidad de nuestro código desde su concepción y consiguiento mantener la presión sobre el diseño y su evolución a golpe de test unitario. Para ello Kent Beck ha sido desde siempre la referencia con su Test-Driven Development by Example, aunque personalmente me gustan mucho también el Growing Object-Oriented Software Guided by Tests de Steve Freeman o el Refactoring to Patterns de Joshua Keriesvsky.

Entrega de valor

Cómo entregamos valor a nuestro usuarios es un aspecto fundamental que debemos cuidar en cualquier de nuestros desarrollos. Complejos scripts de despliegue o interminables checklists a seguir manualmente son algunos de los olores que podemos detectar. Al final, cualquiera de ellos desemboca en multitud de ineficiencas incluyendo el retardo temporal en hacer un despliegue, errores por operaciones manuales o, incluso, imposibilidad manifiesta de hacer un despliegue con garantías en producción.

La automatización completa del proceso puede resultar vital en la mayoría de escenarios, teniendo como objetivo a largo plazo y con la mejora de nuestra base de código y de tests, el poder acercarnos a los principios de Continuous Delivery.

Pautas de desarrollo

En definitiva se trata de definir cómo se trabaja en nuestro equipo, qué normas de codificación seguimos, qué herramientas empleamos para editar, construir, desplegar y probar nuestras aplicaciones, creando un estándar y un consenso dentro de nuestro equipo de desarrollo que es capaz de seguir cualquiera de sus miembros actuales o futuros.

Cómo actuar

Llegó el gran momento. Hemos sabido ver las señales y conocemos los problemas que afrontamos, situación que no está nada mal como punto de partida. ¿Qué hacemos ahora? Nuestro aplicativo debería de seguir funcionando si no queremos que la gente de negocio nos corte el cuello, pero seguir añadiendo funcionalidad sin ir mejorando el diseño del código puede llevarnos a una situación peligrosa en la que atender a nuevos requisitos resulte cada vez más lento. En este escenario, pocas son las veces que podemos permitirnos una reescritura completa del producto (tirarlo y volverlo a desarrollar), así que sólo nos queda en muchos casos la opción de ir mejorando progresivamente.

Uno de los riesgos de seguir esta opción de cambio progresivo es entrar en lo que llamamos el _Big Refactor_. Esto sucede cuando vemos tantas cosas a mejorar que nos ponemos a solucionarlas todas al mismo tiempo y cuando nos damos cuenta hemos realizado cambios que afectan a todo el sistema (lo que se conoce como liarla bien parda). Debemos evitar esta situación y centrarnos en único aspecto a mejorar en cada iteración, contando siempre con la red de protección necesaria para cada caso en forma de tests.

Para poder ir mejorando el diseño de forma progresiva, podemos apoyarnos en técnicas como el Parallel Change, el cual nos guía en el proceso de generar nuevo diseño en paralelo al diseño existente, de forma que iremos abandonando el diseño viejo poco a poco hasta que no quede nada de código en esta parte y sea el nuevo diseño el que quede instalado. En estos escenarios de cambio, librerías como Scientist nos pueden ayudar en el proceso aplicando un concepto muy similar al de _seam_ introducido por Michael Feathers en su Working Effectively with Legacy Code.

Todas estas técnicas comportan conocimientos sólidos de lo que buscamos cuando hablamos de código limpio, refactoring o patrones de diseño/implementación y no deben ser abordadas a la ligera.

Deberíamos recordar pues, que la estrategía ganadora es siempre la iterativa e incremental y que siempre debemos comenzar modestamente mejorando el sustrato de nuestro código y la forma que tenemos de hacer las cosas en nuestro entorno antes de aplicar cambios de más calado que afecten al diseño del código o a la infraestructura.

Refactorizar, igual que hacer testing con TDD supone en la mayoría de los casos el ir paso a paso: _Baby steps carefully_ es uno de los mantras de Kent Beck o Martin Fowler. Aprendamos pues de su experiencia y vayamos poco a poco.

Comentarios cerrados
Inicio