Patrón de método de plantilla Python
El patrón de método de plantilla es un patrón de diseño de comportamiento que define el esqueleto de un algoritmo en una operación, difiriendo algunos pasos a las subclases para su implementación. El método de plantilla permite que las subclases redefinan ciertos pasos específicos del algoritmo sin cambiar la estructura del algoritmo.
En términos simples, es como una receta de cocina: la receta especifica el orden de los pasos (lavar los ingredientes, cortarlos, cocinarlos, emplatarlos), pero la forma concreta de lavar, cortar y cocinar puede ser implementada por diferentes cocineros según su propio estilo.
¿Por qué se necesita el patrón de método de plantilla?
Reutilización de código
El patrón de método de plantilla traslada el comportamiento invariable a la superclase, evitando la duplicación de código en las subclases. Múltiples subclases pueden compartir la estructura de algoritmo definida por el mismo método de plantilla.
Extensibilidad
Las subclases pueden extender o modificar parte del comportamiento del algoritmo reescribiendo métodos de enganche (hook) o pasos concretos, sin necesidad de cambiar la estructura general del algoritmo.
Inversión de control
La clase padre controla el flujo de ejecución del algoritmo; las subclases solo necesitan centrarse en los pasos específicos que deben implementar, lo que realiza el "principio de Hollywood": "No nos llames, nosotros te llamaremos".
Estructura del patrón de método de plantilla
Entendamos la estructura del patrón de método de plantilla mediante un diagrama de clases:

