El camino sano hacia los patrones nunca fue diseñarlos por adelantado: es llegar a ellos desde código simple cuando el dolor aparece — lo venimos repitiendo desde la balanza y lo convertimos en método en 05-01. Esta lección enseña la mitad que faltaba: la mecánica. Refactorizar es cambiar la estructura del código sin cambiar su comportamiento, y hacerlo hacia un patrón sobre código vivo — que produce, que factura, que tiene usuarios — exige red de seguridad, pasos pequeños que compilan siempre, y criterio para saber cuándo el esfuerzo no compensa. Trabajaremos con el catálogo de transformaciones típicas, cada una con su antes/después en PideYa.

Contenido

  1. Qué es refactorizar (y qué no)
  2. La precondición innegociable: tests
  3. La disciplina: pasos pequeños que siempre compilan
  4. Catálogo de refactorizaciones hacia patrones
  5. Cuándo NO refactorizar
  6. Deuda técnica: el lenguaje del coste/beneficio
  7. Ejercicios y conclusión

Qué es refactorizar (y qué no)

Definición de Martin Fowler (Refactoring, 1999; 2ª ed. 2018): cambiar la estructura interna del código sin cambiar su comportamiento observable. Las dos mitades importan:

  • Si cambias comportamiento (arreglas un bug, añades una función), no estás refactorizando: estás desarrollando. Mezclar ambas cosas en el mismo commit es la receta del "no sé qué rompió esto".
  • Si no hay red que verifique que el comportamiento se conserva, no estás refactorizando: estás reescribiendo y cruzando los dedos.

Joshua Kerievsky (Refactoring to Patterns, 2004) añadió la pieza que une este curso: los patrones no son solo un destino de diseño, son destinos de refactorización — cada uno tiene secuencias de pasos seguros para llegar hacia él (y a veces desde él, cuando sobra: lo veremos en antipatrones). Ambos libros están comentados en la bibliografía del curso.

La precondición innegociable: tests

Antes de mover una línea, necesitas poder responder "¿sigue funcionando?" en segundos. Tres escenarios:

  1. Hay tests que cubren la zona: adelante.
  2. No hay tests, pero el código es testeable: escribe primero tests de caracterización — tests que documentan lo que el código hace hoy, incluidas sus rarezas. No juzgan si es correcto; fotografían el comportamiento actual para detectar cualquier desviación.
  3. No hay tests y el código es un nudo intesteable (dependencias estáticas, base de datos, singletons...): usa un golden master — ejecuta el código con un lote amplio de entradas, guarda las salidas como "master", y tras cada paso compara byte a byte.
// Test de caracterización sobre el cálculo de comisiones legado de PideYa.
// OJO: no afirmamos que 4.15 sea CORRECTO — afirmamos que es lo que hace HOY.
@Test
void caracterizacion_comisionRestauranteEstandar() {
    var calculadora = new CalculadoraComisionLegada();
    // Valores obtenidos EJECUTANDO el código actual, no de la especificación:
    assertEquals(new BigDecimal("4.15"), calculadora.calcular(pedidoDe("27.90"), "ESTANDAR"));
    assertEquals(new BigDecimal("0.00"), calculadora.calcular(pedidoDe("0.00"), "ESTANDAR"));
    assertEquals(new BigDecimal("2.50"), calculadora.calcular(pedidoDe("27.90"), "PREMIUM"));
}

Si al escribirlos descubres un comportamiento que parece bug: anótalo y consérvalo. Se arregla después, en un commit propio; durante la refactorización, el bug reproducido es parte del contrato ("mismo comportamiento" incluye los defectos).

La disciplina: pasos pequeños que siempre compilan

La regla de oro: entre commit y commit, el código compila y los tests pasan. Nunca hay un "estado intermedio roto que arreglaré al final" — porque ese final a veces no llega (llega una urgencia, y la rama muere). El ciclo:

flowchart LR
    A[Tests en verde] --> B[UN paso pequeño:<br/>extraer, mover, introducir]
    B --> C{¿Compila y<br/>tests en verde?}
    C -- Sí --> D[Commit]
    C -- No --> E[Revertir el paso<br/>no depurar sobre rojo]
    E --> B
    D --> F{¿Llegamos<br/>al patrón?}
    F -- No --> B
    F -- Sí --> G[Limpieza final + commit]

Los pasos son los movimientos atómicos de Fowler, que además tu IDE automatiza (y una refactorización automática del IDE es más segura que una manual): Extract Method, Extract Class/Interface, Move Method, Introduce Parameter Object, Replace Constructor with Factory Method, Replace Conditional with Polymorphism. Un patrón se alcanza encadenando media docena de estos movimientos — nunca de un salto.

