El módulo anterior terminó con el nacimiento de los objetos resuelto: fábricas, builders y prototipos deciden qué se instancia, cómo se monta y de dónde se parte. Pero un sistema no es una colección de objetos bien nacidos: es una red de objetos conectados. Y en PideYa esa red ya empieza a tensarse: un SDK de pagos externo cuya interfaz no encaja con nuestra PasarelaPago, una carta de restaurante que es un árbol de secciones dentro de secciones, extras de platos que multiplican subclases, un checkout que obliga a cada pantalla a conocer cinco subsistemas... Ninguno de estos problemas va de crear objetos: van de componerlos. Esa es la segunda familia del catálogo GoF, y esta lección es su mapa.
Contenido
- El problema común: componer sin acoplar
- Panorama de los siete patrones estructurales
- Ámbito de clase y ámbito de objeto
- Síntomas en PideYa de que hace falta un estructural
- Cómo leeremos cada patrón en este módulo
- Ejercicios y conclusión
El problema común: componer sin acoplar
Los patrones creacionales respondían a una pregunta: ¿quién decide qué clase concreta se instancia? Los estructurales responden a otra: ¿cómo se ensamblan clases y objetos en estructuras mayores sin que la estructura se vuelva rígida?
La palabra clave es estructura: relaciones entre piezas. Y el enemigo es el mismo de siempre, con otra cara: el acoplamiento. En el módulo 2 el acoplamiento se colaba por el new; aquí se cuela por las conexiones:
- Una clase que depende de la interfaz exacta de otra que no controlamos (una API de terceros): cuando el tercero cambia, o cuando queremos cambiar de tercero, todo se rompe.
- Una jerarquía de herencia que crece multiplicativamente: cada dimensión de variación nueva duplica las subclases.
- Un cliente que para hacer una operación debe conocer y coordinar media docena de objetos: el conocimiento de la estructura interna se esparce por todo el sistema.
- Estructuras recursivas (árboles parte-todo) tratadas con
if (esGrupo) ... else ...por todas partes. - Objetos caros (memoria, red, carga) creados y conectados sin control, porque nadie media en el acceso.
Los siete patrones estructurales son siete maneras de organizar las conexiones para que sigan cumpliendo lo que aprendimos en la lección de principios: programar contra interfaces, preferir composición sobre herencia, y poder extender sin modificar. De hecho, verás que casi todos los estructurales son aplicaciones sistemáticas de "composición sobre herencia": un objeto que envuelve, contiene o remite a otros, en lugar de una jerarquía que lo hereda todo.
Panorama de los siete patrones estructurales
La foto completa del módulo, cada patrón en una línea. No intentes memorizarla ahora: volveremos a ella, ampliada, en la comparativa final.
| Patrón | Intención en una línea |
|---|---|
| Adapter | Convertir la interfaz de una clase existente en la interfaz que el cliente espera |
| Bridge | Separar una abstracción de su implementación para que ambas evolucionen por separado |
| Composite | Componer objetos en árboles parte-todo y tratar hojas y grupos de manera uniforme |
| Decorator | Añadir responsabilidades a un objeto dinámicamente, envolviéndolo, sin herencia |
| Facade | Ofrecer una interfaz simple y unificada ante un subsistema complejo |
| Flyweight | Compartir el estado común de muchísimos objetos pequeños para ahorrar memoria |
| Proxy | Poner un sustituto con la misma interfaz que controla el acceso al objeto real |
Una observación que conviene hacer ya, porque evita la mayor confusión del módulo: cuatro de los siete (Adapter, Decorator, Facade y Proxy) comparten mecánica —un objeto delante de otro, delegando— y se distinguen solo por la intención. Adapter cambia la interfaz; Decorator la conserva y añade responsabilidades; Facade la simplifica; Proxy la conserva y controla el acceso. Recuerda la definición de patrón de la primera lección: contexto, problema, solución e intención. En esta familia, la intención es a menudo lo único que separa un patrón de otro; les dedicaremos un careo completo en la comparativa.
Los otros tres organizan multitudes: Composite organiza objetos en árboles, Bridge organiza jerarquías en dos ejes independientes, y Flyweight organiza miles de instancias compartiendo lo común.
Ámbito de clase y ámbito de objeto
En la clasificación GoF vimos que cada patrón tiene un ámbito: de clase si la relación se fija con herencia en tiempo de compilación, o de objeto si se establece con composición en tiempo de ejecución.
En la familia estructural la cuenta es rotunda: seis de los siete son de ámbito de objeto. Solo Adapter existe en las dos variantes, y por eso es el ejemplo perfecto para entender la diferencia:
- Adapter de clase: el adaptador hereda de la clase que adapta y a la vez implementa la interfaz esperada. La relación queda soldada al compilar: ese adaptador sirve para esa clase concreta y sus subclases no.
- Adapter de objeto: el adaptador contiene una referencia al objeto adaptado y delega en él. La relación se decide al construir: el mismo adaptador puede envolver cualquier objeto compatible, incluso decidido en ejecución.
classDiagram
class InterfazEsperada { <<interface>> +operacion() }
class ClaseExistente { +operacionVieja() }
class AdaptadorDeClase { +operacion() }
class AdaptadorDeObjeto { -adaptado: ClaseExistente +operacion() }
InterfazEsperada <|.. AdaptadorDeClase
ClaseExistente <|-- AdaptadorDeClase : hereda (ámbito de clase)
InterfazEsperada <|.. AdaptadorDeObjeto
AdaptadorDeObjeto o-- ClaseExistente : contiene (ámbito de objeto)
Que casi toda la familia sea de ámbito de objeto no es casualidad: es el principio de composición sobre herencia hecho catálogo. La herencia fija la estructura para siempre; la composición permite reconfigurarla —envolver, desenvolver, recombinar— mientras el programa corre. Verás esta idea repetida en cada lección: el decorador se apila en ejecución, el árbol del composite se monta en ejecución, el puente se cruza en ejecución.
Síntomas en PideYa de que hace falta un estructural
Igual que el módulo 2 empezó catalogando los dolores del new, este empieza catalogando los dolores de la composición. Todos son reales en PideYa hoy; cada uno tiene su lección:
| Síntoma en PideYa | Olor de diseño | Patrón que lo trata |
|---|---|---|
Queremos cobrar con un SDK de PayPal heredado, pero su interfaz (makePayment(long, String)) no se parece en nada a nuestra PasarelaPago del módulo 2 |
Interfaces incompatibles entre nuestro código y código que no podemos tocar | Adapter |
| Notificaciones de confirmación, retraso y promoción × canales push, SMS y email: 3×3 = 9 subclases, y cada tipo o canal nuevo multiplica | Jerarquía que crece por producto de dos dimensiones independientes | Bridge |
La carta de un restaurante tiene secciones, subsecciones y platos; el código está lleno de if (esSeccion) ... else ... |
Estructura árbol parte-todo tratada con condicionales en vez de recursión uniforme | Composite |
"Doble queso", "sin gluten", "ración grande": cada combinación de extras sobre un plato pide su subclase (PizzaConQuesoSinGluten...) |
Explosión combinatoria de subclases para añadir responsabilidades | Decorator |
| Para confirmar un pedido, la app móvil llama a validación, impuestos, pasarela, persistencia y notificaciones, en el orden correcto, con el manejo de errores correcto | Clientes obligados a conocer y orquestar un subsistema entero | Facade |
| El mapa en tiempo real muestra miles de marcadores de repartidores y restaurantes, y cada uno carga su propio icono: la app consume memoria como si no hubiera mañana | Miles de objetos que duplican el mismo estado inmutable | Flyweight |
| Las fotos de los platos pesan varios MB y se cargan todas al abrir la carta; y cualquier empleado puede invocar operaciones que deberían ser solo de administradores | Acceso al objeto real sin control: sin carga perezosa, sin permisos | Proxy |
Fíjate en el criterio, que es el mismo de todo el curso: primero el síntoma, después el patrón. Ninguna de las lecciones siguientes empieza por "apliquemos X"; todas empiezan por un dolor concreto de PideYa y llegan al patrón como respuesta proporcionada. Si en tu proyecto no reconoces el síntoma, la lección de la balanza ya te dijo qué hacer: nada.
Cómo leeremos cada patrón en este módulo
Las siete lecciones de patrón siguen el mismo esqueleto que las del módulo 2, para que puedas compararlos entre sí sin esfuerzo:
- El problema en PideYa: el síntoma de la tabla anterior, con código del primer intento (el malo).
- Estructura del patrón: intención GoF y
classDiagrammermaid con los roles del patrón, como aprendimos en la lección de UML. - Implementación Java completa, explicada paso a paso.
- Variantes relevantes (de clase/objeto, transparente/segura, estática/dinámica según el patrón).
- Cuándo usarlo y cuándo no, con la honestidad de costes habitual.
- Relación con otros patrones: solo menciones con enlace; cada cosa se desarrolla en su lección.
- Errores comunes, ejercicios con solución y conclusión que enlaza con la lección siguiente.
Y seguimos construyendo sobre lo construido: la PasarelaPago y la familia por mercado del módulo 2 reaparecerán en Adapter y Facade; los Notificador del Factory Method serán decorados y puenteados; el Pedido.Builder participará en el checkout de la fachada. Los patrones no viven en lecciones estancas: viven en el mismo sistema.
Ejercicios
Ejercicio 1: clasificar síntomas
Para cada situación nueva de PideYa, di qué patrón estructural de la tabla parece apuntar (no hace falta que sepas aún cómo funciona; basta con casar síntoma e intención de una línea):
- El equipo de datos quiere consultar pedidos con una librería de informes cuya interfaz de lectura no coincide con nuestro repositorio de pedidos.
- Un "menú del día" contiene primero, segundo y postre; y un "menú familiar" contiene dos menús del día y una bebida grande. Queremos calcular el precio de cualquier cosa que se pueda pedir, sea plato suelto o menú anidado.
- Queremos que ciertas llamadas al servicio de reparto queden registradas en
RegistroEventosy se reintenten si fallan, sin tocar la clase del servicio. - La pantalla "estado de mi pedido" necesita datos de cocina, de reparto y de pagos; hoy la app llama a los tres servicios y combina los resultados a mano.
Ejercicio 2: ¿clase u objeto?
Explica con tus palabras por qué un Adapter de objeto puede adaptar en ejecución "cualquier pasarela vieja que nos llegue" y un Adapter de clase no. ¿Qué relación UML de la lección 01-04 usa cada uno?
Soluciones
Solución 1:
- Adapter: dos interfaces que no encajan y una de ellas (la librería) no es nuestra.
- Composite: estructura parte-todo recursiva (menús que contienen menús) con una operación uniforme (precio).
- Decorator (con matiz de Proxy que afinaremos en sus lecciones): añadir responsabilidades —logging, reintentos— envolviendo, sin modificar la clase.
- Facade: un punto único y simple que orquesta varios subsistemas para un caso de uso.
Solución 2: el Adapter de objeto contiene (composición/agregación, la relación o--) una referencia tipada a lo adaptado, que se le pasa al construir: en ejecución puede recibir una instancia u otra, incluso de subclases distintas. El Adapter de clase hereda (generalización, <|--) de una clase concreta: la relación queda fijada al compilar y solo vale para esa clase. Es exactamente la diferencia entre ámbito de objeto y ámbito de clase, y la razón de que "composición sobre herencia" sea el lema de esta familia.
Conclusión
Ya tienes el mapa del módulo: siete patrones que resuelven un mismo problema de fondo —componer clases y objetos en estructuras mayores manteniéndolas flexibles— atacándolo por siete frentes: interfaces que no encajan, jerarquías que se multiplican, árboles parte-todo, responsabilidades añadidas, subsistemas complejos, multitudes de objetos y accesos que controlar. Sabes que casi todos son de ámbito de objeto (composición sobre herencia llevado a catálogo), y que cuatro de ellos comparten mecánica y se distinguen por la intención, así que la intención será nuestra brújula.
Empezamos por el más inmediato de todos, el que aparece siempre que el mundo exterior llama a nuestra puerta con una interfaz que no es la nuestra: PideYa necesita cobrar con un SDK heredado que no habla PasarelaPago, y no podemos tocar ni el SDK ni todo el código que ya usa PasarelaPago. La pieza no encaja... salvo que le construyamos un enchufe. Nos vemos en Adapter.
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
