Módulo 4: Branch by abstraction

Presentación del módulo: branch by abstraction

Por qué este módulo existe aquí

En el módulo 3 aprendiste a estrangular un sistema legacy desde afuera. Pusiste un facade delante del GET /products, construiste un servicio nuevo al lado, y desviaste el tráfico HTTP del viejo al nuevo por porcentaje, con la vieja ruta como fallback, hasta apagarla. Es la técnica más visible de la modernización incremental, y funciona de maravilla —cuando se cumple una condición—: que exista una frontera externa donde interponer el proxy. Un endpoint, un gateway, un balanceador: un punto de red por el que pasa el tráfico y donde puedes decidir "¿viejo o nuevo?".

Pero la mayor parte del código de un monolito no tiene esa frontera. El cálculo de envío de Mercado no es un endpoint: es una función llamada desde adentro del checkout, del carrito, del panel de admin, de los emails de confirmación. No pasa por la red; se invoca directo, en el mismo proceso. No hay dónde poner un proxy HTTP. Si intentaras estrangularla con un facade externo, te enredarías buscando una frontera que no existe. Para ese caso —que es la regla, no la excepción, dentro de un monolito— existe el patrón de este módulo: branch by abstraction.

Aquí está la idea completa, en una frase: en vez de un proxy externo que decide por tráfico, insertas una capa de abstracción —una interfaz, un Protocoldentro del código, entre los llamadores y la implementación; construyes la implementación nueva detrás de esa misma abstracción, al lado de la vieja, con las dos coexistiendo; un feature flag decide en cada llamada cuál corre; validas que la nueva da el mismo resultado que la vieja en los casos conocidos; conmutas el flag para darle tráfico a la nueva; y cuando confías, borras la vieja. Y todo esto ocurre en main, en pasos chicos, cada uno siempre integrable —sin una rama de git de larga vida que diverja durante meses y explote al mergear—.

Este módulo enseña esa mecánica a fondo, aplicada al cálculo de envío de Mercado. Le insertamos una abstracción ShippingCalculator, envolvemos la lógica vieja en un LegacyShipping, construimos un ModernShipping detrás de la misma abstracción, lo validamos con un parallel-run, conmutamos el flag, y borramos la vieja implementación —todo medido y ejecutado en Python—.

Conexión con el módulo. Esta es la lección-mapa. No entramos todavía al detalle de cada paso; instalamos la metáfora (el motor del avión que se cambia en vuelo con un adaptador, la tubería que se reemplaza detrás de una válvula de paso), el ciclo completo (insertar abstracción → construir la nueva detrás → flag → validar → borrar la vieja) y el vocabulario (ShippingCalculator, LegacyShipping, ModernShipping, abstraction, feature_flag, parallel-run, branch_by_abstraction). Y corremos un ejemplo-mapa: una abstracción ShippingCalculator mínima con dos implementaciones y un flag que conmuta, para ver que los llamadores no saben cuál corre. La lección 2 inserta la abstracción; la 3 construye la nueva detrás; la 4 conmuta con el flag; la 5 valida con parallel-run; la 6 borra la vieja; y la 7 explica por qué todo esto en main evita el merge hell. La 8 lo integra en el cálculo de envío de Mercado. Fíjate en la frontera: aquí el punto de conmutación es interno y por código (una abstracción dentro del proceso). El strangler del módulo 3 es externo y por tráfico (un proxy en la frontera de red); extraer el envío a su propio servicio con un anti-corruption layer es el módulo 5.

Y la promesa de siempre: nada se afirma "de memoria", todo se ejecuta. Cada simulación corre con Python 3.14 y solo la biblioteca estándar, con datos fijos, así que la salida que ves en cada bloque "Qué esperar" es la salida literal de correr el código. Puedes copiarlo y reproducirlo idéntico.

Una analogía: cambiar el motor de un avión en pleno vuelo