Catálogo de refactorizaciones hacia patrones

Olor (síntoma) Refactorización Patrón destino
switch/if-else sobre un "tipo" repetido en varios métodos Replace Conditional with Polymorphism Strategy o State
Condicional que decide qué clase instanciar, duplicado Replace Constructor/Conditional with Factory Factory Method
Constructor telescópico (sobrecargas crecientes) Introduce Builder Builder
Clase-dios que orquesta y ejecuta todo Extract Class + fachada sobre lo extraído Facade + colaboradores
Métodos casi idénticos que difieren en pasos Form Template Method Template Method

Switch de tipos → Strategy (o State)

El olor más común. En PideYa, antes de la lección 04-10, el cálculo de envío era:

// ANTES: el mismo switch aparecía también en estimarTiempo() y en descripcionTarifa()
public BigDecimal calcularEnvio(Carrito carrito, Direccion dir) {
    switch (tipoTarifa) {
        case DISTANCIA: return tarifaBase.add(precioPorKm.multiply(distancia(dir)));
        case PLANA:     return new BigDecimal("2.99");
        case GRATIS:    return BigDecimal.ZERO;
        default:        throw new IllegalStateException();
    }
}

La secuencia segura (tests de caracterización primero, commit tras cada paso):

  1. Extract Method de cada rama: calcularPorDistancia(), calcularPlana()... El switch queda como un despachador trivial. Compila, verde, commit.
  2. Extract Interface CalculoEnvio con calcular(carrito, dir) y una implementación por rama, moviendo cada método extraído a su clase. El switch ahora elige la clase en vez de ejecutar la lógica. Verde, commit.
  3. Sustituir el switch por el objeto: el contexto recibe un CalculoEnvio (inyectado o resuelto una vez) y delega. Los switches de los otros métodos caen uno a uno igual. Verde, commit.
  4. Limpieza: eliminar el enum si ya nadie lo usa, o dejarlo solo como clave de configuración → estrategia.

¿Y State? Misma mecánica exacta cuando el "tipo" del switch es una etapa con transiciones (el EstadoPedido de 04-09 nació así de un enum con cuatro switches). El diagnóstico de cuál toca lo da el careo de 04-13; la refactorización es la misma.

Condicionales de creación → Factory

Cuando if (mercado.equals("ES")) new PasarelaRedsys() else new PasarelaConekta() aparece por segunda vez: Extract Method del condicional a un crearPasarela(mercado), luego Move Method a una clase fábrica (o mapa de Suppliers como el RegistroNotificadores), luego sustituir cada duplicado por la llamada. Tres pasos, tres commits — y la evolución new → simple factory → Factory Method de 02-03 recorrida sobre código real. Si los condicionales creaban familias coordinadas, el mismo camino desemboca en Abstract Factory.

Constructor telescópico → Builder

Pedido(cliente), Pedido(cliente, cupon), Pedido(cliente, cupon, notas, programado)... El camino sin romper a nadie:

  1. Crear el Builder junto a los constructores existentes, delegando en el más completo. Verde, commit.
  2. Migrar los puntos de llamada uno a uno al builder (cada migración compila sola). Commits.
  3. @Deprecated en los constructores telescópicos; cuando el último uso desaparece, eliminarlos y mover las validaciones a build(). Verde, commit final.

El paso 1 es la técnica general para APIs con muchos consumidores: construir el destino en paralelo, migrar gradualmente, demoler lo viejo al final — nunca un big bang.

Clase-dios → Facade + extracciones

El GestorPedidos legendario de 800 líneas (validaba, calculaba, cobraba, notificaba, imprimía). No se "convierte en fachada": se vacía. Secuencia: Extract Class de cada responsabilidad cohesionada (ValidadorPedidos, CalculadoraImportes, ServicioCobro...), dejando en GestorPedidos solo la coordinación — que al final es una Facade legítima, a menudo con el nombre cambiado a FachadaCheckout para que el nombre cuente la verdad. La diferencia con la clase-dios original: ya no ejecuta, orquesta; cada pieza extraída es testeable sola.

If/else de formato → Template Method

