La primera promoción se implementó en una tarde. La segunda necesitó un par de excepciones. La quinta exige consultar a quien escribió la primera, que ahora trabaja en otro proyecto. Cada cambio parece costar un poco más.

Esa evolución invita a revisar qué dificultad viene del negocio y cuál hemos ido introduciendo en la solución.

La complejidad que encontramos y la que añadimos

La tienda tiene impuestos, promociones y devoluciones. Algunas reglas serán complicadas aunque el código esté bien organizado. Esa dificultad pertenece al problema y suele llamarse complejidad esencial.

Después está la complejidad accidental: reglas dispersas, nombres confusos, configuraciones innecesarias o recorridos difíciles de seguir. Es la dificultad que introduce nuestra forma de resolverlo.

Si una promoción depende del país y del tipo de cliente, tendremos que representar esas condiciones. Si entenderla exige además seguir cuatro clases que solo se reenvían parámetros, conviene revisar qué aporta ese recorrido.

La distinción permite orientar el trabajo. Para simplificar el problema quizá necesitemos hablar con quien define las promociones. Para simplificar su implementación podemos revisar la organización del código. Una regla puede cambiar si el negocio acepta otras condiciones; mientras esas condiciones sigan vigentes, tenemos que representarlas correctamente.

Cuánto necesitamos mantener en la cabeza

La carga cognitiva es el esfuerzo mental necesario para trabajar con una parte del sistema.

Una función corta puede exigir recordar quién la llamó, qué variable global cambió antes y qué orden de ejecución evita un error. La longitud del método cuenta poco sobre todas esas obligaciones.

Un diseño comprensible permite concentrarse en una zona sin reconstruir toda la aplicación. Los nombres claros, los contratos explícitos y la localización de las reglas ayudan a reducir ese esfuerzo.

Una comprobación sencilla consiste en explicar un cambio a alguien del equipo. Si antes de llegar a la promoción necesitas contarle toda la historia del proyecto, has encontrado una pista bastante útil.

Refactorizar conservando el comportamiento

La refactorización reorganiza la estructura interna conservando el comportamiento observable. Esa conservación es central en la definición de Martin Fowler.

Extraer el cálculo compartido del descuento o separar su presentación pueden ser refactorizaciones. Cambiar qué clientes tienen derecho a la promoción modifica también el comportamiento.

Conviene hacer transformaciones pequeñas y comprobar que las reglas y contratos relevantes se mantienen. Las pruebas de comportamiento ayudan cuando protegen esas expectativas. Una prueba que exige una distribución concreta de métodos privados puede dificultar precisamente la reorganización que necesitamos.

Podemos refactorizar la zona en la que vamos a trabajar para facilitar el cambio. La mejora tiene un motivo concreto y un alcance que podemos revisar.

Una extracción que conserva el límite

Un taller acepta reservas mientras queden plazas. El código heredado recibe las plazas ocupadas y la capacidad; para este ejemplo, ambas cantidades son no negativas y la capacidad es mayor que cero:

final class LegacyWorkshop {
    String reservationMessage(int occupied, int capacity) {
        if (occupied >= capacity) {
            return "Workshop full";
        }
        return "Places available";
    }
}

El mensaje forma parte del comportamiento observable. Con 9 plazas ocupadas de 10, devuelve "Places available"; con 10, devuelve "Workshop full". Si llegan 11 ocupadas, también devuelve lleno. Aunque ese último dato merezca investigarse, la reorganización no debería abrir reservas por accidente.

Podemos dar un nombre a la condición y separarla de la elección del mensaje:

final class RefactoredWorkshop {
    String reservationMessage(int occupied, int capacity) {
        if (isFull(occupied, capacity)) {
            return "Workshop full";
        }
        return "Places available";
    }

    private boolean isFull(int occupied, int capacity) {
        return occupied >= capacity;
    }
}

reservationMessage() sigue tomando la misma decisión. isFull() expresa qué significa la comparación; mantiene el >=, incluidos los casos en el límite y por encima. La extracción resulta útil si nos permite leer mejor la intención o preparar una reorganización mayor. Dividir métodos por sistema también puede añadir saltos innecesarios.

Antes del cambio podemos escribir pruebas de caracterización: comprobaciones que dejan registrado lo que hace el código actual. Estos casos sirven para comparar ambas versiones:

Ocupadas Capacidad Mensaje antes y después
0 10 Places available
9 10 Places available
10 10 Workshop full
11 10 Workshop full

Cambiar >= por > dejaría el taller de 10 plazas como disponible cuando ya tiene 10 reservas. La comprobación del límite detecta ese cambio que un caso de 9 plazas no ve.

Las pruebas de caracterización registran el comportamiento existente; por sí solas no demuestran que sea el deseado. Si ahora el negocio pide permitir una lista de espera, acordamos esa nueva regla y sus expectativas en otro cambio. Separar ambas tareas facilita saber si una diferencia viene de la refactorización o del requisito nuevo.

