21. Template Method (Método Plantilla) - Patrón de Comportamiento

El patrón Template Method define el esqueleto de un algoritmo en una operación, delegando a las subclases la implementación de ciertos pasos. La estructura general permanece inmutable, pero los puntos de extensión permiten variar detalles según las necesidades del dominio.

Es especialmente valioso cuando varios procesos comparten la misma secuencia de pasos pero difieren en cómo resuelven etapas específicas. La clase abstracta garantiza el flujo, mientras que las subclases rellenan los huecos (hooks) para adaptar el comportamiento.

21.1 Problema y contexto

En numerosos sistemas encontramos procedimientos estandarizados: abrir conexión, validar entrada, procesar datos, registrar resultados y cerrar recursos. Copiar esta secuencia en cada caso genera duplicación, dificulta el mantenimiento y abre la puerta a inconsistencias. Las variaciones suelen concentrarse en pasos concretos como la transformación de datos o la forma de persistirlos.

Template Method evita la duplicación al capturar el flujo común en una clase base, proporcionando puntos de extensión controlados para la lógica variable. Los consumidores heredan de la plantilla y completan los pasos obligatorios para personalizar el proceso.

21.2 Intención y motivación

La intención del patrón es definir el esqueleto de un algoritmo en una operación, diferiendo algunos pasos a subclases. Es pertinente cuando:

  • Se necesita preservar el orden exacto de un proceso pero personalizar etapas específicas.
  • Existen requisitos normativos o de compliance que exigen que la secuencia se mantenga intacta.
  • Se desea reutilizar código base, evitando que cada variante replique la misma lógica de orquestación.
  • Se requiere permitir que clientes agreguen o inhiban pasos opcionales mediante hooks.

La motivación se centra en separar la invariancia del algoritmo de las partes que pueden cambiar, preservando el principio Abierto/Cerrado sin sacrificar claridad.

21.3 Participantes y estructura

Los roles principales del patrón son:

  • AbstractClass: define la operación plantilla (template method) que orquesta los pasos. Implementa aquellos que son comunes y declara abstractos los que deben proporcionar las subclases.
  • ConcreteClass: subclases que implementan los pasos abstractos y redefinen hooks opcionales.
  • Hooks: métodos con implementación por defecto (a veces vacía) que las subclases pueden sobrescribir para ajustar detalles sin romper el algoritmo.

La clase base puede contener métodos protegidos para asegurar que solo las subclases personalicen el comportamiento, conservando el contrato interno del proceso.

21.4 Hooks obligatorios y opcionales

Las plantillas suelen distinguir dos tipos de pasos:

  • Abstract methods: obligatorios, las subclases deben implementarlos para completar el algoritmo.
  • Hooks: opcionales, con implementación por defecto. Permiten ejecutar lógica adicional antes o después de un paso sin forzar a todas las subclases a definirlos.

El uso cuidadoso de hooks evita la proliferación de subclases casi idénticas y reduce el riesgo de ruptura ante cambios menores.

21.5 Escenario: importación de datos en una plataforma analítica

Una empresa ofrece una plataforma que integra datos de clientes provenientes de distintas fuentes: CSV subidos manualmente, APIs externas y streams en tiempo real. Todos los flujos comparten pasos comunes: validar origen, normalizar datos, enriquecer campos, almacenar en el lago de datos y registrar auditoría. Lo que cambia es cómo se obtiene la fuente y qué enriquecimientos se aplican.

Template Method permite definir un proceso base de importación garantizando que todos los pasos se ejecuten en el orden correcto. Cada tipo de fuente hereda de la plantilla y proporciona sus detalles particulares, manteniendo una experiencia consistente para las operaciones y facilitando nuevas integraciones.

21.6 Implementación en Java

El siguiente ejemplo muestra una plantilla con pasos abstractos y hooks para registrar auditoría y personalizar la transformación de datos:

package tutorial.templatemethod;

import java.time.Instant;
import java.util.List;
import java.util.Map;

public abstract class ProcesoImportacion {

    public final void ejecutar(String origen) {
        registrarInicio(origen);
        validarOrigen(origen);
        List<Map<String, Object>> datosCrudos = obtenerDatos(origen);
        List<Map<String, Object>> datosNormalizados = normalizar(datosCrudos);
        List<Map<String, Object>> datosEnriquecidos = enriquecer(datosNormalizados);
        persistir(datosEnriquecidos);
        registrarFin(origen, datosEnriquecidos.size());
    }

    protected void registrarInicio(String origen) {
        System.out.printf("[%s] Iniciando importación desde %s%n", Instant.now(), origen);
    }

    protected abstract void validarOrigen(String origen);

    protected abstract List<Map<String, Object>> obtenerDatos(String origen);

    protected List<Map<String, Object>> normalizar(List<Map<String, Object>> datos) {
        return datos;
    }

    protected abstract List<Map<String, Object>> enriquecer(List<Map<String, Object>> datos);

    protected abstract void persistir(List<Map<String, Object>> datos);

