Módulo 4: Branch by abstraction

Sin rama de larga vida: evitar el merge hell

Descripción

Ya conoces los cinco pasos de branch by abstraction: insertar la abstracción, construir la implementación nueva detrás de ella, conmutar con el flag, validar con parallel-run, borrar la vieja. Pero hay una pregunta que quedó sin responder, y es la que le da su nombre paradójico al patrón: ¿por qué "branch" (rama) si en ningún momento creamos una rama de git? La respuesta es el corazón de esta lección, y es la razón profunda de todo lo que hiciste: branch by abstraction es la técnica que te permite hacer un refactor grande —cambiar toda una implementación por otra— sin una rama de git de larga vida. El "branch" del nombre es una rama lógica en el código (la abstracción con dos implementaciones detrás), no una rama de git.

La alternativa —la que el patrón evita— es la tentación natural: "voy a crear una rama, hago el refactor tranquilo ahí sin molestar a nadie, y cuando esté perfecto lo mergeo a main". Suena ordenado. Pero un refactor grande tarda semanas o meses, y durante ese tiempo main no se detiene: el resto del equipo sigue cambiando el mismo código. Tu rama y main divergen. Cuanto más dura la rama, más divergen, y más doloroso es el merge final —hasta el merge hell: días resolviendo conflictos a mano, con riesgo real de perder cambios de un lado o del otro—. Es la misma dinámica del big-bang de todo el módulo, pero aplicada al control de versiones: acumulas riesgo hasta un evento único y explosivo (el merge), en vez de integrarlo de a poco.

Branch by abstraction rompe esa dinámica porque todo el trabajo vive en main, en pasos chicos, cada uno siempre integrable. Insertar la abstracción es un commit pequeño y transparente que va a main hoy. Construir ModernShipping (apagada, sin tráfico) es otro commit a main —no rompe nada, porque el flag está en OFF—. Cada paso es seguro de integrar inmediatamente porque el sistema sigue funcionando en cada punto. Nunca hay una rama que diverja durante meses; nunca hay un merge hell. Esta lección lo mide: el mismo refactor hecho en una rama larga que no integra durante 4 semanas acumula líneas en conflicto al mergear, contra 0 conflictos cuando cada paso se integra en main.

Conexión con el módulo. Las lecciones 2-6 te dieron la mecánica de los cinco pasos; esta te da el porqué de hacerlos así —en main, incrementales— en vez de en una rama aparte. Es lo que conecta branch by abstraction con una práctica más amplia: el trunk-based development, integrar siempre en el tronco. Fíjate en la frontera: esta lección no enseña a usar git (comandos de branch, merge, rebase) —eso es otra guía—; enseña por qué la estrategia de ramas importa para una migración, y por qué el patrón está diseñado para no necesitar ramas largas. La simulación mide conflictos de forma abstracta (conjuntos de líneas), no ejecuta git.

Una analogía: remodelar la casa habitada vs mudarse a una casa paralela

Imagina que quieres remodelar tu casa por completo —cocina, baños, pisos, instalación eléctrica— mientras sigues viviendo en ella con tu familia. Tienes dos estrategias.

La casa paralela (la rama larga). Construyes una segunda casa idéntica en otro terreno, y ahí haces toda la remodelación con calma, sin que nadie te moleste. El problema: mientras construyes la casa paralela durante seis meses, la familia sigue viviendo en la casa original y la va cambiando —cuelgan cuadros nuevos, mueven muebles, arreglan una gotera, pintan un cuarto—. Cuando por fin terminas la casa paralela y quieres "mudar" la familia a ella, descubres que las dos casas ya no coinciden: la original tiene cambios que la paralela no, y la paralela tiene cambios que chocan con los de la original. Tienes que reconciliar seis meses de divergencia de golpe, cuarto por cuarto, decidiendo qué cambio gana en cada choque. Eso es el merge hell: cuanto más tiempo vivieron las dos casas por separado, más choques al juntarlas.