Deuda técnica: poner nombre a un coste futuro

La deuda técnica es una metáfora para hablar de decisiones o carencias internas que encarecen la evolución del software. Una solución provisional puede acelerar una entrega y dificultar las modificaciones posteriores. También podemos descubrir ese coste al aprender algo que desconocíamos cuando construimos el sistema.

En nuestra tienda, duplicar una política para llegar a una fecha puede permitir entregar antes. Después tendremos que mantener las dos versiones coherentes cada vez que cambie.

Para decidir qué hacer conviene describir la consecuencia: «la promoción está duplicada en tres recorridos y cada ajuste requiere corregirlos por separado». Esa información permite comparar el coste de extraerla con el de seguir manteniendo las copias.

En la explicación de Martin Fowler sobre deuda técnica, el esfuerzo adicional que se repite funciona como los intereses de una deuda. En nuestra tienda sería el trabajo de localizar las copias, modificarlas y comprobar que siguen coincidiendo. El coste de reunirlas en una política común sería parte del esfuerzo para saldarla.

La frecuencia de cambio importa. Una dificultad en una zona que apenas se toca puede tener menos urgencia que un pequeño obstáculo que se repite todas las semanas. Podemos registrar dónde aparece, qué cambios dificulta y qué mejora proponemos, sin inventar una cifra exacta de horas ahorradas. «Es código viejo» explica bastante menos que «cada promoción obliga a revisar tres implementaciones».

Antes, carrito, factura y resumen contienen copias de una regla; después, los tres utilizan una política compartida.

El esquema se lee de arriba abajo: primero aparecen las tres copias y después sus consumidores conectados a una política común. Las flechas representan el uso de esa regla compartida, sin indicar un orden de ejecución. Al extraerla, sus cambios se localizan en una pieza. Es la cautela de DRY: compartir conocimiento que deba evolucionar junto. Para justificar esa unión comprobamos que las copias representaban la misma regla; si tenían condiciones independientes, agruparlas podría aumentar el coste de evolucionarlas.

Elegir decisiones que podamos revisar

La reversibilidad indica cuánto esfuerzo necesitaríamos para cambiar de rumbo. Antes de elegir podemos preguntar qué código habría que modificar, qué datos tendríamos que migrar y qué consumidores resultarían afectados.

Si una decisión incierta tiene mucho impacto, una prueba pequeña puede aportar información antes de comprometer toda la solución. También conviene conservar sus razones.

Un ADR, o Architecture Decision Record, es un registro breve de una decisión arquitectónica. Puede recoger el contexto, la elección, las alternativas relevantes y sus consecuencias. La propuesta de Michael Nygard ayuda a conservar las razones de las decisiones que afectan a la estructura y las dependencias del sistema. No hace falta abrir uno por cada método que renombramos.

Por ejemplo: elegimos una solución concreta para las promociones porque cubre los casos conocidos y el equipo puede mantenerla. Dejamos anotado qué nueva necesidad justificaría revisarla. Dentro de seis meses, esa explicación puede ahorrar una buena sesión de arqueología.

Una nota breve puede bastar:

Decisión: centralizar la política de bienvenida.
Estado: aceptada.
Contexto: carrito, factura y resumen aplican la misma promoción.
Elección: utilizar un cálculo común que conserve las condiciones actuales de la promoción.
Alternativa: mantener tres implementaciones coordinadas manualmente.
Consecuencia: un cambio de la regla se aplica en un único lugar.
Coste: los tres consumidores quedan ligados a la misma política y deben comprobarse al modificarla.
Revisar si: alguno necesita aplicar una política de negocio distinta.

La nota plantea la centralización de la política de bienvenida y conserva sus condiciones actuales. La extracción del taller muestra la misma precaución en un caso más pequeño: reorganizar primero sin alterar la regla. Su alcance queda limitado a esa promoción: no obliga a que todas tengan el mismo tratamiento. Si cambiamos de decisión, podemos marcar el ADR como sustituido y enlazar el nuevo; así conservamos las razones que explican cada etapa.

Mejorar con una dificultad delante

Podemos empezar por la promoción que necesitamos cambiar hoy. Localizamos los obstáculos, elegimos una mejora acotada y comprobamos que conserva las reglas actuales. Después incorporamos el nuevo comportamiento.

El resultado se puede valorar con preguntas cercanas: ¿se entiende mejor?, ¿hemos reducido el trabajo repetido?, ¿el siguiente cambio parecido será más fácil?

La arquitectura también evoluciona a través de esas decisiones pequeñas. Merece la pena dejar el código un poco más preparado para las necesidades que ya conocemos y las razones suficientemente claras para quien venga después.