Паттерн «Шаблонный метод» в Python

Паттерн «Шаблонный метод» — это поведенческий паттерн проектирования, который определяет скелет алгоритма в операции, делегируя некоторые шаги подклассам. Он позволяет подклассам переопределять определённые шаги алгоритма, не меняя структуру алгоритма.

Проще говоря, это похоже на кулинарный рецепт: рецепт задаёт порядок шагов (мытьё овощей, нарезка, обжарка, выкладывание на тарелку), но то, как именно мыть, нарезать и обжаривать, могут реализовать разные повара в соответствии со своим стилем.


Зачем нужен паттерн «Шаблонный метод»?

Повторное использование кода

Паттерн «Шаблонный метод» переносит неизменяемое поведение в суперкласс, избегая дублирования кода в подклассах. Несколько подклассов могут совместно использовать структуру алгоритма, определённую одним и тем же шаблонным методом.

Расширяемость

Подклассы могут расширять или изменять часть поведения алгоритма, переопределяя методы-хуки или конкретные методы шагов, не меняя общую структуру алгоритма.

Инверсия управления

Родительский класс управляет ходом выполнения алгоритма, а подклассам нужно сосредоточиться только на конкретных шагах, которые они должны реализовать, воплощая «голливудский принцип» — «Не звоните нам, мы сами позвоним вам».


Структура паттерна «Шаблонный метод»

Давайте разберём структуру паттерна «Шаблонный метод» с помощью диаграммы классов:

Описание основных компонентов

Компонент Ответственность Описание
AbstractClass Определяет скелет алгоритма Содержит шаблонный метод и абстрактные методы шагов
ConcreteClass Реализует конкретные шаги Реализует абстрактные методы, определённые родительским классом
template_method Шаблонный метод Определяет инвариантные части алгоритма
step1, step2 Абстрактный метод Конкретные шаги, которые должны быть реализованы подклассами
хук Метод-хук Необязательный шаг; подклассы могут выбирать, переопределять ли его

Базовый синтаксис и реализация

Определение абстрактного базового класса

Пример

from abc import ABC, abstractmethod

class AbstractClass(ABC):
    """Абстрактный базовый класс паттерна «Шаблонный метод»"""
   
    def template_method(self):
        """Шаблонный метод - определяет скелет алгоритма"""
        self.step1()
        self.step2()
        self.hook()
        self.step3()
   
    @abstractmethod
    def step1(self):
        """Абстрактный метод 1 - должен быть реализован подклассами"""
        pass
   
    @abstractmethod
    def step2(self):
        """Абстрактный метод 2 - должен быть реализован подклассами"""
        pass
   
    def step3(self):
        """Конкретный метод - имеет реализацию по умолчанию"""
        print("Выполнить шаг 3 - реализация по умолчанию")
   
    def hook(self):
        """Метод-хук - необязательный шаг; подклассы могут переопределить его"""
        print("Выполнить метод-хук - по умолчанию ничего не делает")

Реализация конкретных подклассов

Пример

class ConcreteClassA(AbstractClass):
    """Конкретная реализация класса A"""
   
    def step1(self):
        print("ConcreteClassA - выполнение шага 1")
   
    def step2(self):
        print("ConcreteClassA - выполнение шага 2")
   
    def hook(self):
        print("ConcreteClassA - переопределение метода-хука для добавления дополнительной функциональности")

class ConcreteClassB(AbstractClass):
    """Конкретная реализация класса B"""
   
    def step1(self):
        print("ConcreteClassB - выполнение шага 1")
   
    def step2(self):
        print("ConcreteClassB - выполнение шага 2")
   
    # Не переопределяйте метод-хук; используйте реализацию по умолчанию

Примеры практического применения

Давайте глубже разберём паттерн «Шаблонный метод» на нескольких практических примерах.

Пример 1: Шаблон обработки данных

Пример

from abc import ABC, abstractmethod
import json
import csv

