Tuesday, October 08, 2024

Lean Software Development: Entregar lo más rápido posible

Uno de los principios fundamentales de Lean Software Development es entregar valor lo más rápido posible. Sin embargo, esto no significa simplemente aumentar el ritmo de trabajo, sino optimizar el flujo de valor, entregando con frecuencia, obteniendo feedback y usándolo para adaptarnos constantemente, mejorando así la eficiencia en la entrega de valor.

Vamos a explorar cómo lograrlo ajustando los procesos para movernos de manera eficiente y con confianza.

Velocidad y Dirección: No es sólo desarrollar más rápido

Ser más eficiente no se trata de construir más (ver The Building Fallacy), ni de estar siempre ocupados (eficiencia de recursos, ver The resource utilization trap). La velocidad, en términos físicos, tiene una dirección, y moverse rápido en la dirección equivocada es el mayor desperdicio que podemos tener (ver Eliminar desperdicios en el desarrollo). Para ir en la dirección correcta, necesitamos:

  • Entrega continua (Continous Deployment): para obtener feedback frecuente.
  • Humildad: para aceptar que no siempre sabemos lo que el cliente necesita ni cuál es la mejor solución técnica.
  • Adaptación: ajustando constantemente nuestra dirección técnica y de producto en función del feedback que recibimos.

Desarrollar rápido sin actuar ni decidir en base al feedback es correr en círculos. Nos agotamos mucho, pero no avanzamos nada. De hecho, es aún peor, porque acumulamos Coste Basal sin ningún beneficio (ver El coste basal del software).

¿Qué tan rápido es rápido?

Ir rápido, desde un punto de vista práctico, significa:

  •  Dar pasos muy pequeños en el desarrollo y la entrega (ver Desarrollando software posponiendo decisiones).
  • Asegurarse de que estos pasos duren entre unas pocas horas (segmentación técnica) y un día o día y medio (incrementos de producto).

Esto nos permite desplegar varias veces al día. Desplegar con esta frecuencia implica hacerlo bajo demanda, de forma segura y rápida. En un mundo ideal, estas entregas serían directamente al cliente final. Sin embargo, dependiendo del contexto, puede que haya entornos donde el despliegue a los clientes finales no sea posible, o solo se pueda hacer a ciertos usuarios (por ejemplo, en el desarrollo de sistemas embebidos o componentes de sistemas operativos). Estos entornos deberían ser la excepción (ver el modelo de Jocelyn Goldfin).

Hace unos 15 años, en Alea Soluciones, salíamos a producción entre tres y cinco veces por semana, lo cual no estaba mal, considerando que se trataba de sistemas instalados en los centros de datos de nuestros clientes. Posteriormente, en empresas como The Motion, Nextail y ClarityAI, todos entornos cloud multi-tenant, los equipos con los que he trabajado lograban desplegar varias veces al día.

En todos estos casos, alcanzamos esa velocidad de entrega utilizando Trunk-Based Development, TDD, Integración Continua y pairing/mob programming, es decir, aplicando prácticas de eXtreme Programming (XP).

Control y Velocidad: El Equilibrio Correcto

Tal como dijo el piloto canadiense de Fórmula 1, Gilles Villeneuve: "Si todo parece bajo control, es que no vas lo suficientemente rápido" ;)

Tener todo bajo control puede parecer ideal, pero en realidad, es una señal de que podrías ir más rápido. ¿Cuánto más rápido? Hasta que comiences a notar algunos fallos. Esta idea es clave cuando hablamos de desarrollar software a alta velocidad.

En mi experiencia, al trabajar con TBD, TDD y entrega continua, la confianza en el proceso crece y empiezas a acelerar, entregando cada vez más rápido. Inevitablemente, cometerás un error. Sin embargo, al trabajar en pequeños incrementos, el riesgo es bajo y corregir el fallo suele ser rápido. Tras corregir el error, es común que el equipo reduzca ligeramente la velocidad. 

Este ciclo de acelerar, aprender y ajustar es normal y, en mi opinión, es un signo de un equipo saludable que busca mejorar continuamente mientras mantiene un alto ritmo de entrega.

Necesidad de solidez en los pipelines de despliegue

En Lean, el flujo de valor comienza con una idea o experimento que queremos implementar o validar, y finaliza cuando hemos entregado el incremento, obtenido feedback y estamos listos para evaluarlo y decidir los próximos pasos.  

Aunque en la manufactura Lean se promueve la reducción de la variabilidad como una forma de estandarizar y optimizar el flujo, en Lean Product Development o Lean Software Development no todas las fases deben tener baja variabilidad. Por ejemplo, al inicio del flujo de valor, se requiere experimentación y creatividad para generar ideas y enfoques variados. Lo mismo ocurre al final del ciclo, cuando analizamos el feedback para decidir qué ajustes realizar.

Por otro lado, en la fase correspondiente al proceso “sistemático” de poner en producción, debemos asegurarnos de que haya mínima variabilidad. Esta etapa debe ser rápida, sólida y confiable, algo en lo que podamos confiar sin problemas.  

El objetivo es que los despliegues sean aburridos, una tarea rutinaria que se ejecute en piloto automático.

Para ello, los pipelines de despliegue deben cumplir con las siguientes características:

  • Ser rápidos (<10 minutos) y confiables.
  • Ofrecer un alto grado de confianza en la solución mediante buenos tests automáticos.
  • No permitir la existencia de tests inconsistentes (flaky tests).

Esto solo es posible si consideramos a los pipelines de despliegue como ciudadanos de primer nivel dentro de nuestro sistema, desarrollándolos y manteniéndolos con el mismo nivel de calidad que el resto del sistema.

Entrega continua (Continuous Deployment): El método más eficiente

Idealmente, los cambios que realizamos deberían desplegarse a producción de forma individual, uno detrás de otro. Esto nos permite obtener feedback rápidamente y entregar incrementos de valor de manera continua. Como mencionamos en este artículo y en algunos anteriores, esta forma de trabajar ofrece grandes ventajas:

  • Ajustamos continuamente el rumbo basado en el feedback.
  • Si cometemos un error, el impacto es bajo, ya que se trata de un cambio pequeño y es fácil recuperarnos al revertir a la versión anterior.
  • Además, entender cualquier error es más sencillo, porque el cambio que lo pudo haber causado está más acotado.

