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

from abc import ABC, abstractmethod

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

class ConcreteClassA(AbstractClass):
    """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

from abc import ABC, abstractmethod
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

from abc import ABC, abstractmethod

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

class ConfigurableProcessor(ABC):
    """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

class MultiStepProcessor(ABC):
    """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 comofinal(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

class RobustTemplate(ABC):
    """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 abstractaFileExporter
  • Implementar subclases concretasTXTExporter、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

  1. Mejora la reutilización del código: coloca el código común en la clase padre
  2. Mejora la extensibilidad: extendiendo pasos específicos mediante subclases
  3. Cumple el principio abierto/cerrado: abierto a la extensión, cerrado a la modificación
  4. 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.
Otras extensiones