Los informes de cierre de 04-11: tres métodos casi clónicos (generarCsv, generarPdf, generarHtml) que cargaban y agregaban igual pero formateaban distinto. Form Template Method: igualar la forma de los tres (mismos pasos, mismo orden), Extract Method de cada paso, subir los comunes a una superclase abstracta, dejar los variables como abstractos. Si más adelante los pasos variables necesitan combinarse en caliente, el destino evoluciona a Strategy — la frontera del careo herencia/composición.

Cuándo NO refactorizar

Refactorizar tiene coste (tiempo, riesgo, revisión) y solo paga si el código va a cambiar. No refactorices:

  • Código estable que nadie toca: si el módulo de exportación contable lleva tres años sin un cambio y no hay ninguno previsto, su switch feo no le duele a nadie. La fealdad no es deuda si nadie paga intereses.
  • Código que va a morir: si la integración se reemplaza el trimestre que viene, embellecerla es amortizar un coche camino del desguace.
  • Sin red de seguridad y sin posibilidad de construirla ahora: anota la deuda y espera mejor momento — refactorizar a ciegas convierte "código feo que funciona" en "código bonito que quizá no".
  • En mitad de otra cosa: la "refactorización oportunista" que engorda un commit de bug con 40 ficheros renombrados. Regla práctica: refactor y feature, commits (idealmente PRs) separados.

Deuda técnica: el lenguaje del coste/beneficio

La metáfora de Ward Cunningham da el idioma para decidir y para explicárselo a negocio: el diseño subóptimo es un préstamo (entregar antes a cambio de estructura peor) y su interés es el sobrecoste de cada cambio futuro en esa zona. De ahí las reglas de gestión:

  • El interés solo se paga en código que cambia. Por eso la refactorización se prioriza por frecuencia de cambio × dolor por cambio, no por fealdad. Un git log --since="6 months ago" --name-only te dice dónde están los puntos calientes; cruzarlo con "dónde sufrimos" señala el mejor euro invertido.
  • Amortiza con la regla del boy scout: deja el código un poco mejor de como lo encontraste, en la zona que ya estás tocando. Pequeño, continuo, sin pedir permiso.
  • Las refactorizaciones grandes se pagan como proyectos: con objetivo ("poder añadir un mercado nuevo en días y no semanas"), no como "limpiar código" — negocio compra capacidad, no estética.

Errores Comunes y Consejos

  • Refactorizar sin red ("es un cambio pequeño, qué puede salir mal"). Los tests de caracterización se escriben en una tarde; el bug en producción se paga en semanas de confianza.
  • El big bang: rama refactor-total de tres semanas que no compila hasta el día 15 y muere en conflictos. Pasos pequeños, integración continua, siempre en verde.
  • Cambiar comportamiento de tapadillo ("ya que estoy, arreglo esto"). Rompe el contrato de la caracterización y ensucia el diff. Bug aparte, commit aparte.
  • Refactorizar hacia el patrón equivocado por saltarse el diagnóstico: el método de 05-01 va antes que la mecánica de esta lección. La secuencia es síntoma → diagnóstico → destino → pasos.
  • Pasarse de destino: el olor pedía Strategy y acabó habiendo Strategy + Factory + Observer "ya puestos". Cada patrón extra necesita su propio síntoma — si no, acabas de fabricar el material de la próxima lección.
  • Consejo: usa las refactorizaciones automáticas del IDE (Rename, Extract Method/Interface, Move) siempre que existan: son transformaciones verificadas que no rompen referencias.
  • Consejo: pon nombre a los commits de la secuencia ("paso 2/5: extraer interfaz CalculoEnvio") — el revisor te lo agradecerá y tú podrás revertir quirúrgicamente.

Ejercicios

Ejercicio 1: planificar la secuencia

El método generarTicketCocina(Pedido p, String formato) de PideYa tiene un if-else sobre formato ("TERMICA_58", "TERMICA_80", "PANTALLA") con un 70% de código duplicado entre ramas (cabecera y pie idénticos, cuerpo distinto). Hay dos tests que cubren "TERMICA_58" y ninguno más. Escribe el plan completo: (a) qué red de seguridad construyes primero y cómo, (b) el patrón destino con su diagnóstico, (c) la secuencia de pasos con sus puntos de commit.

Ejercicio 2: ¿refactorizar o no?

Decide y justifica con coste/beneficio:

  1. El parser de un formato de fichero que un único proveedor dejó de emitir; se mantiene "por si acaso" y no se toca desde 2024.
  2. La clase Checkout se ha modificado en 14 de los últimos 20 sprints; cada cambio de forma de pago obliga a tocar 5 métodos con condicionales sobre tipoPago, y ya hubo dos regresiones.
  3. Un compañero propone migrar todos los getters/setters del proyecto a records "para modernizar", 300 clases, sin ningún cambio funcional previsto en la mayoría.