Imagina que tienes que reemplazar el motor de un avión —uno viejo, que consume mucho y falla— por uno nuevo. Y la restricción es brutal: el avión no puede aterrizar. Está lleno de pasajeros y tiene que seguir volando durante toda la operación. No puedes apagarlo, desmontarlo en un hangar y montar el nuevo con calma. Ese es, exactamente, el problema de modernizar un sistema en producción: el negocio no se detiene.

La forma ingenua —y suicida— sería arrancar el motor viejo de un tirón y atornillar el nuevo lo más rápido posible, rezando para que encaje. Durante esos segundos sin motor, el avión cae. Es el equivalente del big-bang: quitas lo viejo y pones lo nuevo de golpe, y si algo no encaja, todo el sistema está caído mientras lo resuelves.

La forma branch by abstraction es distinta y empieza por un paso que no cambia nada: instalas un adaptador estándar donde va atornillado el motor. Un montaje intermedio, una brida universal, entre el fuselaje y el motor. Al principio el motor viejo sigue ahí, atornillado al adaptador, funcionando igual que siempre —el avión no notó nada, sigue volando con el mismo motor de siempre—. Pero ahora existe algo que antes no existía: un punto de acople neutro que no sabe ni le importa qué motor tiene del otro lado. Después traes el motor nuevo y lo montas en un segundo acople, al lado, sin quitar el viejo. Los dos están conectados al adaptador. Entonces —y solo entonces— giras una válvula que dirige la potencia: primero un poco por el motor nuevo mientras el viejo sostiene el vuelo, observas que empuja parejo, y vas pasando más potencia al nuevo hasta que el viejo ya no hace nada. En ese momento lo desmontas. El avión nunca dejó de volar.

Aquí están las piezas del patrón, ya visibles en el avión:

  • El adaptador —la brida universal donde se atornilla cualquier motor— es la capa de abstracción (ShippingCalculator): el punto neutro entre los llamadores y la implementación.
  • El motor viejo atornillado al adaptador es LegacyShipping: la implementación de hoy, ahora detrás de la abstracción.
  • El motor nuevo montado al lado es ModernShipping: construido sin quitar el viejo, conectado al mismo adaptador.
  • La válvula que dirige la potencia es el feature flag: decide, en cada momento, cuánto va por el motor nuevo.
  • Desmontar el motor viejo al final es borrar LegacyShipping.

Y hay una segunda imagen, más doméstica, que ilumina el mismo punto: reemplazar una tubería detrás de una válvula de paso, sin cortarle el agua a toda la casa. Cierras la válvula de paso de ese tramo, cambias la tubería vieja por la nueva detrás de la válvula, y vuelves a abrir. El resto de la casa —la cocina, el otro baño— nunca se quedó sin agua, porque la válvula aisló el cambio a un solo tramo. La válvula de paso es la abstracción: el punto donde puedes cambiar lo que hay del otro lado sin que el resto del sistema lo note. El módulo entero es aprender a instalar esa válvula en el código, cambiar la tubería detrás de ella, y quitarla si ya no hace falta.

Ejemplo trabajado: una abstracción ShippingCalculator mínima con un flag que conmuta

No vamos a describir branch by abstraction: lo vamos a ejecutar, aunque sea en su versión más pequeña. La idea es tener las tres piezas mínimas —la abstracción, dos implementaciones detrás de ella, y un flag que decide cuál corre— y ver, con nuestros ojos, que un llamador cualquiera del monolito produce el mismo resultado sin importar cuál implementación esté activa. Ese es el corazón del patrón: el llamador habla con la abstracción, no con la implementación, así que cambiar la de atrás no lo toca.

La abstracción es ShippingCalculator: un Protocol con un solo método, cost(order). Un Protocol en Python es una interfaz estructural —define qué forma debe tener algo (aquí, "tener un método cost que recibe un order y devuelve un float") sin obligar a heredar de una clase base—. LegacyShipping y ModernShipping la cumplen las dos: cada una tiene su cost(order). El feature_flag —un simple booleano, por ahora— elige cuál instancia se le entrega al llamador. Y el llamador, checkout_total, recibe un ShippingCalculator y le pide .cost(), sin preguntar de qué clase es.

