Паттерн «Шаблонный метод» в Python
Паттерн «Шаблонный метод» — это поведенческий паттерн проектирования, который определяет скелет алгоритма в операции, делегируя некоторые шаги подклассам. Он позволяет подклассам переопределять определённые шаги алгоритма, не меняя структуру алгоритма.
Проще говоря, это похоже на кулинарный рецепт: рецепт задаёт порядок шагов (мытьё овощей, нарезка, обжарка, выкладывание на тарелку), но то, как именно мыть, нарезать и обжаривать, могут реализовать разные повара в соответствии со своим стилем.
Зачем нужен паттерн «Шаблонный метод»?
Повторное использование кода
Паттерн «Шаблонный метод» переносит неизменяемое поведение в суперкласс, избегая дублирования кода в подклассах. Несколько подклассов могут совместно использовать структуру алгоритма, определённую одним и тем же шаблонным методом.
Расширяемость
Подклассы могут расширять или изменять часть поведения алгоритма, переопределяя методы-хуки или конкретные методы шагов, не меняя общую структуру алгоритма.
Инверсия управления
Родительский класс управляет ходом выполнения алгоритма, а подклассам нужно сосредоточиться только на конкретных шагах, которые они должны реализовать, воплощая «голливудский принцип» — «Не звоните нам, мы сами позвоним вам».
Структура паттерна «Шаблонный метод»
Давайте разберём структуру паттерна «Шаблонный метод» с помощью диаграммы классов:

Описание основных компонентов
| Компонент | Ответственность | Описание |
|---|---|---|
| AbstractClass | Определяет скелет алгоритма | Содержит шаблонный метод и абстрактные методы шагов |
| ConcreteClass | Реализует конкретные шаги | Реализует абстрактные методы, определённые родительским классом |
| template_method | Шаблонный метод | Определяет инвариантные части алгоритма |
| step1, step2 | Абстрактный метод | Конкретные шаги, которые должны быть реализованы подклассами |
| хук | Метод-хук | Необязательный шаг; подклассы могут выбирать, переопределять ли его |
Базовый синтаксис и реализация
Определение абстрактного базового класса
Пример
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("Выполнить метод-хук - по умолчанию ничего не делает")
Реализация конкретных подклассов
Пример
"""Конкретная реализация класса 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: Шаблон обработки данных
Пример
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: Шаблон приготовления напитков
Пример
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. Шаблонный метод с параметрами
Пример
"""Настраиваемый обработчик данных"""
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. Многошаговый шаблонный метод
Пример
"""Многошаговый процессор"""
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. Обработка ошибок
Пример
"""Надёжный класс шаблона"""
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: Усовершенствуйте шаблон приготовления напитков
Расширьте предыдущий шаблон приготовления напитков и добавьте следующие возможности:
- Поддержка выбора размера напитка (маленький, средний, большой)
- Добавить функциональность расчёта цены
- Поддержка настраиваемых приправ
Итог
Шаблонный метод — это мощный и практичный паттерн проектирования, который помогает нам создавать лучший код следующими способами:
Основные преимущества
- Повышение переиспользуемости кода: общий код размещается в родительском классе
- Повышение расширяемости: расширение отдельных шагов с помощью подклассов
- Соблюдение принципа открытости/закрытости: открыт для расширения, закрыт для изменения
- Инверсия управления: родительский класс управляет потоком, а подклассы реализуют детали
Применимые сценарии
- Несколько классов имеют один и тот же метод, но конкретные реализации различаются
- Необходимость контролировать точки расширения подклассов
- Необходимость определить скелет алгоритма, но допускать изменение отдельных шагов
Ключевые моменты
- Шаблонный метод определяет неизменяемые части алгоритма
- Абстрактные методы определяют изменяемые части алгоритма
- Методы-хуки предоставляют необязательные точки расширения