class DataProcessor(ABC):
    """Шаблон обработки данных"""
   
    def process_data(self, input_file, output_file):
        """Шаблонный метод обработки данных"""
        print(f"Начало обработки данных: {input_file} -> {output_file}")
       
        # Чтение данных
        data = self.read_data(input_file)
        print(f"Прочитано {len(data)} записей")
       
        # Преобразование данных
        transformed_data = self.transform_data(data)
        print("Преобразование данных завершено")
       
        # Сохранить данные
        self.save_data(transformed_data, output_file)
        print("Сохранение данных завершено")
       
        # Очистка
        self.cleanup()
   
    @abstractmethod
    def read_data(self, file_path):
        """Чтение данных - абстрактный метод"""
        pass
   
    @abstractmethod
    def transform_data(self, data):
        """Преобразование данных - абстрактный метод"""
        pass
   
    def save_data(self, data, file_path):
        """Сохранение данных - конкретный метод"""
        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):
        """Очистка - метод-хук"""
        print("Очистка завершена")

class JSONProcessor(DataProcessor):
    """Обработчик данных 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):
        # Простое преобразование: преобразовать все строковые значения в верхний регистр
        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):
    """Обработчик 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):
        # Добавить метку времени обработки к каждой записи
        import datetime
        timestamp = datetime.datetime.now().isoformat()
        for record in data:
            record['processed_at'] = timestamp
        return data
   
    def cleanup(self):
        """Переопределите метод cleanup, чтобы добавить дополнительную логику очистки"""
        print("Очистка CSV-процессора завершена")
        print("Освобождение ресурсов CSV-парсера")

# Пример использования
if __name__ == "__main__":
    # Создание тестовых данных
    import os
   
    # Тестирование 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")
   
    # Тестирование CSV-процессора
    csv_data = "имя,возраст,город\nДжон,30,Нью-Йорк\nДжейн,25,Лос-Анджелес"
    with open('test.csv', 'w') as f:
        f.write(csv_data)
   
    csv_processor = CSVProcessor()
    csv_processor.process_data('test.csv', 'output_csv.txt')
   
    # Очистить тестовые файлы
    for file in ['test.json', 'test.csv', 'output_json.txt', 'output_csv.txt']:
        if os.path.exists(file):
            os.remove(file)

Пример 2: Шаблон приготовления напитков

Пример

from abc import ABC, abstractmethod

class BeverageMaker(ABC):
    """Шаблон приготовления напитка"""
   
    def make_beverage(self):
        """Шаблонный метод приготовления напитков"""
        self.boil_water()
        self.brew()
        self.pour_in_cup()
        if self.customer_wants_condiments():
            self.add_condiments()
        self.serve()
   
    def boil_water(self):
        """Вскипятить воду - конкретный метод"""
        print("Кипячение воды")
   
    @abstractmethod
    def brew(self):
        """Brew - абстрактный метод"""
        pass
   
    def pour_in_cup(self):
        """Налить в чашку - конкретный метод"""
        print("Налейте в чашку")
   
    @abstractmethod
    def add_condiments(self):
        """Добавить приправы - абстрактный метод"""
        pass
   
    def customer_wants_condiments(self):
        """Метод-хук - хочет ли клиент приправы"""
        return True
   
    def serve(self):
        """Подать напиток - конкретный метод"""
        print("Ваш напиток готов, наслаждайтесь!")

class CoffeeMaker(BeverageMaker):
    """Приготовление кофе"""
   
    def brew(self):
        print("Заварите молотый кофе кипящей водой")
   
    def add_condiments(self):
        print("Добавьте сахар и молоко")
   
    def customer_wants_condiments(self):
        answer = input("Хотите сахар и молоко в кофе? (y/n): ")
        return answer.lower() == 'y'

class TeaMaker(BeverageMaker):
    """Приготовление чая"""
   
    def brew(self):
        print("Заварите чайные листья в кипящей воде")
   
    def add_condiments(self):
        print("Добавьте лимон")

# Пример использования
if __name__ == "__main__":
    print("Приготовление кофе:")
    coffee = CoffeeMaker()
    coffee.make_beverage()
   
    print("\n" + "="*30 + "\n")
   
    print("Заваривание чая:")
    tea = TeaMaker()
    tea.make_beverage()

Варианты паттерна «Шаблонный метод»

1. Шаблонный метод с параметрами

Пример

