En los módulos 2 a 4 cada patrón tuvo su lección y su rincón de PideYa; en la lección anterior aprendiste el método para elegirlos. Esta lección junta ambas cosas y las lleva a la escala real: funcionalidades completas donde varios patrones colaboran, y donde lo interesante ya no es cada pieza sino las costuras — el punto exacto donde un patrón termina y otro empieza, y por qué la frontera está ahí y no en otro sitio. Veremos dos casos: el flujo completo de un pedido (seis patrones ya construidos, vistos por fin de una sola pasada) y un módulo nuevo de fidelización construido desde cero aplicando el método — incluidas las decisiones de no usar patrón, que en código real son la mitad del diseño.
Contenido
- Caso A: el viaje completo de un pedido — seis patrones, cinco costuras
- Las costuras, una a una
- Caso B: el módulo de fidelización, diseñado desde cero con el método
- Los trade-offs: dónde decidimos no usar patrón
- Ejercicios y conclusión
Caso A: el viaje completo de un pedido
Cada patrón de esta secuencia tiene su lección; lo que nunca habíamos dibujado es el conjunto. Un cliente pulsa "confirmar pedido" y esto es lo que ocurre:
sequenceDiagram
participant App as App del cliente
participant F as FachadaCheckout
participant CH as Cadena de validación
participant S as CalculoEnvio (Strategy)
participant B as Pedido.Builder
participant P as Pedido (State)
participant O as Observadores
App->>F: confirmarPedido(carrito, cliente)
F->>CH: manejar(solicitud)
Note over CH: Fraude → Zona → Stock → Mínimo<br/>(cualquiera puede cortar)
CH-->>F: aprobada
F->>S: calcular(carrito, direccion)
S-->>F: gastosEnvio
F->>B: construir con líneas, envío, impuestos
B-->>F: Pedido (en estado CREADO)
F->>P: pagar()
Note over P: EstadoCreado autoriza<br/>y transita a PAGADO
P->>O: transitarA difunde el cambio
Note over O: NotificadorCliente, MonitorCocina,<br/>PanelEstadisticas... reaccionan
F-->>App: ConfirmacionPedido
En texto, la cadena de responsabilidades (con minúscula) es esta:
- Facade —
FachadaCheckoutes el único punto de entrada: la app no conoce ni la cadena, ni las estrategias, ni el builder. Su papel: orquestar y ocultar. - Chain of Responsibility — la fachada dispara la validación (
ManejadorFraude→ManejadorZonaReparto→ManejadorStock→ManejadorImporteMinimo). Su papel: decidir si el pedido entra, sin que la fachada sepa cuántos filtros hay. - Strategy —
CalculoEnviocalcula los gastos con la variante del mercado. Su papel: un cálculo intercambiable sinifen la fachada. - Builder —
Pedido.Buildermonta el objeto con sus líneas, envío e impuestos, validando invariantes enbuild(). Su papel: construcción compleja fuera del constructor. - State — el pedido nace en CREADO y
pagar()transita a PAGADO solo si el estado lo autoriza. Su papel: que cada operación sea legal solo cuando toca. - Observer —
transitarAdifunde la transición a cliente, cocina y estadísticas. Su papel: que los interesados se enteren sin que el pedido los conozca. La máxima del módulo 4 sigue vigente: State autoriza, Observer difunde.
Las costuras, una a una
Lo que hace que esto sea un diseño y no un montón de patrones es dónde está cada frontera:
Costura fachada–cadena. La fachada conoce un objeto: el primer eslabón (inyectado ya montado, como se configuró en 04-02). Si mañana compliance exige un quinto filtro, se añade al montaje de la cadena y la fachada no cambia. La costura es la interfaz ManejadorPedido: por encima de ella, orquestación; por debajo, política de admisión.
public class FachadaCheckout {
private final ManejadorPedido cadenaValidacion; // primer eslabón, ya enlazado
private final CalculoEnvio calculoEnvio; // estrategia del mercado
private final RegistroEventos eventos;
public ConfirmacionPedido confirmarPedido(Carrito carrito, Cliente cliente) {
// Costura 1: la fachada delega la admisión y solo mira el resultado
ResultadoValidacion resultado = cadenaValidacion.manejar(new SolicitudPedido(carrito, cliente));
if (!resultado.aprobada()) {
throw new PedidoRechazadoException(resultado.motivo());
}
// Costura 2: el cálculo variable está fuera; la fachada solo lo invoca
BigDecimal envio = calculoEnvio.calcular(carrito, cliente.direccion());
// Costura 3: la construcción compleja vive en el Builder, no aquí
Pedido pedido = new Pedido.Builder(cliente)
.lineas(carrito.lineas())
.gastosEnvio(envio)
.build(); // nace en estado CREADO
// Costura 4: la fachada pide la operación; el ESTADO decide si es legal
pedido.pagar(); // CREADO → PAGADO... y ahí salta la costura 5:
// transitarA difunde a los observadores.
return new ConfirmacionPedido(pedido);
}
}Fíjate en lo que la fachada no hace: no valida (cadena), no calcula (estrategia), no construye (builder), no cambia estados a mano (State) y no notifica a nadie (Observer). Diez líneas de orquestación pura — cada responsabilidad expulsada hacia el patrón cuyo síntoma la reclamaba. Esa es la prueba de que las costuras están bien puestas: la fachada sigue siendo aburrida.
Costura State–Observer — la más fina del sistema y la más fácil de romper. Un solo punto las conecta:
// Dentro de Pedido: el ÚNICO lugar donde ambos patrones se tocan
void transitarA(EstadoPedido nuevoEstado) {
EstadoPedido anterior = this.estado;
this.estado = nuevoEstado; // State: la transición ya autorizada
observadores.forEach(o -> o.alCambiarEstado(this, anterior, nuevoEstado)); // Observer
}Si un observador empezara a provocar transiciones (cocina que al enterarse de PAGADO llama a prepararse() sobre el mismo pedido en el mismo hilo), la difusión se volvería recursiva y el orden de notificación pasaría a ser lógica de negocio encubierta — la señal de que esa reacción pertenece a un Mediator o a un comando explícito, no al observador.
Caso B: el módulo de fidelización, desde cero
Negocio pide: "programa de puntos: cada pedido entregado acumula puntos según reglas por nivel (bronce/plata/oro); los puntos se canjean por cupones; los cupones se aplican en el checkout". No hay código previo. Apliquemos el método de 05-01 decisión a decisión:
| Decisión | Fuerzas (qué varía / qué es estable) | Diagnóstico | Resultado |
|---|---|---|---|
| ¿Cómo se entera fidelización de las entregas? | Varía quién escucha; estable el pedido | Difusión a interesado anónimo, sin protocolo | Observer: AcumuladorPuntos implements ObservadorPedido — nos suscribimos a la infraestructura existente; el pedido no cambia ni una línea |
| ¿Cómo se calculan los puntos por nivel? | Varía el algoritmo por nivel (oro multiplica, plata redondea, bronce plano) | Algoritmos alternativos elegidos desde fuera | Strategy: CalculoPuntos con una implementación por nivel |
| ¿Cómo se aplican los cupones al total? | Varía la regla de descuento; estable el checkout | Ídem — y las reglas ya combinan (cupón + envío gratis) | Strategy para la regla + los cupones aplicables como lista ordenada (ver trade-offs) |
| ¿Dónde se engancha al checkout? | Estable: la fachada orquesta | El paso "aplicar descuentos" es una llamada más | Una línea nueva en FachadaCheckout, sin patrón nuevo |
| ¿Cómo se crean los cupones al canjear? | Tres tipos hoy, campaña nueva cada trimestre | El canje sabe qué quiere, no cuál clase | Factory Method con registro de Suppliers, réplica del RegistroNotificadores |
| ¿El nivel del cliente (bronce→plata→oro)? | Sube por puntos, baja por inactividad | ¿Transiciones con reglas propias por etapa? Sí, pero... | Espera: hoy es solo un umbral numérico — enum + CalculoPuntos por nivel. Nota de intención: "si aparecen operaciones legales por nivel, evaluar State" |
El corazón del módulo, con las piezas elegidas:
// Observer: fidelización se suscribe; el resto del sistema ni se entera de que existe
public class AcumuladorPuntos implements ObservadorPedido {
private final RepositorioPuntos repositorio;
private final Map<Nivel, CalculoPuntos> calculos; // Strategy por nivel
@Override
public void alCambiarEstado(Pedido pedido, EstadoPedido anterior, EstadoPedido nuevo) {
if (!(nuevo instanceof EstadoEntregado)) {
return; // solo nos interesa la entrega
}
Cliente cliente = pedido.cliente();
CalculoPuntos calculo = calculos.get(cliente.nivel());
int puntos = calculo.calcular(pedido); // el algoritmo del nivel, y solo él
repositorio.abonar(cliente, puntos);
}
}
// Strategy: cada nivel encapsula SU aritmética, el acumulador no sabe cuál usa
public class CalculoPuntosOro implements CalculoPuntos {
@Override
public int calcular(Pedido pedido) {
return pedido.total().multiply(new BigDecimal("2")).intValue(); // x2 para oro
}
}La decisión más valiosa de la tabla es la primera: reutilizar el Observer existente en vez de inventar infraestructura. Un módulo nuevo bien diseñado se acopla a las costuras que el sistema ya ofrece — por eso las costuras importan más que los patrones.
Los trade-offs: dónde decidimos no usar patrón
Tres tentaciones descartadas, con su porqué — porque el diseño real se cuenta también por lo que no se construyó:
- ¿Chain of Responsibility para aplicar los cupones en orden? Tentador: "cada cupón decide si aplica y pasa al siguiente"... pero todos los cupones aplicables deben aplicarse, nadie corta la cadena. Falla la prueba del algodón de 04-13. Un
forsobre unaList<ReglaDescuento>ordenada hace lo mismo con cero clases extra:
BigDecimal total = subtotal;
for (ReglaDescuento regla : cuponesAplicables) { // orden: mayor descuento primero
total = regla.aplicar(total, carrito); // Strategy sí; Chain no hacía falta
}- ¿Interpreter para las reglas de canje? ("500 puntos Y nivel >= plata"). Ya existe
ExpresionReglade 04-04 para promociones... pero las reglas de canje hoy son dos y las escribe el propio equipo en Java, no marketing en texto. Sin autor externo del mini-lenguaje, Interpreter es coste sin cliente. Nota de intención en el código y a otra cosa. - ¿Facade propia para fidelización? El módulo tiene tres operaciones públicas (
consultarPuntos,canjear,aplicarCupones) sobre dos clases. Una fachada delante de tan poco es una puerta monumental para un trastero — se reevaluará si el módulo crece.
El saldo final del módulo: dos Strategy, un Observer reutilizado, un Factory Method con registro — y tres patrones descartados con argumento. Eso es aplicar el catálogo con criterio: cada patrón presente tiene un síntoma que lo reclama, y cada ausencia tiene un porqué articulado.
Errores Comunes y Consejos
- Diseñar el módulo "por patrones" ("necesitará su Singleton, su Factory y su Facade") en vez de por decisiones. La tabla decisión-a-decisión del caso B es el antídoto: cada fila tiene fuerzas y diagnóstico, no cuota de patrones.
- Duplicar infraestructura en vez de enchufarse a las costuras existentes. Antes de crear tu propio sistema de eventos, cadena o fábrica, mira si el sistema ya expone uno (el
AcumuladorPuntosno inventó nada: se suscribió). - Costuras gordas: si la fachada empieza a validar "solo este caso especial" o un observador empieza a orquestar, la frontera se está deshaciendo. La fachada debe seguir siendo aburrida; el observador, reactivo.
- No registrar los descartes. El próximo desarrollador verá el
forde cupones y pensará "aquí falta un Chain". Un comentario de dos líneas con el porqué del descarte vale más que la elegancia silenciosa. - Consejo: en revisión de diseño, pide siempre las dos listas — patrones usados y patrones descartados. Un diseño sin descartes casi nunca ha sido diseñado: ha sido decorado.
Ejercicios
Ejercicio 1: mapa de costuras
Sin mirar el diagrama: dibuja (o describe) el viaje del pedido del caso A nombrando los seis patrones y las cinco costuras (qué interfaz o punto de código separa cada par). Después responde: si negocio pide "los pedidos de más de 100 € requieren verificación telefónica antes de cocinarse", ¿qué costura absorbe el cambio y qué patrones no se enteran?
Ejercicio 2: extender fidelización con el método
Negocio amplía: "en el cumpleaños del cliente, sus pedidos acumulan puntos dobles, se combinen como se combinen con su nivel". Aplica el método: fuerzas, diagnóstico, candidato — y cuidado: hay una solución con patrón estructural del módulo 3 y una solución sin patrón; presenta ambas y elige según los filtros.
Ejercicio 3: detectar el descarte erróneo
Un compañero implementó la aplicación de cupones como Chain of Responsibility real: cada cupón hereda de ManejadorCupon, aplica su descuento y llama a siguiente.manejar(...). Funciona. Escribe (a) por qué formalmente no es un Chain aunque lo parezca, (b) qué problema práctico causará el día que un cupón deba "cortar" de verdad (cupón exclusivo no combinable), (c) la refactorización mínima hacia la versión con lista.
Soluciones
Solución 1: Costuras: (1) app–fachada: el método confirmarPedido (la app no ve nada más); (2) fachada–cadena: la interfaz ManejadorPedido del primer eslabón; (3) fachada–estrategia: la interfaz CalculoEnvio; (4) fachada–builder: la API fluida de Pedido.Builder; (5) pedido–observadores: transitarA. El cambio de la verificación telefónica es una regla de admisión condicional que puede cortar → nuevo eslabón ManejadorVerificacionTelefonica en el montaje de la cadena (costura 2)... o, si la verificación es asíncrona (esperar la llamada), un estado nuevo EN_VERIFICACION en el ciclo de vida (costura 5, State). Con la lectura síncrona: Builder, Strategy, State y Observer no se enteran; la fachada tampoco — solo cambia el montaje de la cadena.
Solución 2: Fuerzas: varía un multiplicador temporal que se suma sobre el cálculo del nivel; estable la interfaz CalculoPuntos y el acumulador. Opción con patrón: Decorator — CalculoPuntosCumpleanos implements CalculoPuntos que envuelve al cálculo del nivel y duplica su resultado; combina con cualquier nivel sin tocarlo (exactamente los extras de 03-05). Opción sin patrón: un if (cliente.esSuCumpleanos()) puntos *= 2; en el acumulador. Filtros: si el "puntos dobles" es el único modificador previsto, el if gana (KISS); si marketing ya habla de "puntos dobles los martes" y "x3 en tu primera semana premium" — modificadores que se apilan — el eje de variación es real y Decorator paga su coste. La respuesta honesta depende del roadmap, y decirlo así es la respuesta correcta.
Solución 3: (a) La prueba del algodón: en un Chain cada eslabón decide atender o pasar y puede cortar; aquí todos aplican siempre y nadie corta — es una delegación incondicional, o sea la estructura de Decorator con nombre de Chain, y la intención real es "aplicar todos" (un recorrido). (b) Cuando llegue el cupón exclusivo, cortar la cadena hará que el resultado dependa del orden de montaje de forma invisible (¿corta antes o después del cupón de envío gratis?), y el bug será de configuración, indetectable en el código de los cupones. Con lista explícita, la exclusividad es una regla visible (filtrar antes de iterar). (c) Extraer de cada ManejadorCupon su lógica a ReglaDescuento.aplicar(total, carrito), eliminar el campo siguiente, y sustituir el arranque de la cadena por el for sobre la lista ordenada — paso a paso y con tests, como manda la lección de refactorización.
Conclusión
Has visto el catálogo funcionando a escala real: seis patrones colaborando en el viaje de un pedido con la fachada como director de una orquesta que no toca ningún instrumento, y un módulo nuevo diseñado decisión a decisión — con sus patrones elegidos por síntoma, su infraestructura reutilizada por las costuras y sus tres descartes argumentados. La lección de fondo: un buen diseño se reconoce tanto por dónde están las fronteras como por qué patrones lo habitan. Pero PideYa es nuestro laboratorio; la pregunta natural es si el software que usas cada día — el JDK, Spring, Hibernate — está construido igual. Lo está, y aprender a reconocer sus patrones al leer su código es la siguiente lección: Patrones de Diseño en Proyectos Reales.
Curso de Patrones de Diseño de Software
Módulo 1: Introducción a los Patrones de Diseño
- ¿Qué son los Patrones de Diseño?
- Historia y Origen de los Patrones de Diseño
- Principios de Diseño: SOLID y Otros Fundamentos
- UML Esencial para Entender Patrones
- Clasificación de los Patrones de Diseño
- Ventajas y Desventajas de Usar Patrones de Diseño
Módulo 2: Patrones Creacionales
- Introducción a los Patrones Creacionales
- Singleton
- Factory Method
- Abstract Factory
- Builder
- Prototype
- Comparativa y Elección de Patrones Creacionales
Módulo 3: Patrones Estructurales
- Introducción a los Patrones Estructurales
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
- Comparativa y Elección de Patrones Estructurales
Módulo 4: Patrones de Comportamiento
- Introducción a los Patrones de Comportamiento
- Chain of Responsibility
- Command
- Interpreter
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
- Comparativa y Elección de Patrones de Comportamiento
Módulo 5: Aplicación de Patrones de Diseño
- Cómo Seleccionar el Patrón Adecuado
- Ejemplos Prácticos de Uso de Patrones
- Patrones de Diseño en Proyectos Reales
- Refactorización Usando Patrones de Diseño
- Antipatrones: Cuándo los Patrones se Vuelven un Problema
Módulo 6: Patrones de Diseño Avanzados
- Patrones de Diseño en Arquitecturas Modernas
- Patrones de Diseño en Microservicios
- Patrones de Diseño en Sistemas Distribuidos
- Patrones de Concurrencia
- Patrones de Diseño en Desarrollo Ágil