    protected void registrarFin(String origen, int registros) {
        System.out.printf("[%s] Importación finalizada: %d registros desde %s%n",
                Instant.now(), registros, origen);
    }
}

class ImportacionCsv extends ProcesoImportacion {
    @Override
    protected void validarOrigen(String origen) {
        if (!origen.endsWith(".csv")) {
            throw new IllegalArgumentException("El archivo debe ser CSV: " + origen);
        }
    }

    @Override
    protected List<Map<String, Object>> obtenerDatos(String origen) {
        System.out.println("Leyendo archivo CSV " + origen);
        return List.of(Map.of("cliente", "Ana", "pais", "AR"));
    }

    @Override
    protected List<Map<String, Object>> enriquecer(List<Map<String, Object>> datos) {
        System.out.println("Aplicando reglas de enriquecimiento para CSV");
        return datos;
    }

    @Override
    protected void persistir(List<Map<String, Object>> datos) {
        System.out.println("Persistiendo datos en el lago de datos");
    }
}

class ImportacionApi extends ProcesoImportacion {
    @Override
    protected void validarOrigen(String origen) {
        if (!origen.startsWith("https://")) {
            throw new IllegalArgumentException("La API debe ser segura: " + origen);
        }
    }

    @Override
    protected List<Map<String, Object>> obtenerDatos(String origen) {
        System.out.println("Invocando endpoint remoto " + origen);
        return List.of(Map.of("cliente", "Luis", "pais", "CL"));
    }

    @Override
    protected List<Map<String, Object>> normalizar(List<Map<String, Object>> datos) {
        System.out.println("Normalizando campos JSON a estructura interna");
        return datos;
    }

    @Override
    protected List<Map<String, Object>> enriquecer(List<Map<String, Object>> datos) {
        System.out.println("Enriqueciendo datos con catálogo de países");
        return datos;
    }

    @Override
    protected void persistir(List<Map<String, Object>> datos) {
        System.out.println("Encolando registros en el pipeline de streaming");
    }
}

class AplicacionTemplateMethod {
    public static void main(String[] args) {
        ProcesoImportacion csv = new ImportacionCsv();
        ProcesoImportacion api = new ImportacionApi();

        csv.ejecutar("clientes-latam.csv");
        api.ejecutar("https://api.ejemplo.com/clientes");
    }
}

21.7 Explicación del flujo

La clase ProcesoImportacion fija el orden de las etapas: registro, validación, obtención, normalización, enriquecimiento y persistencia. Las subclases completan los métodos abstractos y, cuando lo necesitan, redefinen hooks como normalizar para personalizar parte del flujo sin alterar el método plantilla. Cualquier nueva fuente que se agregue al ecosistema hereda de la plantilla y garantiza que se ejecuten los pasos críticos de auditoría.

21.8 Aplicaciones en el ecosistema Java

Muchas APIs oficiales utilizan Template Method para compartir comportamiento. La documentación de AbstractList muestra cómo se define el esqueleto de operaciones de colecciones, delegando en las subclases detalles como el acceso a los elementos. Frameworks de persistencia, motores de plantillas y servidores web aplican la misma idea para encapsular flujos de peticiones, validaciones y renderizados.

21.9 Variantes y extensiones

Existen ajustes comunes del patrón:

  • Template Method con objetos estrategia: la clase base delega en estrategias internas para pasos opcionales, combinando este patrón con Strategy.
  • Plantillas parametrizadas: la clase abstracta recibe componentes colaborativos por inyección, lo que evita el uso intensivo de herencia.
  • Plantillas asíncronas: la secuencia fija se mantiene pero cada paso devuelve futuros o promesas para trabajar con pipelines reactivos.

21.10 Riesgos y malas prácticas

La herencia excesiva puede derivar en jerarquías profundas y difíciles de mantener. También es peligroso sobrecargar la clase base con demasiados hooks, generando incertidumbre sobre cuáles deben sobrescribirse. Otro riesgo es romper la inmutabilidad del algoritmo permitiendo que las subclases alteren el orden de los pasos; la plantilla debe proteger esos puntos críticos.

21.11 Buenas prácticas para aplicar Template Method

  • Mantener el método plantilla como final para impedir que el orden se modifique en subclases.
  • Documentar qué pasos son obligatorios y cuáles se pueden personalizar.
  • Proveer implementaciones predeterminadas para los hooks más comunes a fin de evitar código repetitivo.
  • Complementar con pruebas unitarias que aseguren la ejecución del flujo completo y la integración de las subclases.

21.12 Relación con otros patrones

Template Method se complementa con Strategy para parametrizar pasos específicos sin recurrir a la herencia. Comparte objetivos con Builder cuando se construyen objetos complejos paso a paso, aunque Builder se centra en la creación y Template Method en la ejecución de procesos. También puede combinarse con Hook Method (nombre que a veces se da a los propios hooks) y con Chain of Responsibility cuando algunos pasos se delegan a filtros encadenados.