Cerramos el módulo anterior con una constatación: el catálogo GoF se escribió para objetos que conviven dentro de un mismo proceso, pero el software actual se organiza a una escala mayor. Antes de repartir PideYa por la red (eso llegará en las dos próximas lecciones), necesitamos subir un nivel de zoom dentro del propio monolito: cómo se organizan los objetos en capas, cómo se conecta el dominio con el mundo exterior y cómo se estructura el acceso a datos. Verás que los patrones arquitectónicos no sustituyen al GoF: lo contienen. Cada patrón de esta lección es, en el fondo, una intención GoF (o un principio SOLID) aplicada a la estructura global de la aplicación.
Contenido
- Arquitectura en capas: dónde vive cada patrón GoF de PideYa
- MVC y sus variantes (MVP, MVVM)
- Arquitectura hexagonal: puertos y adaptadores
- Inyección de dependencias y contenedores IoC
- Repository y Unit of Work
- CQRS y Event Sourcing: introducción conceptual
Arquitectura en capas
El patrón arquitectónico más veterano (ya aparece en POSA, que conocimos en la lección 01-02) consiste en organizar el código en capas horizontales, donde cada capa solo conoce a la inferior:
| Capa | Responsabilidad | Patrones GoF de PideYa que viven en ella |
|---|---|---|
| Presentación | UI, controladores web, API REST | Command (panel con undo), Observer (refrescar la vista del pedido) |
| Aplicación | Casos de uso, orquestación | Facade (FachadaCheckout), Mediator (CentralReparto), Template Method (informes) |
| Dominio | Reglas de negocio, entidades | State (EstadoPedido), Strategy (envío), Builder (Pedido.Builder), Composite (carta), Chain (validación) |
| Infraestructura | BD, red, servicios externos | Adapter (AdaptadorPayPal), Proxy (caché), Abstract Factory (FabricaEspana/FabricaMexico) |
Fíjate en que la tabla no es una clasificación nueva: es el mapa de todo lo que construimos en los módulos 2-4, colocado en su sitio. La regla de oro es la dirección de las dependencias: el dominio no debe importar nada de infraestructura. Si Pedido conociera a PasarelaRedsys, un cambio en la pasarela obligaría a recompilar el corazón del negocio.
flowchart TD
P[Presentación] --> A[Aplicación]
A --> D[Dominio]
A --> I[Infraestructura]
I -.implementa interfaces de.-> D
La flecha punteada es la clave y es pura inversión de dependencias (DIP), que vimos en 01-03: la infraestructura implementa interfaces definidas por el dominio, no al revés.
MVC y sus variantes
Dentro de la capa de presentación, el patrón dominante es MVC (Modelo-Vista-Controlador):
- Modelo: el estado y las reglas (nuestro dominio:
Pedido,EstadoPedido...). - Vista: lo que ve el usuario (plantillas HTML, pantalla de la app del repartidor).
- Controlador: recibe la acción del usuario, invoca al modelo y elige la vista.
MVC es, esencialmente, Observer + Strategy + Composite trabajando juntos: la vista observa al modelo, el controlador es una estrategia de manejo de entrada y las vistas se componen de subvistas. No es casualidad: el GoF cita MVC como ejemplo en su introducción.
Sus variantes ajustan quién conoce a quién — las mencionamos sin desarrollarlas:
| Variante | Diferencia clave | Dónde se ve |
|---|---|---|
| MVP | El Presenter habla con la vista a través de una interfaz; la vista es pasiva | UIs de escritorio, Android clásico |
| MVVM | El ViewModel expone estado observable y la vista se enlaza por data binding | Frontends reactivos, Jetpack Compose, frameworks JS |
Arquitectura hexagonal: puertos y adaptadores
La arquitectura hexagonal (Alistair Cockburn) radicaliza la idea de las capas: el dominio en el centro, y todo lo demás (web, BD, pasarelas, colas) conectado mediante puertos y adaptadores.
- Un puerto es una interfaz definida por el dominio: es DIP hecho arquitectura.
- Un adaptador es una implementación concreta de ese puerto: es, literalmente, el patrón Adapter de 03-02 elevado a pieza arquitectónica.
En PideYa ya teníamos el ejemplo perfecto sin saberlo:
// PUERTO (vive en el dominio): el negocio define QUÉ necesita
public interface PasarelaPago {
ResultadoCobro cobrar(Pedido pedido, DatosTarjeta tarjeta);
}
// ADAPTADORES (viven en infraestructura): CÓMO se consigue
public class AdaptadorRedsys implements PasarelaPago { /* API de Redsys */ }
public class AdaptadorPayPal implements PasarelaPago { /* el de 03-02 */ }
public class PasarelaEnMemoria implements PasarelaPago { /* para tests */ }El dominio de PideYa cobra pedidos sin saber si detrás hay Redsys, PayPal o un doble de pruebas. Se distinguen dos tipos de puertos:
- Puertos primarios (driving): por donde entran las peticiones (la interfaz que implementa
FachadaCheckout— nuestra Facade era ya un puerto primario avant la lettre). - Puertos secundarios (driven): lo que el dominio necesita del exterior (
PasarelaPago,RepositorioPedidos,Notificador).
Inyección de dependencias y contenedores IoC
Si el dominio solo conoce interfaces, alguien tiene que decidir qué implementación concreta se usa y pasarla. Eso es inyección de dependencias (DI): en lugar de que FachadaCheckout haga new AdaptadorRedsys(), recibe un PasarelaPago por constructor.
public class FachadaCheckout {
private final PasarelaPago pasarela;
private final Notificador notificador;
// Las dependencias se INYECTAN: la fachada no sabe cuáles son
public FachadaCheckout(PasarelaPago pasarela, Notificador notificador) {
this.pasarela = pasarela;
this.notificador = notificador;
}
}Un contenedor IoC (Spring, Guice, CDI) industrializa esto: escanea las clases, decide qué implementación satisface cada interfaz y construye el grafo de objetos completo. Visto desde el catálogo:
- El contenedor es una Abstract Factory generalizada: la
FabricaEspanade 02-04 creaba familias coherentes a mano; Spring lo hace por configuración. - Los beans singleton de Spring resuelven la intención del Singleton de 02-02 sin sus inconvenientes: una sola instancia, pero testeable e inyectable — la crítica que hicimos allí encuentra aquí su respuesta.
Repository y Unit of Work
Bajamos ahora a la frontera con la base de datos, con dos patrones del catálogo PoEAA de Fowler (presentado en 01-02).
Repository ofrece al dominio una colección aparente de objetos, ocultando el SQL:
// Puerto secundario: el dominio habla de pedidos, no de tablas
public interface RepositorioPedidos {
Optional<Pedido> buscarPorId(String id);
List<Pedido> pendientesDeReparto(Zona zona);
void guardar(Pedido pedido);
}La implementación (RepositorioPedidosJpa) vive en infraestructura. El repositorio combina intenciones conocidas: es un Adapter sobre la persistencia, a menudo devuelve resultados vía Iterator, y sus consultas complejas pueden construirse con un Builder (así funciona la Criteria API de JPA, como vimos en 05-03).
Unit of Work registra todos los objetos tocados durante una transacción de negocio y los persiste de una vez al confirmar. Si el checkout modifica el pedido, el stock y los puntos de fidelización, la Unit of Work garantiza que se escriban juntos o ninguno. En Hibernate ya lo usas sin saberlo: la Session/EntityManager es una Unit of Work que hace dirty checking de las entidades cargadas.
CQRS y Event Sourcing
Terminamos con dos patrones que preparan el terreno para los módulos siguientes. Solo la idea; su despliegue real es asunto de sistemas distribuidos.
CQRS (Command Query Responsibility Segregation) separa el modelo de escritura (comandos: RealizarPedido, CancelarPedido) del modelo de lectura (consultas: "pantalla de seguimiento", "listado de la cocina"). En PideYa lo motivó un dato real: por cada escritura de un pedido hay cientos de lecturas de su estado. Con CQRS, las lecturas atacan vistas desnormalizadas y baratas, y las escrituras pasan por todo el dominio (validaciones Chain, EstadoPedido...). ¿Te suena la mitad "command"? Es la intención del patrón Command de 04-03: peticiones cosificadas como objetos.
Event Sourcing va un paso más allá: en lugar de guardar el estado actual del pedido, guarda la secuencia de eventos que lo produjo:
El estado actual se reconstruye reproduciendo los eventos. Reconoce las intenciones GoF:
- Como Memento (04-07), captura el pasado para poder volver a él — pero guardando deltas (eventos) en vez de fotos completas.
- Como Observer (04-08), cada evento persistido puede notificar a otros interesados: las vistas de lectura de CQRS se actualizan escuchando esos eventos.
Esa pareja CQRS + Event Sourcing es la puerta natural a las arquitecturas orientadas a eventos que veremos en 06-03.
Errores Comunes y Consejos
- Capas de mentira: tener paquetes
controller/service/repositorypero con el dominio importando clases de infraestructura. Las capas se miden por las dependencias, no por los nombres de paquete. Consejo: usa ArchUnit para verificar en un test quedominiono importainfraestructura. - Hexagonal ceremonial: crear puerto + adaptador para todo, incluida lógica que jamás tendrá segunda implementación. Es la patternitis de 05-05 a escala arquitectónica. Crea el puerto cuando haya una frontera real (algo externo o algo que necesitas sustituir en tests).
- Repository que filtra la tecnología: métodos como
ejecutarHql(String)en la interfaz del repositorio destruyen su propósito. Si el dominio ve HQL, no hay abstracción. - Adoptar CQRS/Event Sourcing por moda: son patrones caros (dos modelos, consistencia diferida, versionado de eventos). Aplica el "espera hasta que duela" de 05-01: si un solo modelo CRUD te sirve, quédatelo.
- Confundir DI con tener un framework: la inyección de dependencias es pasar colaboradores por constructor; puedes (y debes poder) hacerla a mano en un
main. El contenedor solo automatiza el ensamblaje.
Ejercicios
- Clasifica en capas. Coloca cada clase de PideYa en su capa (presentación, aplicación, dominio o infraestructura) y justifica:
EstadoPedido,AdaptadorPayPal,FachadaCheckout,ControladorPedidosRest,RepositorioPedidosJpa,Pedido.Builder. - Diseña un puerto. PideYa necesita calcular tiempos estimados de entrega consultando un servicio externo de tráfico. Define el puerto (interfaz de dominio), un adaptador real y un adaptador falso para tests. Indica si es un puerto primario o secundario.
- Eventos de un pedido. Escribe la secuencia de eventos (Event Sourcing) que dejaría un pedido que se crea con dos platos, se confirma, se cobra, y al que el cliente cambia la dirección antes del reparto. ¿Cómo reconstruirías la dirección actual del pedido?
Soluciones
EstadoPedido→ dominio (reglas del ciclo de vida).AdaptadorPayPal→ infraestructura (implementa el puertoPasarelaPagocontra una API externa).FachadaCheckout→ aplicación (orquesta un caso de uso completo).ControladorPedidosRest→ presentación (traduce HTTP a llamadas de aplicación).RepositorioPedidosJpa→ infraestructura (implementa el puertoRepositorioPedidoscon JPA).Pedido.Builder→ dominio (construcción del agregado con sus invariantes).- Puerto secundario (el dominio necesita algo del exterior):
public interface EstimadorEntrega { Duration estimar(Direccion origen, Direccion destino); }. Adaptador real:EstimadorGoogleMaps implements EstimadorEntrega, que llama a la API y traduce su respuesta. Adaptador de test:EstimadorFijo implements EstimadorEntregaque devuelve siempreDuration.ofMinutes(30). El dominio calcula promesas de entrega sin tocar red. PedidoCreado(id, cliente)→LineaAnadida(plato1)→LineaAnadida(plato2)→PedidoConfirmado(direccionInicial)→PedidoCobrado(importe)→DireccionCambiada(nuevaDireccion). La dirección actual se obtiene reproduciendo los eventos en orden: la última escritura relevante (DireccionCambiada) gana. Nótese el bonus: el historial conserva que hubo un cambio de dirección, información que un modelo de estado actual habría perdido.
Conclusión
Hemos subido el zoom y descubierto que la arquitectura de PideYa está tejida con los mismos hilos del catálogo: las capas ordenan dónde vive cada patrón GoF, la hexagonal convierte DIP y Adapter en el plano del edificio, los contenedores IoC industrializan las fábricas, Repository y Unit of Work disciplinan la persistencia, y CQRS/Event Sourcing reinterpretan Command, Memento y Observer sobre el flujo de datos. Todo esto, todavía, dentro de un solo proceso desplegable. Pero PideYa sigue creciendo: el equipo de reparto quiere desplegar sin esperar al de pagos, y el monolito empieza a ser el cuello de botella. Toca partirlo — con sus propios patrones y sus propios peligros — en Patrones de Diseño en Microservicios.
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
