Patrón Template Method
Define el esqueleto de un algoritmo en la clase base y deja que las subclases rellenen los pasos. Patrón GoF para eliminar lógica duplicada.
Resumen
Tienes tres importadores — CSV, JSON, XML — y cada uno ejecuta el mismo pipeline: fetch, parse, validate, save, notify. La secuencia nunca cambia, solo cambian los pasos. Entonces alguien corrige un bug de validación en CSVImporter y olvida los otros dos, y ahora el mismo algoritmo tiene tres implementaciones ligeramente distintas que auditar.
El Patrón Template Method — uno de los 11 patrones de comportamiento del libro Design Patterns de la Gang of Four (Gamma et al., 1994) — mueve el esqueleto del algoritmo a una clase base. La clase base controla la secuencia con un único método final; las subclases sobrescriben solo los pasos que realmente varían. Corrige validate una vez y todos los importadores reciben la corrección.
Cuándo Usarlo
Usa el Patrón Template Method cuando:
- Varias clases ejecutan el mismo algoritmo con pequeñas variaciones en pasos individuales — parsers de datos, generadores de reportes, pipelines ETL, bucles de juego.
- Quieres que la secuencia de operaciones quede garantizada, no solo sugerida. Un método template
finalhace imposible romper el orden desde una subclase. - Algunos pasos tienen valores por defecto razonables y otros deben venir de cada implementación — el patrón te da tres tipos de miembros para expresar exactamente eso.
No lo uses cuando:
- Solo varía un paso y bastaría un parámetro o un callback. Un argumento de función es más ligero que una relación de herencia.
- Los pasos necesitan variar de forma independiente en tiempo de ejecución — eso es composición, y Strategy es la herramienta correcta.
- Ya tienes una jerarquía profunda. Añadir Template Method encima de tres niveles de herencia convierte la pregunta “¿qué clase implementa este paso?” en algo doloroso de responder.
Problema
Esto es lo que el patrón elimina: la misma orquestación duplicada en cada clase concreta.
class CSVImporter:
def import_data(self, source):
raw = self.fetch(source)
parsed = self.parse(raw)
if "id" not in parsed:
raise ValueError("Missing required field: id")
print(f"Saving: {parsed}")
class JSONImporter:
def import_data(self, source):
raw = self.fetch(source) # idéntico
parsed = self.parse(raw)
if "id" not in parsed: # idéntico
raise ValueError("Missing required field: id")
print(f"Saving: {parsed}") # idéntico
print("JSON import completed!") # la única diferencia real
Cada línea duplicada es un punto donde las copias pueden divergir. Añade un flag dry_run o un paso de reintento y tocas N clases; cambia la regla de validación y tienes que verificar que todas quedaron sincronizadas.
Solución
Sube el esqueleto a la base. La clase base define import_data una sola vez y declara qué pasos deben aportar las subclases, cuáles pueden sobrescribir y cuáles son hooks opcionales.
Python
from abc import ABC, abstractmethod
class DataImporter(ABC):
def import_data(self, source: str):
"""El método template que define el esqueleto del algoritmo."""
raw = self._fetch(source)
parsed = self._parse(raw)
validated = self._validate(parsed)
self._save(validated)
self._on_complete()
@abstractmethod
def _fetch(self, source: str) -> str:
pass
@abstractmethod
def _parse(self, raw: str) -> dict:
pass
def _validate(self, data: dict) -> dict:
"""Paso por defecto; puede ser sobrescrito."""
if "id" not in data:
raise ValueError("Missing required field: id")
return data
def _save(self, data: dict):
"""Paso por defecto; puede ser sobrescrito."""
print(f"Saving: {data}")
def _on_complete(self):
"""Método hook — las subclases pueden sobrescribir o ignorar."""
pass
class CSVImporter(DataImporter):
def _fetch(self, source: str) -> str:
return f"CSV content from {source}"
def _parse(self, raw: str) -> dict:
return {"id": 1, "format": "csv", "content": raw}
class JSONImporter(DataImporter):
def _fetch(self, source: str) -> str:
return f"JSON content from {source}"
def _parse(self, raw: str) -> dict:
return {"id": 2, "format": "json", "content": raw}
def _on_complete(self):
print("JSON import completed!")
# Uso
CSVImporter().import_data("users.csv")
JSONImporter().import_data("users.json")
JavaScript
class DataImporter {
importData(source) {
const raw = this.fetch(source);
const parsed = this.parse(raw);
const validated = this.validate(parsed);
this.save(validated);
this.onComplete();
}
fetch(source) {
throw new Error("Subclasses must implement fetch()");
}
parse(raw) {
throw new Error("Subclasses must implement parse()");
}
validate(data) {
if (!data.id) throw new Error("Missing required field: id");
return data;
}
save(data) {
console.log("Saving:", data);
}
onComplete() {
// Hook — las subclases pueden sobrescribir
}
}
class CSVImporter extends DataImporter {
fetch(source) {
return `CSV content from ${source}`;
}
parse(raw) {
return { id: 1, format: "csv", content: raw };
}
}
class JSONImporter extends DataImporter {
fetch(source) {
return `JSON content from ${source}`;
}
parse(raw) {
return { id: 2, format: "json", content: raw };
}
onComplete() {
console.log("JSON import completed!");
}
}
// Uso
new CSVImporter().importData("users.csv");
new JSONImporter().importData("users.json");
Java
import java.util.Map;
abstract class DataImporter {
public final void importData(String source) {
String raw = fetch(source);
Map<String, Object> parsed = parse(raw);
Map<String, Object> validated = validate(parsed);
save(validated);
onComplete();
}
protected abstract String fetch(String source);
protected abstract Map<String, Object> parse(String raw);
protected Map<String, Object> validate(Map<String, Object> data) {
if (!data.containsKey("id")) {
throw new IllegalArgumentException("Missing required field: id");
}
return data;
}
protected void save(Map<String, Object> data) {
System.out.println("Saving: " + data);
}
protected void onComplete() {
// Hook — las subclases pueden sobrescribir
}
}
class CSVImporter extends DataImporter {
protected String fetch(String source) {
return "CSV content from " + source;
}
protected Map<String, Object> parse(String raw) {
return Map.of("id", 1, "format", "csv", "content", raw);
}
}
class JSONImporter extends DataImporter {
protected String fetch(String source) {
return "JSON content from " + source;
}
protected Map<String, Object> parse(String raw) {
return Map.of("id", 2, "format", "json", "content", raw);
}
protected void onComplete() {
System.out.println("JSON import completed!");
}
}
Dos detalles que conviene notar en la versión Java: importData es final para que ninguna subclase pueda reordenar los pasos, y el hook se llama onComplete en lugar de notify — Object.notify() es final y cualquier método con ese nombre no compila. Normalmente cada clase viviría en su propio archivo; aquí se agrupan por legibilidad.
Explicación
La clase base termina con cuatro tipos de miembros:
- Método template (
importData): el punto de entrada público que fija la secuencia. Márcalofinal(o el equivalente del lenguaje) para que el esqueleto no pueda romperse por accidente. - Pasos abstractos (
fetch,parse): no existe valor por defecto, así que cada subclase debe proporcionarlos. Mantén esta lista corta — cada uno es una carga para toda subclase futura. - Pasos concretos (
validate,save): implementaciones por defecto que la subclase hereda gratis y sobrescribe solo cuando necesita un comportamiento distinto. - Hooks (
onComplete): puntos de extensión vacíos o casi vacíos. La subclase se conecta solo si le interesa; el pipeline lo llama de todas formas.
El trade-off es real: el patrón compra una secuencia única y garantizada a cambio de una relación de herencia. Las subclases quedan fuertemente acopladas a la estructura de la base — renombra un paso o cambia su firma y todas las subclases tienen que moverse. Es el clásico problema de la clase base frágil, y es la razón por la que el propio libro GoF se inclina por la composición para variaciones mayores. Template Method brilla cuando los pasos son pocos y estables; cuando lo que varía es el algoritmo completo, prefiere Strategy.
Variantes
| Variante | Descripción | Caso de uso |
|---|---|---|
| Clásica | Herencia con métodos abstractos y hooks | Frameworks, parsers, importadores |
| Basada en callbacks | Pasos inyectados como funciones o lambdas | Streams de JavaScript/Node, cuando una subclase es exceso de ceremonia |
| Estilo Hollywood | El framework llama a tus pasos — “no nos llames, nosotros te llamamos” | Runners de tests, plugins de build, ciclos de vida de servlets |
La variante basada en callbacks mantiene la misma idea — una secuencia fija con pasos variables — pero cambia la clase base por parámetros de función. En TypeScript, un objeto de opciones con callbacks fetch, parse y un onComplete opcional cubre la mayoría de los casos donde una jerarquía completa sería ceremonia.
Lo Que Funciona
- Haz el método template
final— el punto es que la secuencia no es negociable. - Mantén los hooks opcionales y vacíos por defecto — documenta claramente qué métodos son obligatorios y cuáles opcionales, o las subclases lo adivinarán.
- Minimiza los pasos abstractos — cada método abstracto es trabajo obligatorio para toda subclase futura; dos o tres es un número sano.
- Nombra los hooks por lo que pasó, no por lo que hay que hacer —
onComplete,afterSavese leen como los puntos de extensión que son (y evitan colisiones con métodos heredados comoObject.notify()de Java). - Considera Strategy cuando las subclases sobrescribirían muchos pasos — si varía la mitad del algoritmo, la herencia es el vehículo equivocado.
Errores Comunes
- Olvidar marcar el método template como
final, permitiendo que una subclase reordene o salte pasos silenciosamente. - Hacer abstracto cada paso, lo que obliga a las subclases a implementar pasos que no necesitan — justo lo contrario de la conveniencia que busca el patrón.
- Llamar a pasos sobrescribibles desde el constructor. En Java y C# los campos de la subclase aún no están inicializados cuando corre el constructor de la base, así que el paso sobrescrito ve estado a medio construir.
- Meter lógica real en los hooks. Un hook que hace trabajo significativo es un paso obligatorio disfrazado; hazlo abstracto o acepta que las subclases olvidarán llamar a
super. - Introducir un segundo nivel de clases abstractas para variar un solo paso — ahí es cuando el impuesto de la jerarquía supera a la duplicación que pretendía eliminar.
Temas Avanzados
Escenario: Template Method para un pipeline de ETL
// Template Method: la clase base controla la forma del pipeline
abstract class ETLPipeline {
async run(source: DataSource): Promise<void> {
const rawData = await this.extract(source);
const transformed = await this.transform(rawData);
await this.load(transformed);
await this.onComplete();
}
abstract extract(source: DataSource): Promise<unknown[]>;
abstract transform(data: unknown[]): Promise<unknown[]>;
abstract load(data: unknown[]): Promise<void>;
async onComplete(): Promise<void> {
console.log("[ETL] Pipeline completed");
}
}
// Pipeline concreto: CSV -> DB
class CSVToDBPipeline extends ETLPipeline {
async extract(source: DataSource): Promise<unknown[]> {
const csv = await readFile(source.path);
return parseCSV(csv);
}
async transform(data: unknown[]): Promise<unknown[]> {
return data.map(row => ({
...row,
normalizedEmail: (row.email as string).toLowerCase().trim(),
timestamp: new Date().toISOString(),
}));
}
async load(data: unknown[]): Promise<void> {
await db.batchInsert("users", data);
}
}
// Pipeline concreto: API -> Redis
class APIToRedisPipeline extends ETLPipeline {
async extract(source: DataSource): Promise<unknown[]> {
const response = await fetch(source.url);
return response.json();
}
async transform(data: unknown[]): Promise<unknown[]> {
return data.filter(item => item.active);
}
async load(data: unknown[]): Promise<void> {
const pipeline = redis.pipeline();
data.forEach(item => pipeline.set(`item:${item.id}`, JSON.stringify(item)));
await pipeline.exec();
}
async onComplete(): Promise<void> {
await slack.notify("#data", "ETL completed: API -> Redis");
}
}
Lo que muestra esta versión que la básica no muestra:
- El método template
runesasyncy las subclases no lo tocan — extracción, transformación y carga ocurren siempre en ese orden. - Solo cambian los pasos entre pipelines: una lectura de disco vs. un fetch HTTP, una normalización vs. un filtro, un insert por lotes vs. un pipeline de Redis.
onCompleteno hace nada en un pipeline y manda una notificación a Slack en el otro — esa asimetría es exactamente para lo que sirven los hooks.- Añadir un tercer pipeline (por ejemplo, S3 a Elasticsearch) significa escribir tres implementaciones de pasos, no copiar la orquestación.
Véase También
- Patrón Strategy — el hermano basado en composición: úsalo cuando varía el algoritmo completo, no solo los pasos
- Patrón Factory — combina natural con Template Method cuando un paso debe decidir qué clase instanciar
- Guía de Patrones de Diseño — el catálogo completo: cuándo cada patrón GoF justifica su complejidad
- Repositorio companion — el ejemplo del importador como archivos ejecutables en Python, JavaScript y Java
Lectura Recomendada
- Refactoring.Guru — Template Method — recorrido UML con un ejemplo de parser de documentos
- Design Patterns: Elements of Reusable Object-Oriented Software (GoF) — la definición original (Gamma, Helm, Johnson, Vlissides, 1994)
- Effective Java, Item 18 — Prefiere la composición a la herencia — el argumento para Strategy cuando la variación es más que un paso
Preguntas frecuentes
¿Cuál es la diferencia entre Template Method y Strategy?
Template Method varía los pasos de un algoritmo mediante herencia; el flujo queda fijo en la clase base. Strategy intercambia el algoritmo completo mediante composición, y el cliente elige cuál inyectar. Template Method encaja en un pipeline de pasos fijos como un ETL; Strategy encaja cuando el algoritmo entero cambia en tiempo de ejecución, como reglas de precios u órdenes de clasificación.
¿Puedo combinar Template Method con Factory Method?
Sí, y es muy común. Un paso dentro del template puede ser un Factory Method que decide qué clase concreta instanciar — el template controla cuándo se crean los objetos, y la subclase controla qué se crea.
¿Qué pasa si una subclase olvida implementar un paso abstracto?
Depende del lenguaje. Java se niega a compilar — los métodos abstract deben implementarse. El abc de Python lanza TypeError al instanciar, no al importar. JavaScript no tiene palabra clave abstract; la convención habitual es lanzar dentro del método base, que solo falla cuando el paso se llama de verdad, así que los bugs aparecen más tarde.
¿El método template debería ser final?
Sí, siempre que el lenguaje lo permita. El método template es el contrato: si una subclase puede sobrescribirlo, el invariante "estos pasos corren en este orden" deja de ser una garantía y pasa a ser una sugerencia. Java tiene final, C# requiere sellado, Python y JavaScript dependen de convención — documéntalo y no lo toques en las subclases.
¿Cómo pruebo las subclases sin re-probar el esqueleto?
Prueba los pasos de forma aislada en cada subclase (¿CSVImporter.parse maneja campos con comillas?) y prueba la secuencia una sola vez en la clase base con un spy o una subclase fake que registre el orden de llamadas. Si te encuentras probando la misma secuencia de cinco pasos en el test de cada subclase, esa es la duplicación que el patrón debía eliminar.
¿Cuándo se convierte Template Method en un problema?
Cuando la variación crece más allá de los pasos. Si las subclases empiezan a sobrescribir métodos concretos por completo, a añadir pasos en medio de la secuencia o a saltarse el template para casos borde, el algoritmo ya no es "esqueleto fijo más variaciones" — esa es la señal para moverse a composición con Strategy o a una cadena de handlers.
Recursos Relacionados
Patrón Strategy
Define una familia de algoritmos, encapsula cada uno y los hace intercambiables. Patrón de diseño conductual para selección flexible de comportamiento.
PatternPatron Factory
Crea objetos sin especificar la clase exacta a instanciar. Un patrón de diseño creacional para la creación flexible de objetos.
PatternPatrón Decorator
Añade nueva funcionalidad a objetos dinámicamente envolviéndolos. Patrón de diseño estructural para extensión flexible de comportamiento.
GuideGuía Práctica de Design Patterns
Elige el design pattern correcto para cada problema: Singleton, Factory, Observer, Strategy y más, con código ejecutable y trade-offs honestos.
RecipeIntercambiar Algoritmos en Runtime con el Strategy Pattern
Cómo encapsular algoritmos y comportamientos intercambiables usando el strategy pattern con inyección de dependencias, function pointers y lambda strategies en Java, TypeScript y Python.