from typing import Protocol

# --- Los llamadores del monolito de Mercado calculan envio en muchos lugares. ---
# Todos pasan por esta abstraccion: una interfaz, no una implementacion concreta.
class ShippingCalculator(Protocol):
    def cost(self, order: dict) -> float: ...

# --- Implementacion LEGACY: la logica de envio que hoy vive en el monolito. ---
class LegacyShipping:
    def cost(self, order: dict) -> float:
        base = {"local": 5.0, "national": 10.0, "international": 25.0}[order["zone"]]
        surcharge = max(0.0, order["weight_kg"] - 1.0) * 2.0
        cost = base + surcharge
        if order["order_total"] >= 50.0 and order["zone"] != "international":
            cost = 0.0
        return round(cost, 2)

# --- Implementacion MODERN: reescrita al lado, MISMA interfaz, otra estructura. ---
class ModernShipping:
    RATES = {"local": 5.0, "national": 10.0, "international": 25.0}
    def cost(self, order: dict) -> float:
        base = self.RATES[order["zone"]]
        over = max(0.0, order["weight_kg"] - 1.0)
        free = order["order_total"] >= 50.0 and order["zone"] != "international"
        return 0.0 if free else round(base + over * 2.0, 2)

# --- El flag (en memoria) elige que implementacion corre detras de la abstraccion. ---
def get_calculator(feature_flag: bool) -> ShippingCalculator:
    return ModernShipping() if feature_flag else LegacyShipping()

# --- Un llamador cualquiera del monolito. NO sabe cual implementacion corre. ---
def checkout_total(order: dict, calc: ShippingCalculator) -> float:
    return round(order["items_subtotal"] + calc.cost(order), 2)

ORDERS = [
    {"id": 1, "zone": "local",         "weight_kg": 0.5, "order_total": 20.0, "items_subtotal": 20.0},
    {"id": 2, "zone": "national",      "weight_kg": 2.0, "order_total": 30.0, "items_subtotal": 30.0},
    {"id": 3, "zone": "international", "weight_kg": 3.0, "order_total": 80.0, "items_subtotal": 80.0},
    {"id": 4, "zone": "local",         "weight_kg": 1.0, "order_total": 60.0, "items_subtotal": 60.0},
]

print("El mismo llamador, la misma abstraccion; el flag decide quien calcula.\n")
print(f"{'order':>6}{'zone':>16}{'flag=OFF (legacy)':>20}{'flag=ON (modern)':>19}")
print("-" * 65)
for o in ORDERS:
    off = checkout_total(o, get_calculator(feature_flag=False))
    on = checkout_total(o, get_calculator(feature_flag=True))
    match = "OK" if off == on else "DIFF"
    print(f"{o['id']:>6}{o['zone']:>16}{off:>20}{on:>19}   {match}")

print("\n  checkout_total es identico en ambas columnas: recibe un ShippingCalculator")
print("  y llama .cost(). No sabe ni le importa cual implementacion corre detras.")

Qué esperar. Al correr el archivo, la salida es exactamente esta:

El mismo llamador, la misma abstraccion; el flag decide quien calcula.

 order            zone   flag=OFF (legacy)   flag=ON (modern)
-----------------------------------------------------------------
     1           local                25.0               25.0   OK
     2        national                42.0               42.0   OK
     3   international               109.0              109.0   OK
     4           local                60.0               60.0   OK

  checkout_total es identico en ambas columnas: recibe un ShippingCalculator
  y llama .cost(). No sabe ni le importa cual implementacion corre detras.

Lee las dos columnas de números: son idénticas, fila por fila, y la columna de la derecha lo confirma con OK. Esto es el patrón en su forma más pura. El mismo checkout_total —el llamador— produjo 25.0, 42.0, 109.0, 60.0 tanto con el flag en OFF (corriendo LegacyShipping) como en ON (corriendo ModernShipping). ¿Por qué? Porque checkout_total no menciona a ninguna de las dos clases: recibe un ShippingCalculator —la abstracción— y le pide .cost(). Quién esté del otro lado es asunto del get_calculator, no suyo.