La remodelación en sitio (branch by abstraction en main). Remodelas la casa donde vives, un pedazo a la vez, sin mudarte a ningún lado. Cambias el baño detrás de una válvula de paso (la abstracción) mientras el resto de la casa sigue con agua. Terminas ese baño, lo integras —ya es parte de la casa donde todos viven—, y sigues con el siguiente pedazo. En ningún momento hay "dos casas" que se separen: siempre hay una sola casa, la que habitas, que va cambiando por partes. Como cada cambio se integra de inmediato a la casa real, nunca acumulas divergencia, nunca hay un choque de seis meses. La familia vivió incomodidades pequeñas y pasajeras (un baño cerrado un día), pero nunca el caos de mudar dos casas que se separaron.

La casa paralela es la rama larga: diverge de main mientras dura, y explota al mergear. La remodelación en sitio es branch by abstraction: todo ocurre en la única casa (main), por partes, siempre integrado. La válvula de paso —la abstracción— es lo que hace posible cambiar un pedazo sin cortarle el agua al resto: es la herramienta que permite remodelar en sitio en vez de tener que mudarse a una casa paralela.

Ejemplo trabajado: los conflictos medidos, rama larga vs integración continua

Vamos a medir la diferencia. Modelamos shipping.py como un archivo, y el mismo refactor grande hecho de las dos formas. Cada semana, "main" (el resto del equipo) cambia ciertas líneas del archivo. La rama larga reescribe las líneas 5..24 durante 4 semanas sin integrar; al mergear al final, choca con toda línea que main cambió mientras tanto y que la rama también tocó —esos son los conflictos, y se acumulan—. Branch by abstraction integra su trabajo en main semana a semana: como cada semana parte del main más reciente, lo que main cambió ya está en su copia, y nunca acumula divergencia —0 conflictos—.

# Modelamos shipping.py y el mismo refactor grande hecho de dos formas.
# Cada semana, "main" (el resto del equipo) cambia ciertas lineas del archivo.
main_weekly = {
    1: {10, 11, 12},
    2: {12, 20, 21},
    3: {5, 6, 30},
    4: {21, 22, 40},
}

# --- FORMA A: una RAMA LARGA reescribe shipping.py durante 4 semanas SIN integrar.
# Toca las lineas 5..24. Al mergear al final, choca con toda linea que main cambio
# mientras tanto y que la rama tambien toco: eso son los conflictos. ---
long_branch_edits = set(range(5, 25))   # la rama toca 5..24

# --- FORMA B: BRANCH BY ABSTRACTION. El trabajo vive en main, integrado semana a
# semana en pasos chicos. Cada semana partes del main mas reciente, asi que lo que
# main cambio ya esta en tu copia: nunca acumulas divergencia. Conflictos = 0. ---

print("Conflictos al integrar el mismo refactor, semana a semana:\n")
print(f"{'semana':>7}{'main toco':>26}{'rama larga (acum.)':>20}{'branch-by-abstr.':>18}")
print("-" * 71)
running_main = set()
for week in (1, 2, 3, 4):
    running_main |= main_weekly[week]
    conflicts_long = len(running_main & long_branch_edits)   # choque acumulado
    conflicts_bba = 0                                        # integraste esta semana
    touched = ",".join(str(n) for n in sorted(main_weekly[week]))
    print(f"{week:>7}{touched:>26}{conflicts_long:>20}{conflicts_bba:>18}")

final_long = len(running_main & long_branch_edits)
print("-" * 71)
print(f"\n  Merge final de la rama larga: {final_long} lineas en conflicto (hay que")
print(f"  resolverlas a mano, con riesgo de perder cambios de ambos lados).")
print(f"  Branch by abstraction: 0 conflictos. El refactor entero se integro en main")
print(f"  en pasos chicos, cada uno siempre mergeable.")

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

Conflictos al integrar el mismo refactor, semana a semana:

 semana                 main toco  rama larga (acum.)  branch-by-abstr.
-----------------------------------------------------------------------
      1                  10,11,12                   3                 0
      2                  12,20,21                   5                 0
      3                    5,6,30                   7                 0
      4                  21,22,40                   8                 0