Sin embargo, desplegar no es gratis, tiene un coste asociado. Requiere inversión en:

  • Desarrollo de pipelines de despliegue automáticos.
  • Tiempo de espera cada vez que ejecutamos el pipeline.

Este coste se denomina "coste de transacción", y cuando es alto, limita la velocidad con la que podemos entregar.

Mantener bajos los costos de transacción

Lean Software Development recomienda mantener el coste de transacción lo más bajo posible. Para conseguirlo, es fundamental:

  • Invertir en pipelines de despliegue automáticos.
  • Usar pruebas automatizadas que nos permitan ir a producción directamente si los tests pasan.

Esto nos brinda la confianza necesaria para desplegar sin intervención manual y garantiza la calidad en cada entrega a producción.

En los equipos en los que he trabajado, nuestra opción por defecto es el Deployment continuo. Esto significa que cualquier cambio que subimos a la rama principal va directamente a producción. Para lograrlo, separamos el deployment del release mediante feature toggles, lo que nos permite liberar nuevas funcionalidades de forma controlada, sin que estén visibles inmediatamente para los usuarios finales.



Además, hemos logrado mantener los tiempos de ejecución de nuestros pipelines de despliegue entre 10 y 15 minutos, asegurando que el ciclo de entrega sea ágil y no interrumpa el flujo de trabajo. Si algo falla, solemos implementar un rollback automático cuando los smoke tests post-despliegue detectan un problema. Esto minimiza el impacto y asegura que podemos recuperar rápidamente el control.

Como destacan Jez Humble, Joanne Molesky y Barry O'Reilly en Lean Enterprise:

"The goal of continuous delivery is to make it safe and economic to work in small batches. This in turn leads to shorter lead times, higher quality, and lower costs."

Este enfoque nos permite trabajar con confianza en cambios pequeños, acelerar los tiempos de entrega, y reducir el riesgo y coste de errores.

Conclusiones

Para entregar valor rápidamente en Lean Software Development, es crucial optimizar el flujo de trabajo sin comprometer la calidad. La velocidad no implica trabajar más rápido sin control, sino ajustar continuamente en función del feedback, permitiendo una adaptación constante. Los equipos que siguen esta filosofía logran entregas continuas y seguras, minimizando riesgos y maximizando valor.

Los principios clave son:

  • Pasos pequeños y frecuentes: Entregar en incrementos pequeños reduce el riesgo y facilita la corrección rápida de errores.
  • Costes de transacción bajos: Invertir en pipelines de despliegue rápidos y fiables permite mantener la velocidad sin afectar la calidad.
  • Feedback continuo: Entregar rápidamente permite recibir retroalimentación constante, lo que facilita mejorar y adaptar el producto.
  • Confianza en la calidad: Con pruebas automatizadas y un flujo de entrega sólido, los equipos pueden estar seguros de que cada iteración libera un producto robusto.

En resumen, este enfoque no solo optimiza la entrega de valor, sino que también permite a los equipos operar de manera sostenible, con ajustes rápidos y minimizando el impacto de los errores.


Recursos relacionados

Sunday, September 29, 2024

The Building Fallacy: A Misleading Metaphor in the Age of Continuous Evolution

The "building fallacy" in software development refers to the flawed comparison between creating software and constructing a physical building. While this analogy may seem intuitive at first glance, it breaks down when we consider the nature of software and the realities of today's technology landscape. This misconception leads to inefficient processes, increased costs, and diminished capacity for innovation.


Here's why this metaphor is misleading and why it's more critical than ever to abandon it:

Why the Building Metaphor Doesn’t Hold Up

Software is Evolutionary, Buildings are Static

Buildings are designed with a fixed endpoint in mind. Once constructed, they primarily require maintenance to address wear and tear. Software, however, exists in a state of constant change. To remain relevant, it requires ongoing adaptation in response to evolving user needs, market demands, and technological advancements. Software development is more like tending a garden than erecting a structure—it requires continuous nurturing, pruning, and adjustment to thrive.

Overemphasis on Upfront Design

The building analogy often leads teams to prioritize extensive upfront planning and design, mirroring the blueprints used in construction. However, in software development, excessive upfront design can stifle innovation and lead to the dreaded "waterfall" model, where changes become costly and time-consuming. A more iterative approach allows teams to learn and adapt based on real feedback, making decisions at the last responsible moment and incorporating the latest insights.

Ignoring the Long Tail of Costs

The building fallacy promotes the misconception that most of the effort lies in the initial "construction" phase. In reality, maintaining, evolving, and supporting software systems far outweighs initial development costs.

A crucial concept that challenges the building analogy is Basal Cost—the recurring expense of maintaining features or code regardless of usage. This includes updates, security patches, and the cognitive load of supporting the code. Ignoring these costs leads to an incomplete understanding of the true financial and resource implications of software development. See more at Basal Cost of software.

Why the Building Fallacy is More Problematic Than Ever

In today's rapidly evolving tech landscape, clinging to the building fallacy is more detrimental than ever. Here's why:

The Pace of Change is Relentless

Technology, user needs, and market dynamics are shifting at an unprecedented pace. Software must evolve quickly to stay relevant. A rigid, building-centric mindset makes it hard to adapt to new challenges or opportunities.

Increased Reliance on External Systems

Modern software heavily depends on external APIs, cloud services, and open-source libraries, all of which evolve constantly. The building analogy fails to account for the complexity of managing these dependencies and ensuring seamless integration over time.

The Rise of Continuous Delivery

Continuous delivery and DevOps practices emphasize frequent releases, rapid iteration, and tight feedback loops, clashing with the notion of a fixed endpoint or grand unveiling. These practices underscore the need for flexibility, not rigidity.

Shifting to an Evolutionary Mindset

To succeed in today's environment, teams must abandon the building fallacy and embrace software as a living, evolving system. This shift involves:

Prioritizing Learning and Adaptation