Fíjate en lo que no hubo que hacer para conmutar de una implementación a la otra: no se tocó checkout_total. Ni una línea. Ese es el punto de acople neutro de la analogía del avión: el adaptador donde se atornilla cualquier motor. El llamador se atornilló al adaptador (ShippingCalculator), no al motor (LegacyShipping), y por eso puedes cambiar el motor sin destornillar al llamador.

Que las dos columnas den lo mismo también dice algo sobre la implementación nueva: ModernShipping —con una estructura interna distinta, una tabla RATES como atributo de clase en vez de un diccionario inline— produce el mismo resultado que LegacyShipping en estos cuatro casos. Eso es exactamente lo que un parallel-run verifica antes de confiar en la nueva (la lección 5). Aquí lo vemos "a mano", en cuatro filas; en la lección 5 lo mediremos sobre cientos de pedidos y atraparemos un caso donde no coinciden.

Este ejemplo tocó las cinco piezas del patrón sin desarrollarlas: la abstracción (ShippingCalculator), la implementación vieja detrás de ella (LegacyShipping), la nueva al lado (ModernShipping), el flag que conmuta (get_calculator), y la evidencia de que el llamador no se entera. El resto del módulo desarrolla cada pieza, una por lección.

El ciclo completo de branch by abstraction

Ese ejemplo mostró las piezas montadas; vale la pena ver el orden en que se instalan, porque es la columna vertebral de las siete lecciones que siguen:

Paso                          Leccion   Que haces                              Estado del legacy
────────────────────────────  ────────  ─────────────────────────────────────  ────────────────
1. Insertar la abstraccion    L2        interfaz entre llamadores y codigo      100% activo
2. Construir la nueva detras   L3        ModernShipping, mismo contrato          100% activo
3. Conmutar con el flag        L4        flag: 0 -> 100 por llamada              bajando
4. Validar con parallel-run    L5        llamar a las dos y comparar             activo (de red)
5. Borrar la vieja             L6        eliminar LegacyShipping y el flag       0%, borrado

Un matiz de orden que conviene aclarar: el paso 4 (validar con parallel-run) no ocurre después de conmutar, sino entrelazado con el paso 3. Validas primero con el parallel-run (con el flag aún en 0, sin exponer la nueva a nadie), y solo cuando la validación está limpia empiezas a subir el flag. La lección 5 lo trata a fondo; el orden de la tabla es lógico, no estrictamente secuencial.

Y así encajan los pasos en el flujo del módulo:

flowchart LR
    C["llamadores<br/>(checkout, carrito, admin)"] --> A["ShippingCalculator<br/>(la abstraccion)"]
    A -->|flag = OFF| L["LegacyShipping<br/>(el viejo)"]
    A -->|flag = ON| M["ModernShipping<br/>(el nuevo, al lado)"]
    M -.->|parallel-run: comparar| L
    L -.->|flag a 100 y validado| X["borrar<br/>LegacyShipping"]

Léelo así: los llamadores ya no llaman a la implementación concreta, llaman a la abstracción. La abstracción, según el feature_flag, delega en LegacyShipping o en ModernShipping. El parallel-run (línea punteada) corre las dos y compara para validar la nueva antes de confiar. Y cuando el flag llegó a 100 y la validación está limpia, LegacyShipping se borra —el motor viejo se desmonta—.

El mapa: dónde está este módulo en la guía

Branch by abstraction es la técnica interna de la migración incremental: la que aplica cuando no hay una frontera de red donde poner un proxy. Así se conecta con el resto:

flowchart TD
    M1["M1 · Por que no reescribir<br/>(la conviccion)"]
    M2["M2 · Caracterizar el legacy<br/>(la red de seguridad)"]
    M3["M3 · Strangler fig<br/>(migrar por fuera, por trafico)"]
    M4["M4 · Branch by abstraction<br/>(migrar por dentro, por codigo)"]
    M5["M5 · Extraer un servicio<br/>(anti-corruption layer)"]
    M6["M6 · Migrar datos sin downtime"]
    M7["M7 · Medir el progreso"]
    M1 --> M2 --> M3 --> M4 --> M5 --> M6 --> M7

