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 C#

El siguiente ejemplo muestra una plantilla con pasos abstractos y hooks para registrar auditoría y personalizar la transformación de datos utilizando colecciones genéricas de .NET:

using System;
using System.Collections.Generic;

namespace Tutorial.TemplateMethod
{
    public abstract class ProcesoImportacion
    {
        public void Ejecutar(string origen)
        {
            RegistrarInicio(origen);
            ValidarOrigen(origen);
            IList<IDictionary<string, object>> datosCrudos = ObtenerDatos(origen);
            IList<IDictionary<string, object>> datosNormalizados = Normalizar(datosCrudos);
            IList<IDictionary<string, object>> datosEnriquecidos = Enriquecer(datosNormalizados);
            Persistir(datosEnriquecidos);
            RegistrarFin(origen, datosEnriquecidos.Count);
        }

        protected virtual void RegistrarInicio(string origen)
        {
            Console.WriteLine($"[{DateTimeOffset.UtcNow:O}] Iniciando importacion desde {origen}");
        }

        protected abstract void ValidarOrigen(string origen);

        protected abstract IList<IDictionary<string, object>> ObtenerDatos(string origen);

        protected virtual IList<IDictionary<string, object>> Normalizar(IList<IDictionary<string, object>> datos)
        {
            return datos;
        }

        protected abstract IList<IDictionary<string, object>> Enriquecer(IList<IDictionary<string, object>> datos);

        protected abstract void Persistir(IList<IDictionary<string, object>> datos);

        protected virtual void RegistrarFin(string origen, int registros)
        {
            Console.WriteLine($"[{DateTimeOffset.UtcNow:O}] Importacion finalizada: {registros} registros desde {origen}");
        }
    }

    public sealed class ImportacionCsv : ProcesoImportacion
    {
        protected override void ValidarOrigen(string origen)
        {
            if (string.IsNullOrWhiteSpace(origen) || !origen.EndsWith(".csv", StringComparison.OrdinalIgnoreCase))
            {
                throw new ArgumentException("Se esperaba un archivo CSV valido.", nameof(origen));
            }

            Console.WriteLine("Archivo CSV validado correctamente.");
        }

        protected override IList<IDictionary<string, object>> ObtenerDatos(string origen)
        {
            Console.WriteLine($"Leyendo archivo {origen}");
            return new List<IDictionary<string, object>>
            {
                new Dictionary<string, object>
                {
                    ["id"] = Guid.NewGuid(),
                    ["nombre"] = "Cliente CSV",
                    ["pais"] = "AR"
                }
            };
        }

        protected override IList<IDictionary<string, object>> Enriquecer(IList<IDictionary<string, object>> datos)
        {
            Console.WriteLine("Enriqueciendo datos con segmento de marketing.");
            return datos;
        }

        protected override void Persistir(IList<IDictionary<string, object>> datos)
        {
            Console.WriteLine($"Persistiendo {datos.Count} registros en la base de datos relacional.");
        }
    }

    public sealed class ImportacionApi : ProcesoImportacion
    {
        protected override void ValidarOrigen(string origen)
        {
            if (!Uri.TryCreate(origen, UriKind.Absolute, out var uri) || uri.Scheme != Uri.UriSchemeHttps)
            {
                throw new ArgumentException("La API debe exponer un endpoint HTTPS valido.", nameof(origen));
            }

            Console.WriteLine("Endpoint remoto validado.");
        }

        protected override IList<IDictionary<string, object>> ObtenerDatos(string origen)
        {
            Console.WriteLine($"Invocando API {origen}");
            return new List<IDictionary<string, object>>
            {
                new Dictionary<string, object>
                {
                    ["id"] = Guid.NewGuid(),
                    ["nombre"] = "Cliente API",
                    ["pais"] = "UY",
                    ["ultimoLogin"] = DateTimeOffset.UtcNow
                }
            };
        }

        protected override IList<IDictionary<string, object>> Normalizar(IList<IDictionary<string, object>> datos)
        {
            Console.WriteLine("Normalizando contenido JSON a diccionarios tipados.");
            return datos;
        }

        protected override IList<IDictionary<string, object>> Enriquecer(IList<IDictionary<string, object>> datos)
        {
            Console.WriteLine("Enriqueciendo datos con catalogo de paises.");
            return datos;
        }

        protected override void Persistir(IList<IDictionary<string, object>> datos)
        {
            Console.WriteLine("Publicando registros en la cola de eventos.");
        }
    }

    public static class AplicacionTemplateMethod
    {
        public static void Main()
        {
            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 sobre colecciones IList<IDictionary<string, object>> 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 registrados con DateTimeOffset.

21.8 Aplicaciones en el ecosistema .NET

Muchas APIs oficiales utilizan Template Method para compartir comportamiento. La clase DelegatingHandler define el esqueleto de procesamiento de mensajes HTTP delegando en subclases los pasos específicos. Del mismo modo, DbContext en Entity Framework Core expone hooks virtuales como SaveChanges/OnModelCreating para garantizar el flujo y permitir personalizaciones controladas.

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.