Ejercicio 3: la trampa del paso grande

Durante la migración del constructor telescópico de Restaurante al Builder, un compañero propone: "borro ya los cuatro constructores viejos y arreglo los 60 errores de compilación esta tarde". Explica (a) los dos riesgos concretos de ese plan frente a la migración gradual, (b) qué señal del ciclo de la lección está violando, (c) cómo reconducirlo manteniendo su objetivo de acabar esta semana.

Soluciones

Solución 1: (a) Tests de caracterización para "TERMICA_80" y "PANTALLA" (los que faltan): ejecutar el código actual con pedidos representativos (con extras, con notas, vacío de notas) y fijar las salidas exactas como esperadas — si el ticket es texto multilínea, un golden master por fichero comparado línea a línea es aún más cómodo. (b) Diagnóstico: flujo fijo (cabecera → cuerpo → pie) con pasos variables por formato y sin necesidad de cambiar en caliente → Template Method (careo con Strategy mediante). (c) Secuencia: 1. igualar la forma de las tres ramas (mismos pasos en el mismo orden) — verde, commit; 2. Extract Method por paso (imprimirCabecera, imprimirCuerpo, imprimirPie) — verde, commit; 3. crear TicketCocina abstracta con el método plantilla generar() y una subclase por formato, moviendo los cuerpos — verde, commit; 4. sustituir el if-else por la selección de subclase (probablemente un mini-factory, segundo síntoma, patrón propio) — verde, commit; 5. borrar el método original — verde, commit final.

Solución 2: (1) No: código congelado sin cambios previstos — la deuda sin intereses no se amortiza; como mucho, documentar que es candidato a borrado. (2) Sí, y con prioridad: frecuencia de cambio altísima × dolor probado (5 métodos, 2 regresiones) — es el punto caliente exacto que la métrica busca; destino probable Strategy para tipoPago (más quizá un Factory para su creación), con caracterización previa de los flujos de pago. (3) No como proyecto: cambio masivo sin cambio funcional previsto = coste y riesgo hoy, beneficio especulativo; reconducir a regla del boy scout — migrar a record las clases que ya se toquen por otro motivo, y solo donde el record no altere el contrato (¡equals/hashCode cambian!).

Solución 3: (a) Riesgo 1: esas "tardes de 60 errores" no compilan hasta el último arreglo — si surge una urgencia a mitad, la rama queda muerta o se fuerza un merge a medias; riesgo 2: 60 arreglos mecánicos bajo presión son 60 oportunidades de variar sutilmente la semántica (¿ese constructor aplicaba un default que el builder no aplica?) sin que la compilación lo detecte. (b) Viola "entre commit y commit el código compila y los tests pasan" — el estado roto prolongado es exactamente lo que el ciclo prohíbe. (c) Mismo objetivo, otra mecánica: builder delegando en el constructor completo hoy (verde en una hora), migrar los 60 usos en tandas pequeñas durante la semana (cada tanda compila y se commitea sola, se puede repartir entre el equipo), @Deprecated el miércoles, borrar el viernes cuando el IDE confirme cero usos.

Conclusión

Refactorizar hacia patrones es una disciplina completa: red de tests (de caracterización o golden master cuando no los hay), pasos atómicos que siempre compilan, el catálogo de secuencias — switch a Strategy/State, condicional de creación a Factory, telescópico a Builder, clase-dios a Facade, clones de formato a Template Method — y el criterio económico de la deuda técnica para elegir dónde sí y dónde no. Fowler da los movimientos, Kerievsky los destinos, y el método de 05-01 el diagnóstico previo. Pero este camino tiene dirección de vuelta: a veces el problema no es llegar al patrón sino que alguien llegó de más — patrones plantados sin síntoma, Singletons que esconden estado global, fábricas de una sola cosa. Reconocer esos excesos, y saber deshacerlos, cierra el módulo: Antipatrones: Cuándo los Patrones se Vuelven un Problema.

Curso de Patrones de Diseño de Software

Módulo 1: Introducción a los Patrones de Diseño

Módulo 2: Patrones Creacionales

Módulo 3: Patrones Estructurales

Módulo 4: Patrones de Comportamiento

Módulo 5: Aplicación de Patrones de Diseño

Módulo 6: Patrones de Diseño Avanzados

Módulo 7: Recursos Adicionales y Conclusión

© Copyright 2026. Todos los derechos reservados