Léelo así: en M1 te convenciste de que incremental gana; en M2 pusiste la red de seguridad con characterization tests; en M3 aprendiste a desviar tráfico con un facade externo; aquí, en M4, aprendes la variante para cuando no puedes poner ese facade y hay que migrar la implementación por dentro del código; en M5 extraes el servicio a su propio proceso; en M6 migras sus datos; y en M7 mides el avance hasta apagar lo viejo. Los tres patrones de migración —strangler (M3), branch by abstraction (M4) y extracción (M5)— son primos: mismo objetivo, distinto punto de interposición. Elegir el correcto es preguntar: ¿dónde puedo interponer el cambio? Si hay una frontera de red, strangler. Si es código interno, branch by abstraction. Si el objetivo es sacar la pieza a su propio proceso, extracción.

Y la frontera con las guías hermanas del ecosistema: esta guía es la técnica de migración, no el destino. A dónde llega ModernShipping —si termina siendo un módulo más limpio, un servicio propio o una función serverless— lo enseñan architectural-styles-and-boundaries y las guías de estilos. La decisión de migrar —el ADR, el costo, la reversibilidad registrada— es architecture-decisions-and-tradeoffs. Aquí solo enseñamos cómo cambiar la implementación por dentro del viejo al nuevo, sin apagar el sistema y sin una rama larga.

Errores comunes

Creer que branch by abstraction es "meter una interfaz" y ya. Qué pasa: el equipo inserta la abstracción, construye la implementación nueva, conmuta el flag —y da la migración por "hecha", con las dos implementaciones vivas y el flag prendido para siempre—. Por qué pasa: la parte visible y satisfactoria del patrón es llegar a que lo nuevo corra; la parte tediosa es borrar lo viejo y quitar el flag. Cómo detectarlo: si llevas meses con LegacyShipping y ModernShipping los dos en el código, y el feature_flag sigue ahí "por si acaso", no terminaste la migración, la congelaste a mitad. Cómo corregirlo: el patrón es un ciclo con final. Los cinco pasos (insertar, construir, conmutar, validar, borrar) tienen que completarse, y el quinto —borrar la vieja— es el que paga la migración (elimina el doble mantenimiento y el drift). La lección 6 lo trata a fondo; guárdalo como la meta desde el día uno.

Confundir branch by abstraction (interno, por código) con el strangler (externo, por tráfico). Qué pasa: el equipo intenta poner un proxy HTTP delante de una función interna que ni siquiera está expuesta como endpoint, y se enreda buscando una frontera de red que no existe. Por qué pasa: los dos patrones logran lo mismo —migrar de una implementación vieja a una nueva de forma incremental— y es fácil mezclarlos. Cómo detectarlo: pregunta "¿lo que quiero migrar tiene una frontera de red o de proceso donde interponer un proxy?". Si es un endpoint (GET /products), sí: strangler (módulo 3). Si es una función llamada desde adentro del mismo proceso, sin frontera externa (calculate_shipping()), no hay dónde poner el proxy: eso es branch by abstraction. Cómo corregirlo: usa el strangler cuando exista un punto de interposición externo; usa branch by abstraction cuando la migración sea por dentro del código, insertando la abstracción como punto de conmutación. Son primos, no gemelos: el punto de interposición los distingue.

Ejercicios

Ejercicio 1 — Las cinco piezas en el avión. Con la analogía del avión al que se le cambia el motor en vuelo, empareja cada pieza del patrón branch by abstraction con lo que ocurre en el avión: (a) la capa de abstracción, (b) la implementación legacy, (c) la implementación modern, (d) el feature flag, (e) borrar la vieja. Luego explica por qué instalar el adaptador (paso a) no cambia nada para el pasajero, y por qué aun así es el paso más importante.

