StackPractices
beginner Por Mathias Paulenko

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.

Temas: design

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 final hace 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.

Diagrama de clases de Template Method — DataImporter abstracta con importData final que llama a fetch, parse, validate, save, onComplete; CSVImporter y JSONImporter heredan e implementan fetch y parse

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árcalo final (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

VarianteDescripciónCaso de uso
ClásicaHerencia con métodos abstractos y hooksFrameworks, parsers, importadores
Basada en callbacksPasos inyectados como funciones o lambdasStreams de JavaScript/Node, cuando una subclase es exceso de ceremonia
Estilo HollywoodEl 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, afterSave se leen como los puntos de extensión que son (y evitan colisiones con métodos heredados como Object.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 run es async y 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.
  • onComplete no 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

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.