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 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 incluso el redondeo

Imagina que heredamos esta regla en Java 17 o posterior. El ejemplo trabaja con importes no negativos expresados en céntimos y devuelve el total después del descuento; dejamos fuera impuestos y otras promociones:

final class LegacyPriceCalculator {
    static long calculateTotal(long amountInCents) {
        return amountInCents - amountInCents / 10;
    }
}

Para importes positivos, la división entera trunca el descuento. Con 105 céntimos descuenta 10 y devuelve 95. Esta regla histórica difiere del redondeo HALF_UP utilizado en la segunda entrega: allí, el descuento de 10,5 céntimos se redondea a 11 y el total es 94. Necesitamos entender esa diferencia antes de tocarla.

Podemos dar nombre al cálculo sin cambiar su comportamiento:

final class RefactoredPriceCalculator {
    private static final int LEGACY_DISCOUNT_DIVISOR = 10;

    static long calculateTotal(long amountInCents) {
        return amountInCents - calculateLegacyDiscount(amountInCents);
    }

    private static long calculateLegacyDiscount(long amountInCents) {
        return amountInCents / LEGACY_DISCOUNT_DIVISOR;
    }
}

Ambas versiones siguen devolviendo 95 para ese caso. Cambiar después el redondeo exigiría acordar la nueva regla y actualizar sus expectativas. La extracción hace visible una decisión existente y permite separar las dos tareas.

Antes de extraer el método podemos escribir pruebas de caracterización: comprobaciones que dejan registrado lo que hace el código actual. Para esta regla, los importes de 0, 9, 10 y 105 céntimos producen totales de 0, 9, 9 y 95. Los casos cercanos a 10 ayudan a detectar un cambio en el punto donde empieza a descontarse un céntimo.

Esas pruebas aportan una referencia para comparar las versiones. No demuestran que la regla histórica sea la que desea el negocio ni cubren por sí solas todo el comportamiento. Si descubrimos un error, podemos documentarlo y corregirlo en un cambio separado, con una expectativa nueva. Así resulta más fácil explicar por qué una comprobación deja de esperar 95 y pasa a esperar 94.

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. 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 el truncamiento del descuento en céntimos.
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 una regla de cálculo como la del ejemplo histórico. 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.