Cada noche, PideYa genera el informe de cierre diario de cada restaurante: se cargan los pedidos del día, se agregan las cifras, se formatea el resultado y se distribuye. El flujo es siempre el mismo; lo que cambia son pasos concretos: el informe PDF para el dueño formatea y distribuye distinto que el CSV para contabilidad o que el resumen que se envía por email. Strategy nos enseñó a intercambiar algoritmos completos; aquí el algoritmo completo es común y solo varían piezas. Para esto, el GoF reserva su patrón de herencia por excelencia: el esqueleto del algoritmo vive en una clase base, fijado e intocable, y las subclases rellenan los huecos — bajo el principio de Hollywood: "no nos llames; nosotros te llamaremos".
Contenido
- El problema en PideYa: informes que se copian el flujo
- Intención y estructura del patrón
- Implementación Java completa
- Hooks: los pasos opcionales
- El principio de Hollywood
- Ya has visto este patrón (aunque no lo llamamos)
- Template Method vs. Strategy: herencia contra composición
- Cuándo usarlo y cuándo no
- Relación con otros patrones
- Errores comunes
- Ejercicios y conclusión
El problema en PideYa: informes que se copian el flujo
Hoy existen InformePdfCierre e InformeCsvCierre, y son gemelos con copia-y-pega:
public class InformePdfCierre {
public void generar(String restauranteId, LocalDate dia) {
List<Pedido> pedidos = repositorio.pedidosDelDia(restauranteId, dia); // igual
pedidos = pedidos.stream().filter(p -> p.getEstado() == ENTREGADO).toList(); // igual
CifrasCierre cifras = agregar(pedidos); // igual
byte[] pdf = maquetarPdf(cifras); // ← distinto
almacen.guardar(restauranteId, dia, pdf); // ← distinto
}
// agregar(...) duplicado aquí...
}
public class InformeCsvCierre {
public void generar(String restauranteId, LocalDate dia) {
// ...las mismas cuatro primeras líneas, copiadas...
String csv = volcarCsv(cifras); // ← distinto
sftp.subir("contabilidad/" + dia + ".csv", csv); // ← distinto
}
// ...y agregar(...) duplicado también
}El síntoma exacto: dos algoritmos idénticos en estructura que solo difieren en pasos concretos. Las consecuencias del copia-y-pega ya las conoces: cuando operaciones pide "excluir también los pedidos de prueba", hay que acordarse de tocar todas las copias del filtrado (y la tercera copia, el informe por email que viene en camino, nace ya desincronizada). El flujo común no tiene dueño: vive duplicado en cada variante.
La inversión que lo arregla: el flujo común sube a una clase base como método único e intocable, y los pasos variables bajan a las subclases como métodos abstractos. La base llama a las subclases — no al revés.
Intención y estructura del patrón
Intención (GoF): definir el esqueleto de un algoritmo en una operación, delegando algunos pasos a las subclases. Template Method permite que las subclases redefinan ciertos pasos de un algoritmo sin cambiar su estructura.
Este es uno de los dos patrones de comportamiento de ámbito de clase (lo anunciamos en la introducción del módulo): la relación se fija con herencia, en compilación.
classDiagram
class InformeCierre {
<<abstract>>
+generar(restauranteId, dia) «final»
#cargarPedidos(restauranteId, dia) List~Pedido~
#agregar(pedidos) CifrasCierre
#formatear(cifras)* byte[]
#distribuir(restauranteId, dia, contenido)*
#debeGenerarse(cifras) boolean «hook»
}
class InformePdfCierre {
#formatear(cifras) byte[]
#distribuir(restauranteId, dia, contenido)
}
class InformeCsvCierre {
#formatear(cifras) byte[]
#distribuir(restauranteId, dia, contenido)
}
class InformeEmailResumen {
#formatear(cifras) byte[]
#distribuir(restauranteId, dia, contenido)
#debeGenerarse(cifras) boolean
}
InformeCierre <|-- InformePdfCierre
InformeCierre <|-- InformeCsvCierre
InformeCierre <|-- InformeEmailResumen
| Rol GoF | En PideYa |
|---|---|
| AbstractClass (el método plantilla + pasos primitivos) | InformeCierre |
| ConcreteClass (implementa los pasos variables) | InformePdfCierre, InformeCsvCierre, InformeEmailResumen |
Los métodos de la base se clasifican en cuatro tipos, y distinguirlos es media lección:
| Tipo de método | Ejemplo | ¿Las subclases lo tocan? |
|---|---|---|
| Método plantilla (el esqueleto) | generar(...) |
Jamás — decláralo final |
| Paso concreto (común, implementado en la base) | cargarPedidos, agregar |
No normalmente |
| Paso abstracto (obligatorio en cada subclase) | formatear, distribuir |
Deben implementarlo |
| Hook (opcional, con implementación por defecto) | debeGenerarse |
Pueden, si quieren |
Implementación Java completa
public abstract class InformeCierre {
protected final RepositorioPedidos repositorio;
protected InformeCierre(RepositorioPedidos repositorio) {
this.repositorio = repositorio;
}
/** EL MÉTODO PLANTILLA: el algoritmo completo, en un solo lugar, final. */
public final void generar(String restauranteId, LocalDate dia) {
List<Pedido> pedidos = cargarPedidos(restauranteId, dia);
CifrasCierre cifras = agregar(pedidos);
if (!debeGenerarse(cifras)) { // hook: paso opcional
RegistroEventos.INSTANCIA.info("Cierre omitido para " + restauranteId);
return;
}
byte[] contenido = formatear(cifras); // paso abstracto
distribuir(restauranteId, dia, contenido); // paso abstracto
}
/** Paso concreto: común a todos los informes. Una sola copia del filtrado. */
protected List<Pedido> cargarPedidos(String restauranteId, LocalDate dia) {
return repositorio.pedidosDelDia(restauranteId, dia).stream()
.filter(p -> p.getEstado() == EstadoPedido.ENTREGADO)
.toList();
}
/** Paso concreto: la agregación, antes duplicada, ahora con dueño único. */
protected CifrasCierre agregar(List<Pedido> pedidos) {
BigDecimal total = pedidos.stream()
.map(Pedido::getTotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
Map<String, Long> porPlato = pedidos.stream()
.flatMap(p -> p.getLineas().stream())
.collect(groupingBy(LineaPedido::getDescripcion, counting()));
return new CifrasCierre(pedidos.size(), total, porPlato);
}
/** Hook: por defecto, todo informe se genera. Las subclases pueden opinar. */
protected boolean debeGenerarse(CifrasCierre cifras) {
return true;
}
/** Pasos abstractos: cada informe DEBE decir cómo formatea y distribuye. */
protected abstract byte[] formatear(CifrasCierre cifras);
protected abstract void distribuir(String restauranteId, LocalDate dia, byte[] contenido);
}Las subclases quedan reducidas a su diferencia esencial:
public class InformeCsvCierre extends InformeCierre {
private final ClienteSftp sftp;
public InformeCsvCierre(RepositorioPedidos repo, ClienteSftp sftp) {
super(repo);
this.sftp = sftp;
}
@Override
protected byte[] formatear(CifrasCierre cifras) {
StringBuilder csv = new StringBuilder("plato;unidades\n");
cifras.porPlato().forEach((plato, n) -> csv.append(plato).append(';').append(n).append('\n'));
return csv.toString().getBytes(StandardCharsets.UTF_8);
}
@Override
protected void distribuir(String restauranteId, LocalDate dia, byte[] contenido) {
sftp.subir("contabilidad/" + restauranteId + "/" + dia + ".csv", contenido);
}
}Detalles de oficio en la base, todos deliberados:
generaresfinal: el esqueleto es el contrato; si una subclase pudiera redefinirlo, el patrón se evapora (y vuelve el copia-y-pega, ahora con herencia).- Los pasos son
protected: son la interfaz hacia las subclases, no hacia el mundo. Nadie de fuera llama aformatear. - La corrección del filtrado ahora es un solo cambio: "excluir pedidos de prueba" se toca en
cargarPedidosy las tres variantes lo heredan al instante.
Hooks: los pasos opcionales
El hook debeGenerarse merece su sección porque es la herramienta de flexibilidad fina del patrón: un método con implementación por defecto (a menudo vacía o trivial) que las subclases pueden sobrescribir para engancharse al flujo sin estar obligadas. El informe-resumen por email lo usa: si el restaurante no abrió, no hay nada que mandar:
public class InformeEmailResumen extends InformeCierre {
// ...
@Override
protected boolean debeGenerarse(CifrasCierre cifras) {
return cifras.numeroPedidos() > 0; // sin pedidos, sin email
}
}Reglas prácticas para diseñar hooks: pocos y con nombre que delate el momento (debeGenerarse, alTerminarDistribucion); por defecto inofensivos (devolver lo permisivo, no hacer nada); y documentados en la base — son el contrato de extensión. Los frameworks los usan por doquier: los onCreate/onPause de Android o los callbacks de ciclo de vida de Spring son hooks de plantillas gigantes.
El principio de Hollywood
"Don't call us, we'll call you." La inversión de control que define al patrón: el código de bajo nivel (las subclases) no llama al flujo; es el flujo (la base) quien llama a las subclases cuando les toca. Compáralo con una librería clásica: tú llamas a Math.max(...) cuando quieres; en Template Method, InformeCsvCierre nunca decide cuándo formatear — es llamado por generar en el momento justo.
Esta inversión es la semilla de algo que ya usas: los frameworks funcionan así. Spring, JUnit o Android definen el flujo (el método plantilla, a escala industrial) y tu código rellena huecos que el framework invoca. La diferencia entre usar una librería y usar un framework es el principio de Hollywood — y lo aprendiste aquí, en un patrón de una clase y media.
Ya has visto este patrón (aunque no lo llamamos)
Dos reencuentros del propio curso:
- El
manejar(...)del Chain of Responsibility: método público que fija el protocolo (comprobar → cortar o delegar) llamando alcomprobarabstracto de cada eslabón. Lo llamamos entonces "truco de plantilla"; era exactamente este patrón en miniatura. - El flujo común de las
Notificaciondel Bridge admite la misma jugada: unenviar()final en la abstracción que hace validar → componer → enviar por el canal, concomponer()abstracto por tipo de notificación. Template Method dentro del lado izquierdo de un Bridge: los patrones anidan.
Y en el JDK: AbstractList te da add, contains o indexOf gratis si implementas get y size (plantillas sobre primitivas), y los tests con @BeforeEach/@Test/@AfterEach de JUnit son un método plantilla que tú no ves y que llama a tus métodos.
Template Method vs. Strategy: herencia contra composición
El careo prometido en la lección anterior — mismo problema (variar el "cómo" sin tocar a los clientes), soluciones en las dos escuelas:
| Criterio | Template Method | Strategy |
|---|---|---|
| Mecanismo | Herencia (ámbito de clase) | Composición (ámbito de objeto) |
| Qué varía | Pasos de un algoritmo cuyo esqueleto es fijo | El algoritmo entero |
| Cuándo se decide la variante | Al compilar/instanciar la subclase | En ejecución; intercambiable en caliente |
| ¿Combinar variantes de varios ejes? | Mal: una subclase por combinación | Bien: inyecta una estrategia por eje |
| Acceso al estado común | Directo (campos protected de la base) |
Solo lo que la interfaz pasa como parámetros |
| Riesgo característico | Jerarquías rígidas, acoplamiento base-subclase | Más piezas y cableado de inyección |
| En PideYa | InformeCierre |
CalculoEnvio, EstrategiaAsignacion |
La guía de decisión, honesta: si los pasos variables necesitan compartir mucho contexto del flujo y las variantes son pocas y estables, Template Method es directo y conciso. Si necesitas cambiar la variante en ejecución, combinar ejes de variación o testear las variantes aisladas del flujo, Strategy — de hecho, la evolución típica es empezar con Template Method y, cuando las subclases se multiplican por combinatoria, extraer cada paso variable como estrategia inyectada ("composición sobre herencia" tiene su historia desde el módulo 1, y esta es su batalla clásica). Ambos conviven bien: un método plantilla cuyos pasos delegan en estrategias.
Cuándo usarlo y cuándo no
Úsalo cuando:
- Varias clases implementan el mismo algoritmo con pasos distintos y el copia-y-pega ya duele (o va a doler: tercera variante a la vista).
- Quieres fijar el orden del flujo como contrato inviolable y ofrecer puntos de extensión controlados (pasos y hooks).
- Estás construyendo una base reutilizable o mini-framework interno donde otros equipos rellenan huecos.
Evítalo cuando:
- Las variantes necesitan cambiar en ejecución o combinarse: la herencia lo fija todo al construir — Strategy.
- La "parte común" es pequeña o forzada: una base artificial para compartir dos líneas acopla jerarquías enteras por nada; a veces un método estático compartido es la respuesta aburrida y correcta.
- Ya hay una jerarquía por otro motivo: la herencia es un recurso único en Java; gastarla en el flujo de informes impide usarla para otra cosa.
Relación con otros patrones
- Strategy: la tabla de arriba; pasos de plantilla extraíbles como estrategias.
- Factory Method: GoF lo define literalmente como "una especialización de Template Method" donde el paso variable es crear un objeto — nuestro viejo conocido del módulo 2, reencuadrado.
- Chain of Responsibility y Bridge: los reencuentros de la sección anterior.
- Builder: el director de un builder ejecuta pasos de construcción en orden fijo — espíritu de plantilla en el mundo creacional.
Errores comunes
- Método plantilla no-final: una subclase lo redefine "solo para este caso" y el esqueleto único se fragmenta.
finalno es paranoia; es el patrón. - Demasiados pasos abstractos: si la base delega todo, no hay plantilla — solo una interfaz cara. La base debe poseer el flujo y la mayor parte del trabajo común.
- Hooks que las subclases están obligadas a llamar (
super.alTerminar()"o se rompe todo"): el contrato frágil clásico de la herencia. Diseña la base para que lo obligatorio viva en la plantilla, no en la buena memoria del que hereda. - Subclases que se saltan el flujo llamando a los pasos directamente (
formateardesde fuera degenerar): visibilidadprotectedy disciplina; los pasos no son API pública. - Estado mutable
protectedcompartido a manos llenas: las subclases acaban dependiendo de las tripas de la base y cada refactor de la base rompe hijos. Pasa datos como parámetros de los pasos (como nuestroCifrasCierre) siempre que puedas.
Ejercicios
Ejercicio 1: nueva variante en diez líneas
Implementa InformeJsonCierre: formatea las cifras como JSON (usa el formato que quieras) y las publica en almacenDatalake.publicar("cierres/" + dia + "/" + restauranteId + ".json", contenido). ¿Cuántas líneas de flujo (cargar/filtrar/agregar) has escrito?
Ejercicio 2: el hook de después
Operaciones quiere que, tras distribuir con éxito, cada informe pueda opcionalmente registrar métricas (el PDF no; el CSV debe anotar metricas.registrarEnvioContabilidad(dia)). Añade el hook a la base (nombre, firma, implementación por defecto, punto de llamada en generar) y úsalo en InformeCsvCierre.
Ejercicio 3: ¿plantilla o estrategia?
El equipo de notificaciones quiere unificar el flujo validar destinatario → componer mensaje → enviar → registrar: (a) si las variantes son "confirmación", "retraso" y "promoción", que difieren solo en componer, ¿qué usarías?; (b) ¿y si además el envío debe poder cambiar en ejecución entre push/SMS/email según preferencias del usuario? Relaciona tu respuesta con lo construido en el Bridge del módulo 3.
Soluciones
Solución 1:
public class InformeJsonCierre extends InformeCierre {
private final AlmacenDatalake almacenDatalake;
public InformeJsonCierre(RepositorioPedidos repo, AlmacenDatalake almacen) {
super(repo);
this.almacenDatalake = almacen;
}
@Override
protected byte[] formatear(CifrasCierre cifras) {
String json = "{\"pedidos\":" + cifras.numeroPedidos()
+ ",\"total\":" + cifras.total() + "}";
return json.getBytes(StandardCharsets.UTF_8);
}
@Override
protected void distribuir(String restauranteId, LocalDate dia, byte[] contenido) {
almacenDatalake.publicar("cierres/" + dia + "/" + restauranteId + ".json", contenido);
}
}Líneas de flujo escritas: cero — cargar, filtrar y agregar vienen heredados y con sus correcciones futuras incluidas. Ese es el dividendo del patrón.
Solución 2: en la base:
/** Hook: llamado tras una distribución completada. Por defecto, nada. */
protected void alDistribuirConExito(String restauranteId, LocalDate dia, CifrasCierre cifras) {
}y en el método plantilla, la última línea de generar pasa a ser:
distribuir(restauranteId, dia, contenido);
alDistribuirConExito(restauranteId, dia, cifras); // el flujo llama al hook (Hollywood)En InformeCsvCierre:
@Override
protected void alDistribuirConExito(String restauranteId, LocalDate dia, CifrasCierre cifras) {
metricas.registrarEnvioContabilidad(dia);
}El PDF no toca nada: los hooks no obligan. (Y nota el orden: el hook va después de distribuir en la plantilla, no dentro de cada distribuir — el momento de llamada es propiedad del esqueleto.)
Solución 3: (a) Template Method: esqueleto fijo, un solo paso variable (componer), variantes estables — plantilla con componer() abstracto, exactamente la jugada esbozada en la sección de reencuentros; (b) el eje del canal debe cambiar en ejecución y por usuario: eso no puede fijarlo la herencia — y es justo lo que el Bridge ya resolvió: la abstracción Notificacion (donde puede vivir el método plantilla con componer variable por herencia) delega el envío en un CanalEnvio compuesto e intercambiable. Es decir: la respuesta correcta es ambos, cada uno en su eje — plantilla para el flujo y sus pasos por tipo, composición para el canal. Los ejes de variación independientes piden composición; los pasos de un flujo fijo toleran herencia.
Conclusión
Template Method pone el flujo común bajo un dueño único: InformeCierre.generar(...) fija el esqueleto — cargar, agregar, ¿generar?, formatear, distribuir — como método final, la lógica compartida vive una sola vez, y cada variante (PDF, CSV, email, y el JSON del ejercicio) aporta solo su diferencia, invocada por la base cuando toca: Hollywood en estado puro, la misma inversión de control con la que te hablan los frameworks que usas a diario. Y quedó trazada la tercera frontera de Strategy: pasos de un flujo fijo → herencia y plantilla; algoritmos completos, combinables o cambiantes en caliente → composición y estrategias.
Queda un último patrón en el catálogo de comportamiento, y ataca el problema más retorcido de todos: añadir operaciones nuevas a una jerarquía estable sin tocarla. La carta Composite lleva dos módulos acumulando pretendientes — exportar a JSON, calcular alérgenos, auditar precios — y meter cada operación dentro de Plato y SeccionCarta las convertiría en cajones de sastre. La solución exige el truco técnico más elegante del GoF: el double dispatch. Nos vemos en Visitor.
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