-----------------------------------------------------------------------

  Merge final de la rama larga: 8 lineas en conflicto (hay que
  resolverlas a mano, con riesgo de perder cambios de ambos lados).
  Branch by abstraction: 0 conflictos. El refactor entero se integro en main
  en pasos chicos, cada uno siempre mergeable.

Lee las dos últimas columnas semana a semana. La columna rama larga (acum.) crece: 3, 5, 7, 8. Cada semana que la rama no integra, main cambia más líneas, y algunas de esas líneas son de las que la rama también está reescribiendo (5..24) —así que el conflicto potencial se acumula—. En la semana 1, main tocó 10, 11, 12, todas dentro del rango de la rama: 3 conflictos en potencia. En la 2, main tocó 12 (ya contada), 20, 21: suben a 5. En la 3, 5 y 6 (30 queda fuera del rango de la rama): 7. En la 4, 22 (21 ya contada, 40 fuera): 8. Cuanto más vive la rama, más diverge, más conflictos.

La columna branch-by-abstr. es plana en 0, semana tras semana. ¿Por qué? Porque branch by abstraction integra cada semana. Al partir siempre del main más reciente, las líneas que main cambió ya están en tu copia antes de que tú toques nada —no hay divergencia que reconciliar—. Insertar la abstracción fue un commit chico a main en la semana 1; construir ModernShipping apagada, otro commit chico en la semana 2; y así. Cada paso se integró de inmediato, y como cada paso deja el sistema funcionando (el flag controla qué corre), integrarlo nunca rompió nada. Nunca hubo una rama divergiendo; nunca hubo conflictos.

Y lee el cierre: la rama larga termina con 8 líneas en conflicto que hay que resolver a mano al mergear —y "a mano" significa que alguien decide, línea por línea, qué versión gana, con el riesgo real de elegir mal y perder un cambio de main o de la rama—. Branch by abstraction termina con 0. El mismo refactor, el mismo trabajo total, pero una estrategia lo acumuló en un evento explosivo al final y la otra lo disolvió en pasos que nunca chocaron. Ocho conflictos en un archivo pequeño no suena a catástrofe; en un refactor real sobre decenas de archivos durante meses, esa curva ascendente es la que produce merges de días y bugs de reconciliación.

Profundización: por qué cada paso es integrable, y qué es trunk-based development

La propiedad que hace todo esto posible es que cada paso de branch by abstraction deja el sistema funcionando, y por eso cada paso es seguro de integrar en main de inmediato. Repásalos con esa lente:

Paso                          ¿Integrable en main hoy? Por que
────────────────────────────  ──────────────────────  ──────────────────────────────
1. Insertar la abstraccion    SI                       transparente: no cambia comportamiento
2. Construir ModernShipping   SI                       apagada (flag OFF): no recibe trafico
3. Conmutar el flag (gradual)  SI                       reversible en caliente; empieza en 0%
4. Parallel-run               SI                       corre en la sombra: no expone la nueva
5. Borrar el legacy            SI                       el flag ya esta en 100%, validado

Cada fila es un commit (o varios commits chicos) que puede ir a main sin romper nada, porque el sistema sigue vivo y correcto en cada punto. Esto es lo contrario de un refactor "todo o nada" que solo funciona cuando está completo —ese sí necesitaría una rama, porque a mitad rompería main—. Branch by abstraction está diseñado para que no haya un "a mitad roto": la abstracción y el flag son justamente lo que mantiene el sistema funcional en cada paso intermedio. Por eso puedes integrar continuamente, y por eso no necesitas una rama larga.

Esto tiene un nombre como práctica: trunk-based development —desarrollo basado en el tronco—. La idea es que todo el equipo integra su trabajo en una sola rama principal (main, el "tronco") de forma frecuente —idealmente varias veces al día—, en vez de mantener ramas de funcionalidad de larga vida que divergen. El beneficio es exactamente el que mediste: sin divergencia acumulada, no hay merge hell. Pero el trunk-based development tiene un problema aparente: si integro en main a cada rato, ¿cómo hago un cambio grande que tarda semanas sin romper main mientras está a medias? La respuesta es branch by abstraction (y los feature flags en general). Son las técnicas que permiten meter trabajo incompleto en main de forma segura —detrás de una abstracción, apagado por un flag— para poder integrar continuamente sin esperar a que el cambio grande esté terminado. Branch by abstraction no es solo un patrón de migración: es la herramienta que hace viable el trunk-based development para los cambios grandes.

