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.
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.
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:
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.
Los roles principales del patrón son:
La clase base puede contener métodos protegidos para asegurar que solo las subclases personalicen el comportamiento, conservando el contrato interno del proceso.
Las plantillas suelen distinguir dos tipos de pasos:
El uso cuidadoso de hooks evita la proliferación de subclases casi idénticas y reduce el riesgo de ruptura ante cambios menores.
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.
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");
}
}
}
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
.
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.
Existen ajustes comunes del patrón:
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.
final
para impedir que el orden se modifique en subclases.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.