¿Cómo comer un elefante [monolítico]?

Dirigir la evolución de un sistema de software "heredado" es un problema difícil de resolver. El tamaño de la base de código, el alto grado de acoplamiento entre los componentes y la deuda técnica acumulada a lo largo de los años pueden ser abrumadores. A menudo, la falta de pruebas automatizadas hace que cada cambio técnico sea arriesgado y potencialmente perjudicial para el negocio. Piense en los clientes que no pueden realizar pedidos debido a una actualización de la biblioteca.

¿Cómo abordan los equipos esta incómoda situación? Cuando tienen la tarea de realizar un importante ejercicio de refactorización o migración, ¿cómo diseñan un plan de transición seguro? ¿Cómo comienzan y miden el progreso?

En esta publicación, presentamos un estudio de caso para mostrar que un proceso basado en datos, basado en el método de meta-pregunta-métrica, puede dar claridad y confianza al equipo. Después de presentar el enfoque, describimos algunas de las herramientas utilizadas para recopilar los datos durante todo el proceso (antes y durante la migración).

Caso de estudio

Recientemente ayudamos a un equipo que desarrolla un producto B2B con una arquitectura típica de varios niveles. Varias aplicaciones web interactúan con una API REST, conectada con servicios empresariales y de acceso a datos. El software tiene aproximadamente 7 años. Si bien hemos trabajado en sistemas mucho más grandes, antiguos y complejos, esta aplicación ya tiene un tamaño decente, con decenas de puntos finales.

Después de una revisión inicial del código, identificamos problemas comunes para los sistemas heredados: dependencias no administradas y desactualizadas, paquete y estructura de archivos erosionados, falta de pruebas automatizadas, falta de automatización de compilación.

Se nos pidió proponer una estrategia de migración técnica y trabajar con el equipo para implementarla. Los principales impulsores comerciales para la migración fueron el costo de mantenimiento y los riesgos de seguridad asociados con bibliotecas y marcos obsoletos.

Nuestra recomendación fue no dividir una aplicación monolítica en una federación de microservicios. Los microservicios distribuidos habrían agregado complejidad sin ningún beneficio real. En cambio, aconsejamos refactorizar el monolito heredado en un monolito más manejable y robusto. Para implementar esta estrategia, diseñamos un proceso incremental:

  • Crea el esqueleto del nuevo monolito.
  • Identificar contextos acotados y extraerlos uno por uno.
  • Durante la migración, los usuarios interactúan con servicios heredados y "frescos".
  • Después de la migración, se eliminaron los puntos finales obsoletos y el código muerto.
  • Para implementar este proceso de manera segura, escriba pruebas automatizadas (aplicando técnicas BDD) para describir el comportamiento actual de cada contexto acotado. Estas pruebas nos permiten validar que el comportamiento de los sistemas antiguos y nuevos es el mismo.

En el diagrama anterior, vemos:

  • El monolito heredado (1). Algunos de los puntos finales y del código ya no se usaban (pero no sabíamos cuántos).
  • Diferentes clientes (2) que interactúan con el sistema de fondo.
  • El nuevo monolito (3), construido sobre una estructura limpia, con dependencias gestionadas. Inicialmente, este nuevo backend no contiene ningún punto final. Con el tiempo, los puntos finales y los servicios de soporte se migran a través de esta nueva base de código.
  • API Gateway (4), que presentamos para habilitar el proceso de migración incremental. La puerta de enlace enruta las solicitudes HTTP al monolito apropiado. Al comienzo de la migración, todas las solicitudes se enrutan al sistema heredado. Al final, todas las solicitudes se envían al nuevo sistema.
  • El arnés de prueba BDD (5), que es un conjunto de pruebas automatizadas que describen el comportamiento previsto de la aplicación de fondo. Las pruebas se escriben al comienzo del proceso para describir el comportamiento del monolito heredado. Durante la migración, a medida que los puntos finales y el tráfico se mueven sobre el nuevo sistema, el arnés de prueba se utiliza para verificar que el comportamiento del sistema no se haya alterado.
  • También se crea un conjunto de pruebas de extremo a extremo (6) para validar el sistema a través de la capa de interfaz de usuario. Mientras que las pruebas en la capa API deben ser exhaustivas, se implementa un número menor de pruebas de extremo a extremo (generalmente se centra en el "camino feliz").

¿Cómo empezamos?

Describir el proceso de migración deja una serie de preguntas abiertas. ¿Cuánto tiempo necesitará el equipo para implementarlo? Y durante el proceso, ¿cómo podrá el equipo medir el progreso y revisar el tiempo de finalización esperado?

Responder estas preguntas es muy importante para brindar visibilidad a la gerencia y claridad al equipo. Sin él, cualquiera se sentiría incómodo al comenzar el ejercicio. Esto es algo que hemos observado muchas veces: cuando los equipos no saben el tamaño del elefante, les resulta muy difícil comenzar a morder.

Enmarcando el problema con Goal-Question-Metric

Para ayudar al equipo, aplicamos el método Objetivo-Pregunta-Métrica. En este caso particular, teníamos dos objetivos:

  • Al evaluar la situación antes de la migración, nuestro objetivo era estimar el esfuerzo general. Se nos ocurrieron 5 preguntas y para cada pregunta identificamos una lista de métricas.