Una precisión sobre las ramas: esto no dice que las ramas de git sean malas. Una rama de vida corta —unas horas, un par de días— que se integra rápido casi no diverge, y es una práctica perfectamente sana. Lo que el patrón evita es la rama de larga vida: la que vive semanas o meses acumulando divergencia. La curva de conflictos que mediste (3→5→7→8) es una función del tiempo que la rama pasa sin integrar. Ramas cortas: poca divergencia, merge trivial. Ramas largas: mucha divergencia, merge hell. Branch by abstraction te permite hacer, con integración continua en main, lo que de otro modo tentaría a abrir una rama larga.

Errores comunes

"Casi" branch by abstraction, pero en una rama larga que diverge igual. Qué pasa: el equipo usa la mecánica del patrón —inserta la abstracción, construye las dos implementaciones, el flag— pero hace todo ese trabajo en una rama aparte que no integra durante semanas, planeando mergear "cuando esté completo". Por qué pasa: la costumbre de "trabajo en mi rama y mergeo al final" es fuerte, y la mecánica del patrón no la contradice explícitamente. Cómo detectarlo: tienes la abstracción y las dos implementaciones, pero viven en una rama que lleva semanas sin tocar main; el merge final sigue pendiente y da miedo. Cómo corregirlo: el patrón pierde su mayor beneficio si lo encierras en una rama larga. La gracia es que cada paso se integra en main de inmediato —la abstracción hoy, ModernShipping apagada mañana, el flag al 5% pasado mañana—. Si haces todo en una rama y mergeas al final, tienes la divergencia de la rama larga y la complejidad del patrón, lo peor de ambos. Integra cada paso; ese es el punto.

Creer que el patrón es "solo para migraciones" y no ver el trunk-based development. Qué pasa: el equipo aprende branch by abstraction como una receta de migración puntual y no conecta que es la herramienta general para hacer cambios grandes sin ramas largas. Por qué pasa: se enseña con un caso de migración (como este módulo), y es fácil no generalizar. Cómo detectarlo: para otros cambios grandes (no migraciones), el equipo sigue abriendo ramas de larga vida y sufriendo merge hell, sin darse cuenta de que la misma técnica aplica. Cómo corregirlo: reconoce que branch by abstraction —abstracción + flag para meter trabajo incompleto en main de forma segura— sirve para cualquier cambio grande que de otro modo requeriría una rama larga: un rediseño, una nueva dependencia, una reescritura de un módulo. Es la pieza que hace posible el trunk-based development para lo grande, no solo una receta de migración. Verlo así multiplica su valor.

Confundir "rama de larga vida" con "todas las ramas son malas". Qué pasa: el equipo, tras aprender la lección, prohíbe toda rama de git y empuja directo a main sin ninguna aislación, generando inestabilidad. Por qué pasa: sobre-corrección —"si las ramas largas son el problema, ninguna rama es la solución"—. Cómo detectarlo: main se rompe seguido porque hay trabajo a medias empujado sin protección, o el equipo abandona toda revisión de código por evitar ramas. Cómo corregirlo: el problema no son las ramas, es la larga vida de una rama. Las ramas de vida corta —horas o un par de días, integradas rápido, con su revisión— casi no divergen y son sanas. Lo que branch by abstraction evita es la rama que vive semanas acumulando divergencia. Y meter trabajo grande en main de forma segura no significa empujar código roto: significa meterlo detrás de una abstracción y apagado por un flag, precisamente para que main siga estable. Integración frecuente con aislación por flag, no ausencia de aislación.

Ejercicios

Ejercicio 1 — Las dos casas. En la analogía, remodelar la casa habitada por partes evita el caos de mudar una "casa paralela" que divergió durante seis meses. (a) ¿Qué representa, en git, la "casa paralela"? (b) ¿Por qué el caos al mudarse crece con el tiempo que las dos casas vivieron por separado? (c) ¿Qué elemento de branch by abstraction hace posible "remodelar en sitio" sin cortarle el agua al resto de la casa?