Ver solución
  • (a) La capa de abstracción → el adaptador / brida universal donde se atornilla cualquier motor. Es el punto neutro entre el fuselaje (los llamadores) y el motor (la implementación).
  • (b) La implementación legacy → el motor viejo atornillado al adaptador. Sigue volando el avión, ahora a través del adaptador en vez de directo al fuselaje.
  • (c) La implementación modern → el motor nuevo montado al lado, conectado al mismo adaptador, sin quitar el viejo. Los dos coexisten.
  • (d) El feature flag → la válvula que dirige la potencia entre los dos motores: primero un poco al nuevo, luego más, hasta el 100%.
  • (e) Borrar la vieja → desmontar el motor viejo una vez que toda la potencia va por el nuevo.

Instalar el adaptador (paso a) no cambia nada para el pasajero porque el avión sigue volando con el mismo motor de siempre, solo que ahora atornillado a través de una brida intermedia. La potencia, la velocidad, el ruido: todo idéntico. Y aun así es el paso más importante porque es el que hace posible todos los demás: sin el adaptador, montar un segundo motor y pasarle potencia de a poco sería imposible —tendrías que arrancar el viejo de un tirón—. El adaptador se instala "vacío" (con solo el motor viejo del otro lado) y a cero riesgo, y es lo que convierte un cambio big-bang en un cambio gradual. En el código, ese adaptador es la abstracción, y ese "no cambia nada" es la prueba de transparencia de la lección 2.

Ejercicio 2 — Lee las dos columnas. En el ejemplo trabajado, la columna flag=OFF (legacy) y la columna flag=ON (modern) dieron números idénticos (25.0, 42.0, 109.0, 60.0). (a) ¿Qué propiedad del llamador checkout_total hace que no haya que tocarlo al conmutar el flag? (b) ¿Qué dice el hecho de que las dos columnas coincidan sobre ModernShipping? (c) Si en un quinto pedido las columnas no coincidieran, ¿qué paso del patrón lo atraparía y qué harías?

Ver solución

(a) La propiedad es que checkout_total depende de la abstracción, no de una implementación concreta: su firma recibe un ShippingCalculator y adentro solo llama calc.cost(order). Nunca nombra a LegacyShipping ni a ModernShipping. Por eso conmutar el flag —que cambia cuál objeto se le pasa— no obliga a tocar una sola línea del llamador. Es el corazón del patrón: los llamadores se atornillan al adaptador, no al motor.

(b) Dice que ModernShipping, a pesar de tener una estructura interna distinta (una tabla RATES como atributo de clase, la condición de envío gratis escrita de otra forma), produce el mismo resultado que LegacyShipping en esos cuatro casos. Es evidencia de paridad de comportamiento —lo mínimo que hay que verificar antes de darle tráfico a la nueva—. Aquí lo vimos en cuatro filas, "a mano"; la validación seria es medirlo sobre muchos casos, que es el parallel-run.

(c) Lo atraparía el parallel-run (paso 4, lección 5): correr las dos implementaciones sobre los mismos pedidos y comparar sus salidas. Una discrepancia significa que ModernShipping todavía no replica el comportamiento del legacy en ese caso —muy probablemente una regla tácita del viejo que la reimplementación no capturó—. Qué harías: no conmutar el flag. Primero arreglar ModernShipping para que coincida con el legacy en ese caso, volver a correr el parallel-run hasta que dé 0 discrepancias, y solo entonces empezar a subir el flag. La regla es dura: no se le da tráfico a la nueva mientras difiera del legacy en un caso conocido.

Ejercicio 3 — ¿Strangler, branch by abstraction, o extraer? Para cada situación, di cuál de los tres patrones primos aplica y por qué: (a) modernizar el endpoint GET /products, ya expuesto por HTTP; (b) reemplazar la función calculate_shipping() llamada desde adentro del monolito, sin frontera de red; (c) sacar todo el módulo catalog a su propio servicio con su propia base de datos.

