El código legacy es el código que temes — código sin tests, con intención poco clara y una larga sombra de “nadie sabe bien qué depende de qué”. Reescribir desde cero es el instinto de novato y el último recurso del senior. Refactorizar in-place es el verdadero oficio: cambiar el sistema pieza a pieza, cada paso reversible, nunca varado en un estado a medio construir.
Qué hace difícil al legacy
Tres propiedades se componen:
- Sin tests — no hay red de seguridad, así que cualquier cambio es una apuesta.
- Sin un solo dueño — quienes lo escribieron se fueron; los dueños actuales saben menos que tú.
- Acoplamiento oculto — renombrar una función rompe un script en otro repo que nadie recordaba.
Un rewrite greenfield hereda los tres problemas, y reintroduce cada bug que el código legacy acumuló. El refactor in-place preserva el historial de bugs de producción mientras mejora la estructura.
El Strangler Fig
Planta un sistema nuevo junto al viejo. Dirige una porción de tráfico hacia él. Cuando esa porción funciona, dirige más. Cuando el sistema nuevo maneja todo, jubila al viejo.
llamadores → [ router ]
↓ ↓
sistema viejo sistema nuevo
↑ ↑
superficie completa hoy slice nuevo hoy, todo mañana
Propiedades clave:
- Reversible en cada paso: rollback del tráfico si el nuevo slice rompe.
- Paralelo: el sistema nuevo se puede construir mientras el viejo sigue generando ingresos.
- Sin fecha límite big-bang: la única fecha que importa es “viejo jubilado”, y solo te comprometes cuando el nuevo maneja demostrablemente toda la superficie.
El patrón funciona a múltiples escalas: un microservicio reemplazando un módulo, una base de datos migrando por tenant a un esquema nuevo, una transición de framework de UI detrás de un feature flag.
Seams
Un seam es un lugar en código existente donde puedes alterar el comportamiento sin editar el código circundante — usualmente sustituyendo una dependencia. Ejemplos:
| Tipo de seam | Ejemplo | Cuándo exponerlo |
|---|---|---|
| Object seam | Inyecta un fake en la construcción | La clase es instanciable en tests |
| Link seam | Cambia un import de módulo por un stub en tiempo de test | Controlas el build / module map |
| Pre-processor seam | #ifdef TEST elimina APIs no disponibles | El lenguaje lo soporta; raro en stacks modernos |
La mayoría del código legacy no tiene seams. El primer refactor es introducir uno: extraer la dependencia detrás de una interfaz, luego el cambio real ocurre contra la interfaz. Extract Interface es el movimiento habilitante que desbloquea todo lo demás.
Branch-by-Abstraction
Necesitas refactorizar LegacyPaymentService a NewPaymentService sin congelar el desarrollo. El patrón:
- Crea una abstracción (
PaymentGateway) que el servicio viejo implemente. - Actualiza cada llamador para depender de la abstracción, no del servicio concreto.
- Implementa
NewPaymentGatewaycontra la misma abstracción — ejecuta en paralelo. - Gira el routing, gateado por feature flag por tenant o por región.
- Jubila
LegacyPaymentGateway.
El equipo sigue enviando cambios de producto durante el refactor — mantienen LegacyPaymentGateway actualizado mientras el nuevo se construye. Los refactors big-bang fallan precisamente porque pausan el desarrollo de producto; branch-by-abstraction nunca lo pausa.
El Método Mikado
Para refactors invasivos donde cada cambio parece engendrar dos prerequisitos, el método Mikado funciona como un árbol recursivo de undo:
- Enuncia el resultado deseado (“extraer
parseConfiga su propio archivo”). - Prueba el cambio. Si el build / test falla con una dependencia, tienes un nuevo sub-objetivo (“necesito exponer el tipo
Config”). Revierte. - Recurre sobre cada sub-objetivo hasta que uno funcione limpio.
- Aplica en orden inverso al árbol, recommiteando cada capa.
El método convierte “rompí todo” en “tengo una lista de cambios alcanzables”. El repositorio siempre compila antes de cada commit; nunca varas al equipo en un estado roto.
Continuo vs Big-Bang
| Propiedad | Big-bang | Continuo |
|---|---|---|
| Visibilidad | Invisible hasta “terminar” | Cada paso va a producción |
| Riesgo | Concentrado en una fecha límite | Distribuido en muchas apuestas pequeñas |
| Reversibilidad | A menudo imposible una vez empezado | Cada paso es revertible |
| Costo cultural | El autor puede ser héroe o chivo expiatorio | Refactorizar es un martes normal |
Elige siempre continuo, a menos que el producto requiera una migración de un solo corte (raro; “relanzamos en la Super Bowl”). Incluso entonces, prepara strangler por debajo — el momento big-bang se convierte en un flip de flag, no en un deploy al filo de la navaja.
Saber cuándo parar
La disciplina más difícil. Una checklist útil:
- El propósito del refactor (¿onboarding más rápido? ¿habilitar feature X? ¿eliminar una clase de bugs?) ya es alcanzable. Declarar terminado es el objetivo, no seguir puliendo.
- El costo por cambio volvió a subir: el próximo movimiento es difícil porque el código ya soporta lo que necesita.
- El diff se convirtió en un nit cosmético de una línea en lugar de un movimiento estructural. Para.
Los ingenieros sobre-refactorizan cuando tratan al codebase como un objeto de arte en lugar de una herramienta. La mayoría de los refactors deberían parar justo antes de “perfecto”, porque el refactor marginal rara vez llega al cliente pero siempre cuesta el presupuesto de review-time del equipo.
Trayectoria de práctica
- Identifica una función “aterradora” que nadie quiere tocar en un codebase que uses. Escribe tres tests de caracterización para ella; describe qué cambia en tu disposición a editarla.
- Aplica Extract Interface a una dependencia legacy. Anota qué se vuelve testeable que antes no lo era.
- Dibuja un plan de migración strangler-fig para cualquier servicio que conozcas: nombra la capa router, los slices, la regla de rollback.
- Elige una mejora estancada en “primero hay que refactorizar” y aplica el método Mikado: dibuja el árbol de dependencias, encuentra una hoja que puedas enviar hoy.
- Corre una pasada de “¿qué borraría?” sobre el sistema después de tu refactor. La mayoría de refactors legacy dan restando código. Si el tuyo suma, pregunta por qué.
Cuándo es la herramienta correcta
| Situación | Conclusión |
|---|---|
| Módulo de alto riesgo sin tests | Tests de caracterización primero; refactor después |
| Migración que necesita enviar producto | Branch-by-abstraction; nunca congelar el desarrollo |
| Reemplazo con puntos de corte claros | Strangler fig; dirige slices pequeños primero |
| Refactor bloqueado por dependencias enredadas | Método Mikado; nunca varar al equipo en estado roto |
| “Refactor listo, pero quiero seguir puliendo” | Para. Envía. Refactoriza otra vez cuando aparezca la próxima necesidad real |