Ver solución

(a) La "casa paralela" representa una rama de git de larga vida: una copia separada del código donde haces el refactor "con calma", divergiendo de main mientras el resto del equipo sigue cambiando el código original.

(b) Porque cuanto más tiempo viven las dos casas (ramas) por separado, más cambios acumula cada una que la otra no tiene —la familia cambió la casa original (el equipo cambió main) y tú cambiaste la paralela (tu rama)—. Al juntarlas, hay que reconciliar toda esa divergencia de golpe, y los choques (conflictos) son proporcionales a cuánto cambió cada lado. La simulación lo mostró: la curva 3→5→7→8 crece con las semanas sin integrar. Tiempo separados = divergencia = conflictos.

(c) La capa de abstracción (la válvula de paso). Es lo que permite cambiar un pedazo del sistema —una implementación— sin romper el resto, porque los llamadores hablan con la abstracción y no notan qué hay detrás. Con la abstracción y el flag, puedes meter la implementación nueva en main apagada, integrarla de inmediato, y encenderla de a poco —remodelar en sitio— en vez de tener que aislar todo el refactor en una rama paralela hasta que esté "completo". La abstracción es la herramienta que hace innecesaria la casa paralela.

Ejercicio 2 — Lee la curva de conflictos. La simulación dio, para la rama larga, la secuencia acumulada 3, 5, 7, 8, y para branch by abstraction, 0 en todas las semanas. (a) ¿Por qué la columna de la rama larga crece cada semana? (b) ¿Por qué la de branch by abstraction se queda en 0? (c) Si la rama larga hubiera integrado main a su copia cada semana (sin mergear de vuelta), ¿cómo cambiaría la curva, y qué práctica sería esa?

Ver solución

(a) Crece porque cada semana main cambia líneas nuevas, y algunas caen dentro del rango que la rama está reescribiendo (5..24). Como la rama no integra, esos cambios de main se acumulan como conflictos en potencia: la semana 1 aporta 3 (líneas 10,11,12), la 2 suma 20 y 21, la 3 suma 5 y 6, la 4 suma 22. El total sube monótonamente porque la divergencia solo se acumula mientras no haya integración.

(b) Se queda en 0 porque branch by abstraction integra cada semana: cada paso (insertar la abstracción, construir la implementación apagada, subir el flag) se commitea a main de inmediato. Al partir siempre del main más reciente, las líneas que main cambió ya están en la copia de trabajo antes de tocar nada —no hay divergencia que reconciliar—. Y como cada paso deja el sistema funcionando, integrarlo nunca rompe main. Sin divergencia acumulada, cero conflictos.

(c) Si la rama larga integrara main a su copia cada semana (hacer pull/merge de main hacia la rama, resolviendo poco a poco), la curva dejaría de acumularse: resolvería 3 conflictos chicos la semana 1, unos pocos la 2, etc., en vez de 8 de golpe al final. Los conflictos totales serían similares, pero distribuidos en porciones manejables en vez de un merge hell único. Esa práctica —traer main a la rama con frecuencia— es una forma de integración continua que reduce el dolor del merge; branch by abstraction va un paso más allá y elimina la rama, integrando el trabajo hacia main en cada paso. La lección de fondo es la misma: integrar seguido disuelve la divergencia; dejarla acumular la concentra en una explosión.

Ejercicio 3 — ¿Rama corta o branch by abstraction? Para cada tarea, di si la harías con una rama de git normal (de vida corta) o si necesitas branch by abstraction, y por qué: (a) corregir un typo en un mensaje de error; (b) reemplazar toda la lógica de cálculo de envío por una implementación nueva, trabajo de varias semanas; (c) agregar un endpoint nuevo que no toca código existente.

Ver solución

(a) Rama corta normal. Corregir un typo es un cambio pequeño de horas: una rama de vida corta (o incluso un commit directo con su revisión) que se integra el mismo día. No diverge, no necesita abstracción ni flag. Branch by abstraction sería exagerado —no hay dos implementaciones que conmutar—.