Ver solución
  • (a) GET /products → strangler fig (módulo 3). Es un endpoint HTTP: tiene una frontera de red clara donde interponer un facade/proxy que decida, request por request, viejo vs nuevo por porcentaje. El punto de interposición es la red.
  • (b) calculate_shipping() interna → branch by abstraction (este módulo, 4). Es una función llamada desde adentro del mismo proceso, sin frontera de red donde poner un proxy. El punto de interposición se inserta en el código: una abstracción entre los llamadores y la implementación, con un flag que conmuta. Es el caso central de este módulo.
  • (c) Sacar catalog a su propio servicio → extraer un servicio (módulo 5). Aquí el objetivo no es solo cambiar una implementación por otra, sino mover la pieza a su propio proceso, con su propia base de datos y un anti-corruption layer que traduzca entre el modelo viejo y el nuevo. Es la extracción, que a menudo usa branch by abstraction y strangler como herramientas dentro del proceso, pero va más allá: cambia la topología de despliegue.

La lección de fondo: los tres patrones logran migración incremental, y la pregunta que los distingue es dónde puedes interponer el cambio. Frontera de red → strangler. Código interno → branch by abstraction. Sacar a un proceso propio → extraer. Saber cuál aplica según el punto de interposición es la mitad de dominarlos.

Resumen y siguiente paso

En esta lección instalaste la técnica interna de la migración incremental: branch by abstraction, la variante del strangler para cuando no hay una frontera de red donde poner un proxy. Viste su idea completa —insertar una abstracción en el código, construir la implementación nueva detrás de ella al lado de la vieja, conmutar con un flag, validar con parallel-run, borrar la vieja— y sus dos metáforas: el motor del avión que se cambia en vuelo montando primero un adaptador neutro, y la tubería que se reemplaza detrás de una válvula de paso sin cortarle el agua a toda la casa. Y lo ejecutaste en su versión mínima: una abstracción ShippingCalculator con dos implementaciones intercambiables por un flag, donde el llamador checkout_total dio el mismo resultado con cualquiera de las dos, sin que hubiera que tocarlo.

Antes de avanzar deberías poder: nombrar los cinco pasos del patrón y en qué lección se desarrolla cada uno; explicar por qué un llamador que depende de la abstracción no se toca al conmutar la implementación; distinguir cuándo aplica el strangler (frontera de red), branch by abstraction (código interno) y la extracción (proceso propio); y decir qué hace la abstracción tan valiosa aunque instalarla "no cambie nada".

La lección 2 hace el primer paso con calma: insertar la capa de abstracción como un seam. Vas a ver cómo se mete la interfaz entre los llamadores y la lógica vieja sin cambiar comportamiento, y vas a ejecutar la prueba de transparencia: comparar, caso por caso, el resultado de la función directa contra el resultado a través de la abstracción, incluidos los casos borde, y verificar que son idénticos. Ese es el paso de menor riesgo de toda la migración, y el que crea el punto de conmutación donde no había frontera de red.

Recursos

  • Martin Fowler, "BranchByAbstraction" (2014) — martinfowler.com/bliki/BranchByAbstraction.html. El texto de referencia del patrón: por qué se llama "branch" aunque no haya una rama de git, y cómo la abstracción hace de punto de conmutación mientras migras la implementación por debajo. La lectura fundacional de todo el módulo. En inglés.
  • Paul Hammant, "branchbyabstraction.com" — branchbyabstraction.com. El sitio dedicado al patrón por el autor que más lo ha documentado en la práctica, con el paso a paso y la conexión con trunk-based development. En inglés.
  • Jez Humble y David Farley, Continuous Delivery (Addison-Wesley, 2010), cap. sobre gestión de ramas y trunk-based development — el marco donde branch by abstraction es la técnica que permite refactorizaciones grandes sin ramas de larga vida. La referencia de cabecera para la lección 7. En inglés.
  • "Trunk-Based Development" — trunkbaseddevelopment.com. La práctica de integrar en main en pasos chicos y siempre integrables, de la que branch by abstraction es la herramienta clave para los cambios grandes. En inglés.