Software development should be seen as a continuous process of experimentation. This means adopting agile methodologies, prioritizing validated learning over feature delivery, and delaying decisions to incorporate the latest insights.

Designing for Change

Architectures should be modular, flexible, and loosely coupled, making it easier to adapt to new requirements, integrate evolving external systems, and experiment with new technologies.

Managing Basal Costs

Teams must actively manage Basal Costs throughout the software lifecycle by:

  • Regular Maintenance: Keeping dependencies updated, applying security patches, and fixing bugs.
  • Cognitive Load Management: Simplifying the codebase and documentation to reduce the mental effort for developers.
  • Eliminating Unused Features: Removing unnecessary features to lower maintenance costs.
  • Investing in Code Quality: Refactoring, code reviews, and automated testing to improve maintainability and manage Basal Costs over time.

Lean Software Development and Extreme Programming (XP) practices are particularly effective in keeping Basal Costs low. Lean principles focus on eliminating waste, such as unnecessary code or features that add no value, which reduces maintenance burdens. XP practices, like Test-Driven Development (TDD), continuous integration, and pair programming, promote clean, maintainable code and help prevent technical debt.

By adopting these practices and embracing a mindset of continuous evolution, teams can break free from the outdated building fallacy and create software that thrives in today's dynamic technology environment.


Related content:

Sunday, September 22, 2024

Desarrollando software: posponiendo decisiones y trabajando en pasos pequeños

En esta entrega de la serie sobre Lean Software Development, después de haber explorado prácticas para posponer decisiones en el producto, hoy hablaremos sobre cómo desarrollar software dando pasos muy pequeños, aplazando decisiones, y haciendo solo lo necesario en cada momento.

Este enfoque está alineado con los principios de Lean Software Development y eXtreme Programming (XP), siendo clave en el desarrollo ágil.

Por qué trabajar en pasos pequeños

Trabajar en pequeños pasos es esencial en entornos de incertidumbre. Tanto nosotros como el cliente no siempre sabemos con exactitud qué se necesita para lograr el impacto deseado. Al avanzar en incrementos reducidos, obtenemos feedback valioso tanto del sistema —sobre su funcionamiento y comportamiento— como del cliente. Este enfoque nos permite aprender y ajustarnos constantemente, evitando decisiones prematuras que podrían limitar nuestras opciones o ser difíciles de revertir.

Es un proceso de aprendizaje continuo donde evitamos el diseño especulativo y las funcionalidades innecesarias. Al avanzar poco a poco, aceptamos que no sabemos todo desde el principio y optamos por experimentar y validar de forma constante.

Beneficios de trabajar en pasos pequeños

Trabajar en pasos pequeños con feedback continuo ofrece numerosos beneficios. Geepaw Hill, en su artículo "MMMSS: The Intrinsic Benefit of Steps", describe de forma brillante los efectos de esta práctica en los equipos. A continuación, hago un resumen, aunque recomiendo leer el artículo completo o la serie "Many More Much Smaller Steps".

Geepaw menciona ocho beneficios de trabajar en pasos de menos de un par de horas:

Beneficios en la capacidad de respuesta:

  • Interruptibilidad: Puedes manejar una interrupción o cambiar de tema sin romper el flujo de trabajo.
  • Capacidad de maniobra (Steerability): Después de cada pequeño paso, puedes reflexionar, incorporar feedback y ajustar la dirección si es necesario.
  • Reversibilidad: Si un paso no cumple con las expectativas, revertirlo implica una pequeña pérdida de tiempo.
  • Paralelismo controlado (Target Parallelism): Al avanzar en pequeños pasos consistentes, es posible trabajar en diferentes áreas del sistema o para distintos stakeholders sin dejar tareas a medias.

Beneficios humanos:

  • Alcance: Obliga a reducir la carga cognitiva, limitando las combinaciones y casos que debemos considerar.
  • Ritmo: Establece un ritmo constante en el equipo, con ciclos de recompensas rápidas (tests exitosos, commits, despliegues, etc.).
  • Seguridad: Los cambios pequeños conllevan menos riesgo que los grandes. Con tests frecuentes y despliegues diarios, el riesgo máximo es revertir el último cambio.
  • Autonomía: Permite que el equipo tome decisiones continuas, lo que requiere un esfuerzo constante por comprender y empatizar con el usuario para abordar problemas o implementar mejoras.

Trabajando en pasos pequeños y posponiendo decisiones

Desde aproximadamente 2009-2010, he intentado aplicar la práctica de trabajar en pasos muy pequeños en todos los equipos con los que colaboro. Estos pasos suelen durar pocas horas, permitiendo lanzar a producción varias veces al día y lograr cambios visibles para el cliente en uno o dos días, como máximo. Este enfoque ágil minimiza el riesgo y maximiza la capacidad de respuesta, pero requiere disciplina y la aplicación rigurosa de las prácticas de desarrollo ágil que propone eXtreme Programming (XP).

Prácticas y tácticas para trabajar en pasos pequeños

A continuación, presento algunas prácticas y estrategias que nos permiten trabajar de esta manera. A veces es difícil separarlas, ya que están estrechamente interrelacionadas y se complementan entre sí.

Desarrollo Iterativo e Incremental

La técnica más importante que usamos es también la más sencilla y, al mismo tiempo, la menos común. En lugar de partir de una solución completa y dividirla para implementarla en pasos, hacemos crecer la solución progresivamente hasta que sea suficientemente buena y podamos pasar a invertir en otro problema. Es decir, nos centramos en, teniendo la solución y el problema en mente, poner en producción (al cliente final) incrementos que estén alineados con la idea de la solución que buscamos. Usamos el feedback para asegurarnos de que vamos en la dirección correcta. Además, no tener miedo a iterar en base a este feedback nos permite trabajar en pasos pequeños y de bajo riesgo.


Por ejemplo, en este caso, partiendo de un problema inicial con una potencial solución, vamos generando los incrementos (Inc 1, Inc 2, etc.) de menos de un día. Cada incremento se entrega al usuario para obtener feedback, lo que nos ayuda a decidir el siguiente paso y si la solución ya es suficientemente buena. Así, evitamos desperdicio (zona gris) al no hacer tareas innecesarias, lo que reduce el coste basal del sistema.