Objetivo Estimar el esfuerzo para realizar la refactorización principal
  Pregunta ¿Qué tan "grande" es el producto?
    Número métrico de escenarios de extremo a extremo
    Número métrico de componentes de la interfaz de usuario (aplicaciones, páginas, componentes)
    Número métrico de puntos finales REST
    Número métrico de entidades persistentes
    Número métrico de consultas DB
    Número métrico de archivos fuente
    Número métrico de confirmaciones en el historial de git
    Distribución de edad métrica de archivos fuente
  Pregunta ¿Cuáles son las partes más importantes de la aplicación?
    Clasificación métrica de puntos finales REST por uso (de registros)
    Métrica Impacto empresarial estimado de los escenarios
  Pregunta ¿Qué tan "significativa" es la refactorización?
    Número métrico de bibliotecas / marcos para reemplazar
    Número métrico de bibliotecas para actualizar
    Delta métrico entre las versiones actuales y de destino de libs
    Número métrico de archivos de origen que deben modificarse
  Pregunta ¿Qué tan "seguros" podemos hacer la refactorización?
    % Métrico de escenarios de extremo a extremo con pruebas automatizadas
    % Métrico de puntos finales REST con pruebas automatizadas
    Cobertura del código métrico
    Tiempo métrico requerido para probar manualmente la aplicación
    Metric Developer sentiment
  Pregunta ¿Cuánto del código todavía se usa?
    % Métrico de puntos finales REST todavía utilizados en escenarios de uso
    % Métrico del código cubierto al pasar por escenarios
    Estimación métrica del desarrollador
  • Durante la migración, nuestro objetivo era seguir el progreso y actualizar el esfuerzo restante. Se nos ocurrieron 2 preguntas, una vez más vinculadas a métricas específicas.
Meta Seguimiento del progreso de la refactorización
  Pregunta ¿Cuánto mejoramos la red de "seguridad"?
    % Métrico de escenarios de extremo a extremo con pruebas automatizadas + delta
    % Métrico de puntos finales REST con pruebas automatizadas + delta
    Cobertura del código métrico + delta
    Metric Developer sentiment
  Pregunta ¿Cuántos "servicios" extrajimos y migramos?
    % Métrico de puntos finales REST (con agregados) migrados
    Número métrico de archivos fuente eliminados de la versión
    Tiempo métrico dedicado a cada automatización de prueba
    Tiempo métrico dedicado a cada extracción

Extrayendo métricas

Con la estructura GQM en su lugar, tuvimos que encontrar una manera de recopilar las métricas, de forma automatizada. Tenemos herramientas para extraer métricas de código, pero no las discutiremos en esta publicación. Por el contrario, nos centraremos en las siguientes 2 métricas de nivel superior, que son más difíciles de recopilar:

  • El porcentaje de puntos finales REST todavía utilizados en escenarios de uso
  • El porcentaje de código cubierto al pasar por escenarios

Para recopilar los datos, diseñamos un sistema para registrar escenarios de uso. El sistema genera archivos de registro que vinculan cada escenario a una lista de puntos finales y a una lista de métodos ejecutados. Por ejemplo, podemos usar el sistema para registrar el escenario "Crear factura". El sistema genera metadatos que vinculan el escenario con 3 puntos finales REST y 12 métodos.

Para construir el sistema, integramos 3 componentes principales:

  • Utilizamos una extensión de código abierto de Chrome diseñada para facilitar las pruebas exploratorias. Proporciona una manera fácil de registrar el "inicio" y el "final" de un escenario. El propietario del producto puede hacer un recorrido completo de la aplicación y señalar el inicio y el final de cada escenario individual. Puede darles nombres descriptivos. Al final de la sesión de grabación, la extensión de Chrome nos da un primer registro de eventos, con la demarcación temporal entre escenarios.
  • Utilizamos una puerta de enlace API simple frente al monolito para capturar las solicitudes HTTP enviadas a la API REST. Este proxy nos proporciona un segundo registro de eventos, donde tenemos una marca de tiempo para cada invocación de punto final.
  • Finalmente, OpenClover se usa para instrumentar el código de la aplicación y generar métricas de cobertura de código. Esto produce una tercera salida de metadatos con marca de tiempo.

Aquí hay una vista simplificada de los tres archivos de registro generados (OpenClover almacena datos en una base de datos y el proceso para grabar sesiones es un poco más complicado):

* Registro de eventos 1 (extensión de Chrome)
12:02:00 inicio - factura al cliente
12:03:12 final - factura al cliente
12:04:10 inicio - imprimir informe mensual
12:05:00 fin - imprimir informe mensual
* Registro de eventos 2 (puerta de enlace API)
12:02:04 POST / api / auth
12:02:30 GET / api / clients / 93
12:02:49 POST / api / tareas / sendInvoice
* Registro de eventos 3 (OpenClover)
12:02:04 método com.acme.controllers.AuthController.login
12:02:04 método com.acme.services.AuthService.authenticate
...

Entonces es bastante fácil procesar estos archivos. El primero se utiliza para identificar límites temporales entre escenarios. Los otros se utilizan para extraer los puntos finales y las invocaciones de métodos que se producen dentro de estos límites. Una forma de hacer que estos datos sean fáciles de usar es generar un archivo CSV y luego usar una herramienta de visualización de datos como Tableau.

Conclusión

Por supuesto, configurar un sistema como ese y registrar cada escenario de uso (crear el inventario de funciones) lleva bastante tiempo.

Pero cuando se hace esto, el equipo tiene algo tangible y cuantificable para trabajar. El equipo tiene una forma concreta y cuantitativa de seguir el progreso de la migración. Además, los desarrolladores rápidamente tienen una idea de la cantidad de puntos finales obsoletos y código muerto.

Como dijimos anteriormente, esto es a menudo lo que los equipos necesitan para comenzar a trabajar en una tarea desafiante y abrumadora. Ya sea que se trate de una refactorización compleja, una iniciativa para pagar la deuda técnica o una campaña para introducir prácticas de prueba automatizadas, se puede aplicar el mismo enfoque de alto nivel.