(b) Branch by abstraction. Reemplazar toda la lógica de cálculo de envío durante varias semanas es exactamente el caso del patrón: es un cambio grande, sobre código existente muy usado, que no puede hacerse de un commit sin romper main ni encerrarse en una rama larga sin merge hell. Insertas la abstracción, construyes la nueva detrás apagada, la validas, subes el flag y borras la vieja —todo integrado en main en pasos chicos—. Es para lo que branch by abstraction existe.

(c) Rama corta normal. Un endpoint nuevo que no toca código existente es aditivo: no hay una implementación vieja que reemplazar ni riesgo de romper lo que ya funciona (nadie lo llamaba antes). Una rama de vida corta que agrega el archivo nuevo y se integra pronto basta. Branch by abstraction resuelve el problema de cambiar algo que ya está en uso sin romperlo; si solo agregas algo nuevo, no hay nada que conmutar. La regla que separa (b) de (c): branch by abstraction es para reemplazar/cambiar código vivo de forma incremental; para agregar código nuevo que no interfiere, una rama corta normal es suficiente.

Resumen y siguiente paso

En esta lección entendiste la razón profunda de todo el patrón: branch by abstraction existe para hacer un refactor grande sin una rama de git de larga vida, evitando el merge hell. Viste, con la casa que se remodela en sitio en vez de mudarse a una casa paralela que divergió seis meses, que integrar por partes en la única casa (main) disuelve la divergencia que una rama larga acumularía. Y lo mediste: el mismo refactor en una rama larga acumuló 8 líneas en conflicto al mergear (curva 3→5→7→8, creciendo con las semanas sin integrar), contra 0 conflictos cuando cada paso se integró en main. Aprendiste por qué cada paso del patrón es integrable de inmediato (la abstracción y el flag mantienen el sistema funcionando en cada punto), qué es el trunk-based development y por qué branch by abstraction es la herramienta que lo hace viable para los cambios grandes, y que el enemigo no son las ramas sino las ramas de larga vida.

Antes de avanzar deberías poder: explicar por qué el patrón se llama "branch" sin usar una rama de git; describir cómo diverge una rama larga y por qué el merge hell crece con el tiempo; explicar por qué cada paso de branch by abstraction es seguro de integrar en main de inmediato; y conectar el patrón con el trunk-based development.

La lección 8 es el proyecto capstone: modernizar el ShippingCalculator de Mercado de punta a punta. Vas a integrar los siete conceptos en una sola bitácora de migración ejecutada: insertar la abstracción con su prueba de transparencia, construir ModernShipping al lado, atrapar y arreglar una discrepancia con parallel-run, subir el flag del 0 al 100% con un gate y el burn-down del legacy, borrar la vieja implementación, y —cerrando el arco de esta lección— demostrar que, al vivir todo en main en pasos chicos, el refactor entero tuvo 0 conflictos de merge contra los 8 de la rama larga. Es branch by abstraction completo, medido, sobre el caso de Mercado.

Recursos

  • Martin Fowler, "BranchByAbstraction" (2014) — martinfowler.com/bliki/BranchByAbstraction.html. Fowler explica por qué el patrón se llama "branch" aunque el trabajo viva en el mainline: la "rama" es lógica (la abstracción con dos implementaciones), no una rama de control de versiones. El núcleo de esta lección. En inglés.
  • "Trunk-Based Development" — trunkbaseddevelopment.com. La práctica de integrar en main de forma frecuente en vez de mantener ramas de larga vida, con branch by abstraction y los feature flags como técnicas para los cambios grandes. En inglés.
  • Jez Humble y David Farley, Continuous Delivery (Addison-Wesley, 2010), cap. sobre estrategias de branching — el argumento medido contra las ramas de larga vida y a favor de la integración continua, con branch by abstraction como técnica clave. La referencia de cabecera de esta lección. En inglés.
  • Paul Hammant, "branchbyabstraction.com" — branchbyabstraction.com. El autor conecta el patrón directamente con trunk-based development y el evitar merges dolorosos de ramas largas. En inglés.