Segmentación vertical (Vertical Slicing)

La segmentación vertical consiste en dividir las funcionalidades y soluciones de manera que podamos tener una aproximación incremental al desarrollo y que cada pequeño incremento aporte valor por sí mismo. Este valor puede reflejarse en mejoras para el usuario, aprendizaje para el equipo, reducción de la incertidumbre, entre otros. En lugar de dividir las historias por capas técnicas (infraestructura, backend, frontend), las dividimos por incrementos que aportan valor y, por lo general, requieren trabajo en todas las capas.

En mis equipos, aplicamos esta segmentación de manera rigurosa, procurando que ningún incremento tome más de dos días y, preferiblemente, menos de un día. Utilizamos diversas heurísticas y procesos para realizar el vertical slicing (https://www.humanizingwork.com/the-humanizing-work-guide-to-splitting-user-stories/), como el método de la hamburguesa de Gojko Adzic, que describiré más adelante.

Aunque usemos esta segmentación vertical para dividir en incrementos lo que queremos implementar, esto no implica que siempre implementemos todos los incrementos identificados. Al contrario, el objetivo es siempre hacer crecer la solución lo mínimo posible para conseguir el impacto deseado.



Abraham Vallez describe muy bien como hacer crecer progresivamente una solución en esta serie de posts 

Segmentación técnica

Como complemento a la segmentación vertical (Vertical Slicing), en mis equipos también dividimos esos incrementos que aportan valor al usuario en tareas más pequeñas que igualmente ponemos en producción. Estas tareas tienen un enfoque más técnico y suelen durar menos de dos o tres horas.

Desplegar estos incrementos técnicos nos permite obtener feedback principalmente del sistema: ¿sigue funcionando bien nuestro pipeline de CI?, ¿genera algún problema evidente el código que hemos desplegado?, ¿afecta de alguna manera al rendimiento?

Esta práctica nos obliga a mantener un coste de despliegue bajo (en tiempo y esfuerzo) y nos permite garantizar en todo momento que el flujo de trabajo sigue funcionando correctamente. Es posible porque contamos con un sistema de pruebas automatizado sólido, pipelines de CI rápidos y trabajamos con Integración Continua/Trunk-Based Development, como explicaremos posteriormente.

Poder aplicar esta segmentación técnica también es esencial para hacer cambios en paralelo, realizar modificaciones importantes en pasos pequeños y seguros, y así reducir significativamente el riesgo.

Generación de opciones

Generar opciones es fundamental para tomar decisiones bien fundamentadas. Cada decisión debe considerar múltiples alternativas; nosotros solemos intentar tener al menos tres o cuatro. Para facilitar la generación de opciones, podemos plantearnos preguntas como:

  • ¿Qué otras opciones podrías considerar si tuvieras la mitad del tiempo?
  • ¿Qué opciones requieren nuevas dependencias?
  • ¿Qué soluciones has implementado en problemas similares en el pasado?
  • ¿Cuál es el mínimo grado de sofisticación necesario para la solución?
  • ¿Quiénes podrían beneficiarse del cambio? ¿Podríamos entregarlo a cada grupo de usuarios de forma independiente?

Estas preguntas nos ayudan a generar opciones que luego el equipo puede evaluar, intentando siempre seleccionar aquellas que aporten valor rápidamente (aprendizaje, capacidad, eliminación de incertidumbre, etc.) comprometiéndonos lo mínimo posible.

Esta forma de trabajar nos permite avanzar en pequeños pasos, teniendo siempre visibilidad sobre distintas opciones que podemos tomar para continuar con el problema o redirigirlo si los pasos dados no están logrando el impacto esperado. Como ves, todo converge en trabajar con pequeños avances, aprendiendo, tomando decisiones lo más tarde posible e intentando que las soluciones sean lo más simples posibles.

Una herramienta que usamos mucho para generar opciones y realizar la segmentación vertical (vertical slicing) es el método de la hamburguesa (https://gojko.net/2012/01/23/splitting-user-stories-the-hamburger-method/) de Gojko Adzic (https://gojko.net/).

Con este método, intentamos dividir una funcionalidad o solución en los pasos necesarios para aportar valor al usuario. Esos pasos los visualizamos como “capas” de la hamburguesa, y para cada una de ellas nos forzamos a generar al menos tres o cuatro opciones. Luego seleccionamos al menos una opción de cada capa para decidir cuál será el primer incremento a implementar. Una vez implementado y entregado ese primer incremento, y con el feedback del usuario en mano, repetimos el proceso para implementar alguna de las otras opciones.

Este proceso continuo no termina cuando implementamos todas las opciones identificadas, sino cuando la funcionalidad es lo suficientemente buena o hay otra funcionalidad o problema más prioritario en el que invertir. Es decir, invertimos en lo más importante hasta que el usuario está satisfecho o hasta que surge una nueva prioridad.


Simplicidad

La simplicidad es uno de los valores fundamentales de XP y, por lo tanto, de la agilidad bien entendida. Un mantra del desarrollo ágil es “haz la cosa más simple que pueda funcionar”. Esto significa empezar con la solución más sencilla y mínima que funcione, iterando y mejorando en base al feedback.

No siempre la solución más simple es la más fácil de implementar. A veces, evitar la complejidad innecesaria requiere un esfuerzo significativo. La verdadera simplicidad es el resultado de un diseño consciente que evoluciona gradualmente.

Desarrollo en dos pasos

Kent Beck nos aconseja hacer “la cosa más simple que pueda funcionar”, pero esto a menudo se confunde con “lo primero que se me ocurra” o “lo único que sé hacer”. Una forma efectiva de asegurarnos de que estamos eligiendo la opción más simple posible es dividir cualquier cambio o incremento en dos partes:

  1. Preparación: Ajustar la base de código actual para que la nueva funcionalidad sea fácil de introducir.
  2. Implementación: Introducir el cambio real.

https://x.com/eferro/status/1810067147726508033

Esta separación evita el diseño especulativo y garantiza que solo se realicen los cambios mínimos necesarios para integrar la nueva funcionalidad, siguiendo el principio de Kent Beck: “Haz que el cambio sea fácil, y luego haz el cambio fácil”.


Principio YAGNI (You Aren't Gonna Need It / No lo vas a necesitar)

Relacionado con el punto anterior, el principio YAGNI nos recuerda que muchas ideas que se nos ocurren probablemente no serán necesarias, y que debemos enfocarnos solo en lo que necesitamos ahora. Nos ayuda a evitar el diseño especulativo y a centrarnos en lo que es realmente relevante en el momento. Incluso si identificamos algo que podríamos necesitar en el futuro cercano, YAGNI nos insta a cuestionarnos si es realmente relevante para las necesidades actuales, recordándonos que deberíamos posponerlo. Si el sistema es sencillo y fácil de evolucionar, más adelante será fácil introducir esos cambios.

Desarrollo Dirigido por Pruebas (TDD) y Outside-In TDD

El Desarrollo Dirigido por Pruebas (TDD) es una práctica que consiste en escribir primero una prueba que defina el comportamiento deseado de una funcionalidad, antes de escribir el código que lo implemente. A partir de ahí, el desarrollador escribe el código mínimo necesario para que la prueba pase, seguido de un proceso de refactorización para mejorar el diseño del código sin cambiar su comportamiento. Este ciclo se repite continuamente, lo que garantiza que cada línea de código tiene un propósito claro y definido, evitando el código innecesario o superfluo.

Outside-In TDD es una variante de TDD que comienza desde los casos de uso de negocio más amplios y se adentra en la implementación del sistema. Al partir de las necesidades del negocio y escribir únicamente el código necesario para pasar cada prueba en cada nivel (desde el nivel funcional más alto hasta las piezas individuales de código), se asegura que solo se crea el código esencial. Este enfoque previene la creación de código innecesario o la introducción de características que no son requeridas en el momento, evitando así el diseño especulativo y asegurando que se sigue siempre el principio YAGNI (You Aren't Gonna Need It).

En nuestro equipo, usamos Outside-In TDD como la forma de trabajo predeterminada para todo el código nuevo, excepto en aquellos casos donde este flujo no resulta beneficioso (spikes, algoritmos complejos, etc.). Esto implica que aproximadamente un 5-10% del código puede ser experimental para aprender, el cual es desechado posteriormente y no suele tener pruebas. Otro 10% del código corresponde a tareas donde las pruebas se escriben después (por ejemplo, integración de librerías o algoritmos complejos). El resto, la mayoría del código, se desarrolla con TDD Outside-In.

Este enfoque minimiza el desperdicio y, por defecto, sigue el principio YAGNI, ya que no se puede crear código ni diseño que no corresponda al incremento actual. Dado que el incremento actual está definido mediante una segmentación vertical radical, trabajamos en pasos pequeños, con poco desperdicio y tomando decisiones lo más tarde posible.

Como ventaja adicional, este proceso facilita la resolución rápida de errores, tanto en el código como en el diseño, ya que se verifican constantemente los avances paso a paso. Cuando se detecta un error, lo más probable es que esté en la última prueba o en el último cambio realizado, lo que permite una recuperación rápida y sin estrés.

Integración Continua, también conocida como Trunk-Based Development

Si hay una práctica técnica que obliga y ayuda a trabajar en pasos pequeños, con feedback constante, permitiendo decidir lo más tarde posible mientras aprendemos y nos adaptamos a la máxima velocidad, esa es la Integración Continua.

Primero, es importante aclarar que la Integración Continua es una práctica de XP (eXtreme Programming) que consiste en que todos los miembros del equipo integren su código en una rama principal de forma frecuente (al menos una vez al día). En otras palabras, esta práctica es equivalente a hacer Trunk-Based Development, donde solo existe una rama principal sobre la que todos los desarrolladores realizan cambios (normalmente en pareja o en equipo).

Esta práctica no tiene nada que ver con ejecutar tests automáticos en feature branches. De hecho, diría que es directamente incompatible con trabajar en ramas separadas para cada funcionalidad.

Desafortunadamente, este enfoque no es el más común en la industria, pero puedo asegurar que, junto con el TDD, es una de las prácticas que más impacto tiene en los equipos. En todos los equipos en los que he trabajado, la introducción de Integración Continua/TBD ha provocado un cambio espectacular. Nos ha obligado a trabajar en pasos muy pequeños (pero seguros), dándonos la agilidad y adaptabilidad que buscábamos.

Evidentemente, como cualquier práctica, requiere esfuerzo y el aprendizaje de una serie de tácticas para poder desplegar a producción de forma muy frecuente sin mostrar funcionalidades incompletas al usuario. Es necesario dominar estrategias que separen el deployment (decisión técnica) de la release al usuario (decisión de negocio). Las más comunes son:

  • Feature toggles: Permiten activar o desactivar funcionalidades, realizar pruebas A/B o mostrar nuevas características solo a ciertos clientes (internos, beta testers, etc.).
  • Despliegue gradual: Métodos como Canary releases o Ring deployments, que permiten realizar un despliegue progresivo del cambio.
  • Dark launches: Lanzar una funcionalidad sin que esté visible al cliente, solo para realizar pruebas de rendimiento o compatibilidad.
  • Shadow launches: Ejecutar un nuevo algoritmo o proceso en paralelo al anterior, pero sin mostrar los resultados al cliente final.

Diseño evolutivo

Esta práctica central de eXtreme Programming (XP) nos permite desarrollar software de manera incremental, refactorizando continuamente el diseño para hacerlo evolucionar conforme a las necesidades del negocio. En la práctica, consiste en crear el diseño más simple posible que cumpla con los requisitos actuales, y luego evolucionarlo en pequeños pasos a medida que aprendemos y agregamos nuevas funcionalidades.

Dentro del diseño evolutivo se pueden aplicar tácticas como:

  • Desarrollo en dos pasos.
  • Refactorización continua en el ciclo de TDD.
  • Refactorización oportunista.
  • No crear abstracciones demasiado pronto (Ver https://www.eferro.net/2015/05/aplicacion-del-principio-dry.html).
  • Cambios paralelos para mantener los tests en verde todo el tiempo mientras efectuamos cambios en varios pasos.
  • Ramificación por abstracción (Branch by abstraction) y el patrón Expandir/Contraer para facilitar cambios en paralelo.

Es importante destacar que, más allá de las tácticas que utilices para guiar el diseño en pequeños pasos, es fundamental desarrollar una sensibilidad por el diseño en el equipo. Ninguna de estas prácticas, por sí sola, enseña diseño orientado a objetos. Por lo tanto, el equipo no solo debe aprender a realizar cambios incrementales en el diseño, sino también adquirir un conocimiento profundo de los principios del diseño orientado a objetos.

Diseño evolutivo diferenciado

En general, en mis equipos intentamos siempre trabajar en pasos pequeños, enfocándonos en lo que necesitamos en ese momento y dejando que las nuevas necesidades guíen los cambios de arquitectura y diseño. Al mismo tiempo, reconocemos que la facilidad de evolución y la fricción generada ante el cambio dependen mucho del tipo de código afectado. Sabemos que no es lo mismo modificar código que implementa reglas de negocio, un API interno entre equipos o un API para el cliente final.



Cada uno de estos casos tiene más o menos fricción al cambio (es decir, distinta facilidad de evolución). Por tanto, aplicamos un diseño evolutivo diferenciado según el tipo de código.

Para el código con mayor fricción al cambio, como un API destinado al cliente final, dedicamos más tiempo a un diseño robusto que permita evolucionar sin necesidad de cambios frecuentes. En cambio, para el código interno de lógica de negocio, que solo se usa en casos específicos, aplicamos una aproximación evolutiva más flexible, permitiendo que el diseño emerja del propio proceso de desarrollo.


Otras tácticas y prácticas

Por supuesto, estas no son las únicas tácticas y prácticas a tener en cuenta, pero sí considero que son las que más nos ayudan. Existen algunos trucos y heurísticas que, aunque no los considero prácticas en sí mismas, contribuyen a la toma de decisiones y, en general, facilitan trabajar en pequeños pasos, posponiendo decisiones lo más tarde posible:

  • Priorizar librerías antes que frameworks, para no cerrar opciones y mantener mayor flexibilidad.
  • Enfocarse en hacer el código usable (y entendible) antes que en hacerlo reusable (a menos que tu negocio sea vender librerías o componentes para otros desarrolladores).
  • Usar tecnología aburrida y sólida, que esté ampliamente aceptada por la comunidad.
  • Crear wrappers ligeros sobre componentes/librerías externas para definir claramente qué parte de ese componente usamos y facilitar el testing. Puedes consultar más sobre este enfoque en https://www.eferro.net/2023/04/thin-infrastructure-wrappers.html.
  • Separar la infraestructura del código de negocio mediante Puertos y Adaptadores u otra arquitectura que permita diferenciarlos claramente.
  • Aplicar Arquitectura evolutiva para partiendo de una arquitectura mínima ir adaptandola a las necesidades de negocio, posponiendo al máximo posible las decisiones difíciles de revertir.


Conclusiones

En el desarrollo de software, la clave está en adoptar un enfoque consciente respecto a nuestras decisiones, trabajando en pasos pequeños y de bajo riesgo, y centrándonos únicamente en lo que necesitamos ahora. La simplicidad y la claridad deben ser prioridades para maximizar la eficiencia y minimizar el desperdicio.

Las prácticas de eXtreme Programming (XP), junto con los principios de Lean Software Development, nos proporcionan una guía clara para evitar el desperdicio y la sobreingeniería. Al entender que no podemos predecir el futuro con certeza, nos enfocamos en construir sistemas que sean fáciles de entender y de evolucionar, evitando la complejidad innecesaria. Trabajar de esta manera implica huir de soluciones sobredimensionadas o altamente configurables, que a menudo se convierten en obstáculos para la evolución del sistema.

Finalmente, se trata de ser humildes: asumir que no tenemos todas las respuestas y que la única forma de encontrar la solución correcta es mediante la experimentación y el aprendizaje continuo. En resumen, la simplicidad, la agilidad y la capacidad de respuesta son fundamentales para desarrollar software de manera efectiva en un entorno siempre cambiante.

Si tuviera que elegir las técnicas y prácticas que más impacto tienen en mis equipos para trabajar en pasos pequeños, seguros y posponiendo decisiones, diría que son:

  • Segmentación vertical
  • Integración Continua / Trunk-Based Development
  • TDD

Todo ello con un foco constante en la simplicidad.

Cada una de las prácticas y tácticas mencionadas en este artículo es amplia y podría explorarse en mayor profundidad. Me encantaría saber si hay interés en conocer más detalles sobre alguna de ellas, ya que sería muy enriquecedor profundizar en aquellas que resulten de mayor utilidad o curiosidad para los lectores.


Referencias y notas


Sunday, September 15, 2024

Good talks/podcasts (Sept 2024 I)



These are the best podcasts/talks I've seen/listened to recently:
  • YOW! 2019 Evolutionary Design Animated (Part1) (James Shore) [Agile, Engineering Culture, Evolutionary Design, Software Design, XP] [Duration: 00:24] (⭐⭐⭐⭐⭐) Modern software development welcomes changing requirements, even late in the process, but how can we write our software so that those changes don’t create a mess? Evolutionary design is the key. It’s a technique that emerges from Extreme Programming, the method that brought us test-driven development, merciless refactoring, and continuous integration. James Shore first encountered Extreme Programming and evolutionary design nearly 20 years ago. Initially skeptical, he’s explored its boundaries ever since. In this session, James will share what he’s learned through in-depth animations of real software projects. You’ll see how designs evolve over time and you’ll learn how and when to use evolutionary design for your own projects.
  • Eric Ries: The Science of Lean Startups (Eric Ries) [Continuous Delivery, Inspirational, Lean Startup] [Duration: 00:58] (⭐⭐⭐⭐⭐) I believe this is the best talk I have heard from Eric about the ideas of Lean Startup. It is also very enlightening to see how one of the fundamental pieces is continuous deployment and the engineering practices he uses. Essential.
  • A Conversation with Kent Beck and Eric Ries (Eric Ries, Kent Beck) [Lean Startup, XP] [Duration: 01:04] Interesting conversation between the author of Lean Startup and the author of XP. Clearly, there are synergies between the two movements, and they complement each other perfectly. With both approaches, I believe we can fully cover "Building the right thing, and building the thing right."
  • The future of software engineering (Grady Booch) [AI, Engineering Culture, Inspirational] [Duration: 01:09] (⭐⭐⭐⭐⭐) Interesting journey through the history of our profession and some predictions about the future with one of the main protagonists (Grady Booch). Very interesting, both the talk and the subsequent questions.
  • Overcomplicated Architecture: Scaling Bottleneck (Cassandra Shum) [Architecture, Architecture patterns, Engineering Culture, Evolutionary Architecture] [Duration: 00:49] Cassandra Shum discusses one of the bottlenecks of software development, an overcomplicated architecture, addressing how a company gets to an overcomplicated architecture, and how to get out of it.
  • T1 and T2 Signals (Adam Hawkins) [Devops, Lean, Operations] [Duration: 00:17] (⭐⭐⭐⭐⭐) Adam presents in this episode an interesting mental model that he calls T1 and T2 signals. It's worth understanding this model.
  • Lean Agile Brighton 2019 - Crossing the River by Feeling the Stones (Simon Wardley) [Inspirational, Product Strategy, Strategy, Technology Strategy] [Duration: 01:05] (⭐⭐⭐⭐⭐) Simon has given this talk several times, in which he presents Wardley maps and how to use them to help with strategy. I particularly like this version because it uses very good examples, including one about when to use Agile development or Lean ideas. Very instructive.
  • Platform Orchestrators: The Missing Middle of Internal Developer Platforms? (Daniel Bryant) [Platform, Platform engineering] [Duration: 00:14] Good introduction to the different approaches to platform creation for development, with an emphasis on Platform Orchestrators, which are not as well-known but can play an important role in the success of these platforms.
Reminder: All of these talks are interesting, even just listening to them.

Related:

Sunday, August 18, 2024

Good talks/podcasts (Aug 2024 I)

These are the best podcasts/talks I've seen/listened to recently:
  • YOW! 2019 Evolutionary Design Animated (Part1) (James Shore) [Agile, Engineering Culture, Evolutionary Design, Software Design, XP] [Duration: 00:24] (⭐⭐⭐⭐⭐) Modern software development welcomes changing requirements, even late in the process, but how can we write our software so that those changes don’t create a mess? Evolutionary design is the key. It’s a technique that emerges from Extreme Programming, the method that brought us test-driven development, merciless refactoring, and continuous integration. James Shore first encountered Extreme Programming and evolutionary design nearly 20 years ago. Initially skeptical, he’s explored its boundaries ever since. In this session, James will share what he’s learned through in-depth animations of real software projects. You’ll see how designs evolve over time and you’ll learn how and when to use evolutionary design for your own projects.
  • Be Quick But Don't Hurry (Joshua Kerievsky) [Agile, Culture, Inspirational] [Duration: 00:02] (⭐⭐⭐⭐⭐) Inspirational. These two minutes summarize very well why it is important to be fast but maintain control in order to be truly agile.
  • YOW! 2019 Evolutionary Design Animated (Part2) (James Shore) [Agile, Engineering Culture, Evolutionary Design, Software Design, XP] [Duration: 00:24] (⭐⭐⭐⭐⭐) The second part of this great talk. Part1 https://www.youtube.com/watch?v=LtBRvsez8DI
  • The best programmer I know (Dan North) [Agile, Engineering Career, Engineering Culture, Inspirational, Nature of Software Development] [Duration: 00:56] (⭐⭐⭐⭐⭐) Very good talk about the nature of software development and the approach to the profession. Dan talks about software as a medium, continuous learning, teamwork, etc. Highly recommended.
  • The Joy of Building Large Scale Systems (Suhail Patel) [Architecture, Engineering Culture, Microservices, Scalability] [Duration: 00:53] Suhail Patel discusses the art and practice of building systems from core principles with a focus on how this can be done in practice within teams and organisations. Very interesting talk with many details about system implementation, taking into account the changes that have occurred in the hardware.
  • Alan Kay at OOPSLA 1997 - The computer revolution hasnt happened yet (Alan Kay) [Engineering Culture, Evolutionary Design, Inspirational, Nature of Software Development, OOP, Software Design] [Duration: 01:04] (⭐⭐⭐⭐⭐) Classic presentation by Alan Kay talks about the nature of software, the design that systems should have, scalability, and how, to some extent, we could compare it to how biological systems work. Many of the ideas behind Smalltalk can be identified in the talk.. Inspirational.
  • Concurrency Oriented Programming in a Modern World (Francesco Cesarini, Robert Virding) [Architecture patterns, Microservices, Scalability, erlang] [Duration: 00:52] This talk delves into how the Erlang concurrency model, initially developed for telecom systems in the 90s, is now vital for modern cloud-based microservices, mobile apps, and machine learning. Presenters Robert and Francesco emphasize how functional languages and fault-tolerant computing principles are essential for distributed multi-core architectures across cloud, edge, and IoT networks.
  • Continuous Integration: That’s Not What They Meant (Clare Sudbery) [CI, Technical Practices, Trunk Based Development, XP] [Duration: 00:56] (⭐⭐⭐⭐⭐) Very good talk about the benefits of using Trunk Based Development (or in other words, the practice of CI as it was originally created).
  • Generative AI in a Nutshell - how to survive and thrive in the age of AI (Henrik Kniberg) [AI, Generative AI] [Duration: 00:17] Simple explanation of what AI is, Generative AI, and how it works at a high level. It's not a detailed explanation, but it's very useful if you want to explain it to someone without a technological background.
Reminder: All of these talks are interesting, even just listening to them.

Related:

Tuesday, July 16, 2024

El coste basal del software

Traduccion del articulo original Basal Cost of Software
Traduccion realizada por https://x.com/simonvlc y publicada originalmente en su genial newsletter Estrategia de Producto: El Coste Basal del Software
---

En mi opinión, el coste de desarrollo de cada funcionalidad en un producto puede dividirse de la siguiente manera:

  1. El coste directo de desarrollo o coste inicial.
  2. Un coste semanal o mensual únicamente asociado con su existencia en el sistema. Haciendo una comparación con la Tasa Metabólica Basal del cuerpo humano, podríamos llamar a este segundo coste la Tasa Metabólica Basal o Coste Basal.

El Coste Basal se compone de dos partes diferenciadas:

  • El impacto directo en la capacidad del equipo debido a la complejidad añadida (nuevas dependencias, más código para entender, más posibilidades de bugs ocultos, etc.).
  • El impacto en el coste de desarrollo o evolución de otras funcionalidades debido a posibles incompatibilidades, acoplamiento, etc.

El coste inicial

Es el coste incurrido por el equipo durante el desarrollo inicial de la funcionalidad. Incluye el coste desde que el equipo comienza a trabajar en la funcionalidad hasta que el cliente la tiene disponible y comienza a usarla. Por supuesto, este proceso debería consistir en múltiples despliegues y lanzamientos parciales para obtener feedback y hacer los ajustes necesarios...

El Coste Basal

Tras el coste inicial de desarrollo, afrontamos también un coste continuo que reduce la capacidad del equipo para el desarrollo de nuevas funcionalidades (innovación).

Este coste, que continúa con el tiempo y solo termina con la eliminación de la funcionalidad o el fin de vida del producto, es el coste que paga el equipo por la existencia de ese código en el producto.

Es importante tener en cuenta que este no se refiere al coste de hacer modificaciones o corregir errores; se refiere al coste de simplemente tener el código allí...

¿Por qué se da este coste? ¿Por qué una funcionalidad que no evoluciona supone un coste? 

  • El equipo tiene que conocer ese código (dónde está, qué dependencias tiene, qué interactúa con él, cómo está diseñado...).
  • El conocimiento y las técnicas del equipo están en constante evolución. Cuando mejora en cualquiera de estos aspectos, deberá actualizar el código.
  • Cuando el equipo diseña una nueva funcionalidad, el código debe ser diseñado de manera que sea compatible con todas las anteriores y no genere problemas o regresiones. Y, por supuesto, este coste es proporcional a todas las que ya teníamos en el sistema.
  • Algo similar ocurre cuando un nuevo miembro se une al equipo. Este nuevo integrante debe aprender sobre todas las funcionalidades, por lo que el coste es proporcional a su número..

Y lo peor de todo, este sobrecoste es continuo hasta la "muerte" de la funcionalidad. Por ejemplo, hasta el fin de la vida del producto, hasta que ningún usuario la use, o hasta el fin del mundo (lo que ocurra primero).

Evolución del coste de una funcionalidad

Como hemos visto, el Coste Basal de una funcionalidad es más o menos constante durante su vida. Pero cada lenguaje, dependencia o tecnología que hayamos usado en su desarrollo, puede alcanzar un punto en el que ya no es utilizable por cualquier razón (dependencias obsoletas, problemas de seguridad, evolución normal que deprecia la versión del lenguaje que usamos, etc.). Desde este momento, el coste puede dispararse porque estamos obligados a actualizarlo, incluso aunque no queramos evolucionar la funcionalidad.

En resumen, el coste real puede ser representado por el siguiente gráfico:

El problema

Un error común es negar el Coste Basal y considerar que si no se hacen cambios, el coste de mantenimiento de una funcionalidad es cero. Supongo que esto proviene de utilizar metáforas de otras profesiones como la construcción, que no son aplicables al desarrollo de software.


La capacidad no es infinita

A pesar de que la capacidad de un equipo cambia (positiva o negativamente) con el paso del tiempo debido a distintos factores (conocimiento del negocio, técnicas, estrés…), esta es finita.

El Coste Basal acumulado de todas las funcionalidades de las que el equipo es responsable reduce la capacidad disponible para desarrollar nuevas funcionalidades (innovar).

Con el tiempo, podemos observar como la capacidad del equipo para innovar decae rápidamente:

Hasta el punto de que cuando la capacidad se agota, el equipo se encuentra en una situación en la que le es imposible innovar porque pasa todo su tiempo “manteniendo” las funcionalidades de las que es responsable.


O bien reduce su velocidad al desarrollar nuevas funcionalidades, acumulando coste basal, pero a menor ritmo.

Conclusiones

Las secciones anteriores destacan varios principios que debemos considerar para mejorar la eficiencia de nuestros equipos de producto.

  • Debemos minimizar el Coste Basal tanto como sea posible, logrando el impacto deseado con la menor cantidad de código posible.
    • Cuando sea posible, incluso sin añadirlo.
    • Iterando la funcionalidad, partiendo de una solución mínima, para adaptarla lo más posible a las necesidades del usuario.
    • Haciendo el software/diseño lo más simple posible. Debe ser fácil de evolucionar sin sobreingeniería (YAGNI).
  • Debemos eliminar cualquier funcionalidad del producto que no tenga el impacto deseado, eliminando así su Coste Basal.
  • Debemos monitorizar continuamente el código del equipo para detectar la obsolescencia de dependencias, lenguajes y tecnologías para evitar que el Coste Basal se dispare.

Veamos un ejemplo teórico sencillo para ayudar a transmitir el mensaje. Supongamos que un equipo acumula un 1% de Coste Basal por semana, lo que significa que, por cada 100 unidades de producto (código, características, mejoras), añade 1 unidad de Coste Basal. A medida que este se acumula, en 52 semanas (aproximadamente 1 año), el equipo solo podrá dedicar el 60% de su capacidad a nuevas funcionalidades/mejoras (innovación). En dos años, será apenas el 35%. Por supuesto, este ejemplo es una simplificación, pero destaca que negar este coste no es una opción viable.


Recordad: La Simplicidad — el arte de maximizar el trabajo no hecho — es esencial.

Relacionado y referencias

Otros artículos interesantes