Explicación de los componentes principales
| Componente | Responsabilidad | Descripción |
|---|---|---|
| AbstractClass | Define el esqueleto del algoritmo | Contiene el método de plantilla y los métodos de pasos abstractos |
| ConcreteClass | Implementa los pasos concretos | Implementa los métodos abstractos definidos por la clase padre |
| template_method | Método de plantilla | Define la parte invariable del algoritmo |
| step1, step2 | Método abstracto | Pasos concretos que deben implementar las subclases |
| hook | Método de enganche (hook) | Paso opcional; la subclase puede elegir si reescribirlo o no |
Sintaxis básica e implementación
Definición de la clase base abstracta
Ejemplo
class AbstractClass(ABC):
"""Clase base abstracta del patrón de método de plantilla"""
def template_method(self):
"""Método de plantilla: define el esqueleto del algoritmo"""
self.step1()
self.step2()
self.hook()
self.step3()
@abstractmethod
def step1(self):
"""Método abstracto 1: debe ser implementado por la subclase"""
pass
@abstractmethod
def step2(self):
"""Método abstracto 2: debe ser implementado por la subclase"""
pass
def step3(self):
"""Método concreto: ya tiene una implementación por defecto"""
print("Paso 3 de ejecución: implementación por defecto")
def hook(self):
"""Método de enganche: paso opcional, la subclase puede elegir reescribirlo"""
print("Ejecutar método de enganche: por defecto no hace nada")
Implementación de la subclase concreta
Ejemplo
"""Clase de implementación concreta A"""
def step1(self):
print("ConcreteClassA - Ejecutar paso 1")
def step2(self):
print("ConcreteClassA - Ejecutar paso 2")
def hook(self):
print("ConcreteClassA - Reescribe el método de enganche para añadir funcionalidad adicional")
class ConcreteClassB(AbstractClass):
"""Clase de implementación concreta B"""
def step1(self):
print("ConcreteClassB - Ejecutar paso 1")
def step2(self):
print("ConcreteClassB - Ejecutar paso 2")
# No reescribe el método hook; usa la implementación por defecto
Ejemplos de aplicación práctica
A través de varios ejemplos prácticos, profundicemos en la aplicación del patrón de método de plantilla.
Ejemplo 1: Plantilla de procesamiento de datos
Ejemplo
import json
import csv
class DataProcessor(ABC):
"""Plantilla de procesamiento de datos"""
def process_data(self, input_file, output_file):
"""Método de plantilla de procesamiento de datos"""
print(f"Comenzando a procesar datos: {input_file} -> {output_file}")
# Leer datos
data = self.read_data(input_file)
print(f"Se leyeron {len(data)} registros")
# Transformar datos
transformed_data = self.transform_data(data)
print("Transformación de datos completada")
# Guardar datos
self.save_data(transformed_data, output_file)
print("Guardado de datos completado")
# Trabajo de limpieza
self.cleanup()
@abstractmethod
def read_data(self, file_path):
"""Leer datos: método abstracto"""
pass
@abstractmethod
def transform_data(self, data):
"""Transformar datos: método abstracto"""
pass
def save_data(self, data, file_path):
"""Guardar datos: método concreto"""
with open(file_path, 'w', encoding='utf-8') as f:
if isinstance(data, list):
for item in data:
f.write(str(item) + '\n')
else:
f.write(str(data))
def cleanup(self):
"""Trabajo de limpieza: método de enganche"""
print("Limpieza completada")
class JSONProcessor(DataProcessor):
"""Procesador de datos JSON"""
def read_data(self, file_path):
with open(file_path, 'r', encoding='utf-8') as f:
return json.load(f)
def transform_data(self, data):
# Transformación simple: convertir todos los valores de cadena a mayúsculas
if isinstance(data, dict):
return {k: v.upper() if isinstance(v, str) else v
for k, v in data.items()}
elif isinstance(data, list):
return [item.upper() if isinstance(item, str) else item
for item in data]
return data
class CSVProcessor(DataProcessor):
"""Procesador de datos CSV"""
def read_data(self, file_path):
data = []
with open(file_path, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
data.append(row)
return data
def transform_data(self, data):
# Añadir una marca de tiempo de procesamiento a cada registro
import datetime
timestamp = datetime.datetime.now().isoformat()
for record in data:
record['processed_at'] = timestamp
return data
def cleanup(self):
"""Reescribir el método de limpieza para añadir lógica de limpieza adicional"""
print("Limpieza del procesador CSV completada")
print("Liberando recursos del analizador CSV")
# Ejemplo de uso
if __name__ == "__main__":
# Crear datos de prueba
import os
# Probar el procesador JSON
json_data = {'name': 'john', 'age': 30, 'city': 'new york'}
with open('test.json', 'w') as f:
json.dump(json_data, f)
json_processor = JSONProcessor()
json_processor.process_data('test.json', 'output_json.txt')
print("\n" + "="*50 + "\n")
# Probar el procesador CSV
csv_data = "name,age,city\njohn,30,new york\njane,25,los angeles"
with open('test.csv', 'w') as f:
f.write(csv_data)
csv_processor = CSVProcessor()
csv_processor.process_data('test.csv', 'output_csv.txt')
# Limpiar archivos de prueba
for file in ['test.json', 'test.csv', 'output_json.txt', 'output_csv.txt']:
if os.path.exists(file):
os.remove(file)
Ejemplo 2: Plantilla para preparar bebidas
Ejemplo
class BeverageMaker(ABC):
"""Plantilla de preparación de bebidas"""
def make_beverage(self):
"""Método de plantilla para preparar bebidas"""
self.boil_water()
self.brew()
self.pour_in_cup()
if self.customer_wants_condiments():
self.add_condiments()
self.serve()
def boil_water(self):
"""Hervir agua: método concreto"""
print("Hervir agua")
@abstractmethod
def brew(self):
"""Infusión: método abstracto"""
pass
def pour_in_cup(self):
"""Verter en la taza: método concreto"""
print("Verter en la taza")
@abstractmethod
def add_condiments(self):
"""Añadir condimentos: método abstracto"""
pass
def customer_wants_condiments(self):
"""Método de enganche: si el cliente quiere condimentos"""
return True
def serve(self):
"""Servir la bebida: método concreto"""
print("¡Bebida lista, disfrútala!")
class CoffeeMaker(BeverageMaker):
"""Preparación de café"""
def brew(self):
print("Infusionar café molido con agua hirviendo")
def add_condiments(self):
print("Añadir azúcar y leche")
def customer_wants_condiments(self):
answer = input("¿Quieres azúcar y leche en el café? (s/n): ")
return answer.lower() == 'y'
class TeaMaker(BeverageMaker):
"""Preparación de té"""
def brew(self):
print("Sumergir hojas de té en agua hirviendo")
def add_condiments(self):
print("Añadir limón")
# Ejemplo de uso
if __name__ == "__main__":
print("Preparando café:")
coffee = CoffeeMaker()
coffee.make_beverage()
print("\n" + "="*30 + "\n")
print("Preparando té:")
tea = TeaMaker()
tea.make_beverage()
Variantes del patrón de método de plantilla
1. Método de plantilla con parámetros
Ejemplo
"""Procesador de datos configurable"""
def process_with_config(self, input_file, output_file, config):
"""Método de plantilla con parámetros de configuración"""
self.validate_config(config)
data = self.read_data(input_file)
processed_data = self.process_with_config_impl(data, config)
self.save_data(processed_data, output_file)
self.post_process(config)
def validate_config(self, config):
"""Validar configuración: método concreto"""
required_keys = ['format', 'encoding']
for key in required_keys:
if key not in config:
raise ValueError(f"Falta una clave obligatoria en la configuración: {key}")
@abstractmethod
def process_with_config_impl(self, data, config):
"""Implementación del procesamiento de datos con configuración"""
pass
def post_process(self, config):
"""Postprocesamiento: método de enganche"""
if config.get('cleanup', False):
print("Ejecutar operación de limpieza")
2. Método de plantilla de múltiples pasos
Ejemplo
"""Procesador de múltiples pasos"""
def complex_processing(self):
"""Flujo de procesamiento complejo"""
self.initialize()
self.pre_process()
self.main_process()
self.post_process()
self.finalize()
def initialize(self):
print("Inicializar el procesador")
def pre_process(self):
"""Preprocesamiento: método de enganche"""
pass
@abstractmethod
def main_process(self):
"""Lógica principal de procesamiento"""
pass
def post_process(self):
"""Postprocesamiento: método de enganche"""
pass
def finalize(self):
print("Procesamiento completado")
Mejores prácticas y consideraciones
1. Uso adecuado de métodos abstractos
- Declarar como abstractos solo los métodos que realmente deben ser implementados por las subclases
- Proporcionar métodos de enganche con implementación por defecto para pasos opcionales
2. Control de acceso al método de plantilla
- El método de plantilla normalmente debería declararse como
final(en Python se puede hacer mediante convenciones de nomenclatura) - Los métodos de paso deben estar protegidos para evitar que sean llamados directamente desde el exterior
3. Manejo de errores
Ejemplo
"""Clase de plantilla robusta"""
def template_method(self):
try:
self.setup()
self.execute_steps()
except Exception as e:
self.handle_error(e)
finally:
self.cleanup()
def execute_steps(self):
Secuencia de pasos de ejecución
self.step1()
self.step2()
self.step3()
def handle_error(self, error):
Manejo de errores - Métodos hook
print(fSe produjo un error durante el procesamiento: {error})
@abstractmethod
def setup(self):
pass
@abstractmethod
def step1(self):
pass
@abstractmethod
def step2(self):
pass
@abstractmethod
def step3(self):
pass
def cleanup(self):
print(Limpiar recursos)
Ejercicios prácticos
¡Ahora te toca practicar a ti! Completa los siguientes ejercicios para afianzar tu comprensión del patrón de método de plantilla:
Ejercicio 1: Implementar una plantilla de exportación de archivos
Crea una plantilla de exportación de archivos que admita la exportación a diferentes formatos (TXT, HTML, JSON).
Requisitos:
- Definir una clase base abstracta
FileExporter - Implementar subclases concretas
TXTExporter、HTMLExporter、JSONExporter - El método de plantilla debe incluir pasos como: preparar datos, formatear datos, guardar archivos, posprocesamiento, etc.
Ejercicio 2: Mejorar la plantilla para preparar bebidas
Extiende la plantilla de preparación de bebidas anterior y añade las siguientes funcionalidades:
- Permitir elegir el tamaño de la bebida (pequeño, mediano, grande)
- Añadir funcionalidad de cálculo de precio
- Permitir condimentos personalizados
Resumen
El patrón de método de plantilla es un patrón de diseño potente y práctico que nos ayuda a construir un mejor código de las siguientes maneras:
Ventajas principales
- Mejora la reutilización del código: coloca el código común en la clase padre
- Mejora la extensibilidad: extendiendo pasos específicos mediante subclases
- Cumple el principio abierto/cerrado: abierto a la extensión, cerrado a la modificación
- Inversión de control: la clase padre controla el flujo y las subclases implementan los detalles.
Escenarios de uso
- Varias clases tienen los mismos métodos, pero con implementaciones diferentes.
- Se necesita controlar los puntos de extensión de las subclases.
- Se necesita definir el esqueleto de un algoritmo, pero permitir que algunos pasos varíen.
Puntos clave
- El método de plantilla define la parte invariante del algoritmo.
- Los métodos abstractos definen la parte variable del algoritmo.
- Los métodos hook proporcionan puntos de extensión opcionales.