class ConfigurableProcessor(ABC):
    """Настраиваемый обработчик данных"""
   
    def process_with_config(self, input_file, output_file, config):
        """Шаблонный метод с параметрами конфигурации"""
        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):
        """Проверка конфигурации - конкретный метод"""
        required_keys = ['format', 'encoding']
        for key in required_keys:
            if key not in config:
                raise ValueError(f"Отсутствует обязательный ключ в конфигурации: {key}")
   
    @abstractmethod
    def process_with_config_impl(self, data, config):
        """Реализация обработки данных с использованием конфигурации"""
        pass
   
    def post_process(self, config):
        """Постобработка - метод-хук"""
        if config.get('cleanup', False):
            print("Выполнение операций очистки")

2. Многошаговый шаблонный метод

Пример

class MultiStepProcessor(ABC):
    """Многошаговый процессор"""
   
    def complex_processing(self):
        """Поток сложной обработки"""
        self.initialize()
        self.pre_process()
        self.main_process()
        self.post_process()
        self.finalize()
   
    def initialize(self):
        print("Инициализация процессора")
   
    def pre_process(self):
        """Предварительная обработка - метод-хук"""
        pass
   
    @abstractmethod
    def main_process(self):
        """Основная логика обработки"""
        pass
   
    def post_process(self):
        """Постобработка - метод-хук"""
        pass
   
    def finalize(self):
        print("Обработка завершена")

Лучшие практики и рекомендации

1. Используйте абстрактные методы надлежащим образом

  • Объявляйте абстрактными только те методы, которые действительно должны быть реализованы подклассами
  • Предоставляйте методы-хуки с реализацией по умолчанию для необязательных шагов

2. Контроль доступа к шаблонным методам

  • Методы шаблона обычно следует объявлять какfinal(В Python это можно сделать с помощью соглашений об именовании)
  • Методы шагов должны быть защищёнными, чтобы избежать прямых внешних вызовов

3. Обработка ошибок

Пример

class RobustTemplate(ABC):
    """Надёжный класс шаблона"""
   
    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):
        """Последовательность шагов выполнения"""
        self.step1()
        self.step2()
        self.step3()
   
    def handle_error(self, error):
        """Обработка ошибок - метод-хук"""
        print(f"Во время обработки произошла ошибка: {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("Очистка ресурсов")

Практические упражнения

Теперь ваша очередь практиковаться! Выполните следующие упражнения, чтобы закрепить понимание паттерна «Шаблонный метод»:

Упражнение 1: Реализуйте шаблон экспорта файлов

Создайте шаблон экспорта файлов, поддерживающий экспорт в различные форматы (TXT, HTML, JSON).

Требования:

  • Определите абстрактный базовый классFileExporter
  • Реализуйте конкретные подклассыTXTExporter、HTMLExporter、JSONExporter
  • Шаблонный метод должен включать: подготовку данных, форматирование данных, сохранение файлов, постобработку и другие шаги.

Упражнение 2: Усовершенствуйте шаблон приготовления напитков

Расширьте предыдущий шаблон приготовления напитков и добавьте следующие возможности:

  • Поддержка выбора размера напитка (маленький, средний, большой)
  • Добавить функциональность расчёта цены
  • Поддержка настраиваемых приправ

Итог

Шаблонный метод — это мощный и практичный паттерн проектирования, который помогает нам создавать лучший код следующими способами:

Основные преимущества

  1. Повышение переиспользуемости кода: общий код размещается в родительском классе
  2. Повышение расширяемости: расширение отдельных шагов с помощью подклассов
  3. Соблюдение принципа открытости/закрытости: открыт для расширения, закрыт для изменения
  4. Инверсия управления: родительский класс управляет потоком, а подклассы реализуют детали

Применимые сценарии

  • Несколько классов имеют один и тот же метод, но конкретные реализации различаются
  • Необходимость контролировать точки расширения подклассов
  • Необходимость определить скелет алгоритма, но допускать изменение отдельных шагов

Ключевые моменты

  • Шаблонный метод определяет неизменяемые части алгоритма
  • Абстрактные методы определяют изменяемые части алгоритма
  • Методы-хуки предоставляют необязательные точки расширения
Другие расширения