Скелет алгоритма в базовом классе. Hooks для расширения поведения. ABC и @abstractmethod.
Template Method задаёт скелет. Hooks позволяют расширять без изменения.
Template Method — поведенческий паттерн, определяющий скелет алгоритма в базовом классе, позволяя подклассам переопределять шаги без изменения структуры.
# ❌ ПЛОХО: Дублирование структуры алгоритма
class CoffeeMaker:
def brew(self):
self.boil_water()
self.brew_coffee()
self.pour()
self.add_condiments()
def boil_water(self):
print("Boiling water")
def brew_coffee(self):
print("Brewing coffee")
def pour(self):
print("Pouring into cup")
def add_condiments(self):
print("Adding sugar and milk")
class TeaMaker:
def brew(self):
self.boil_water() # Дублирование
self.brew_tea()
self.pour()
self.add_condiments()
def boil_water(self):
print("Boiling water")
def brew_tea(self):
print("Steeping tea")
def pour(self):
print("Pouring into cup")
def add_condiments(self):
print("Adding lemon and honey")
# Дублирование boil_water() и pour()# ✅ ХОРОШО: Скелет в базовом классе
from abc import ABC, abstractmethod
class CaffeineBeverage(ABC):
"""Шаблонный метод"""
def prepare(self):
"""Скелет алгоритма — не переопределяется"""
self.boil_water()
self.brew()
self.pour()
self.add_condiments()
def boil_water(self):
print("Boiling water")
def pour(self):
print("Pouring into cup")
@abstractmethod
def brew(self):
pass
@abstractmethod
def add_condiments(self):
pass
class Coffee(CaffeineBeverage):
def brew(self):
print("Brewing coffee grounds")
def add_condiments(self):
print("Adding sugar and milk")
class Tea(CaffeineBeverage):
def brew(self):
print("Steeping tea bag")
def add_condiments(self):
print("Adding lemon and honey")
# Использование
coffee = Coffee()
coffee.prepare()
# Boiling water
# Brewing coffee grounds
# Pouring into cup
# Adding sugar and milk
tea = Tea()
tea.prepare()
# Boiling water
# Steeping tea bag
# Pouring into cup
# Adding lemon and honey# ✅ Hooks позволяют расширять поведение
from abc import ABC, abstractmethod
class DataProcessor(ABC):
def process(self, data: str):
"""Шаблонный метод с hooks"""
if self.validate_input(data):
data = self.preprocess(data)
result = self.transform(data)
result = self.postprocess(result)
self.save_result(result)
else:
print("Validation failed")
def validate_input(self, data: str) -> bool:
"""Hook — может быть переопределён"""
return True # По умолчанию всегда True
def preprocess(self, data: str) -> str:
"""Hook — может быть переопределён"""
return data
@abstractmethod
def transform(self, data: str) -> str:
pass
def postprocess(self, result: str) -> str:
"""Hook — может быть переопределён"""
return result
def save_result(self, result: str):
"""Hook — может быть переопределён"""
print(f"Saving: {result}")
class UpperCaseProcessor(DataProcessor):
def transform(self, data: str) -> str:
return data.upper()
class LoggingProcessor(DataProcessor):
def transform(self, data: str) -> str:
return data.upper()
def validate_input(self, data: str) -> bool:
print(f"Validating: {data}")
return len(data) > 0
def preprocess(self, data: str) -> str:
print(f"Preprocessing: {data}")
return data.strip()
def postprocess(self, result: str) -> str:
print(f"Postprocessing: {result}")
return f"***{result}***"
# Использование
proc = UpperCaseProcessor()
proc.process("hello")
# HELLO
# Saving: HELLO
proc2 = LoggingProcessor()
proc2.process(" hello ")
# Validating: hello
# Preprocessing: hello
# Postprocessing: HELLO
# Saving: ***HELLO***# ✅ Шаблон для API-клиентов
from abc import ABC, abstractmethod
import json
import requests
class APIClient(ABC):
def __init__(self, base_url: str):
self.base_url = base_url
def request(self, method: str, endpoint: str, data: dict = None):
"""Шаблонный метод"""
url = self._build_url(endpoint)
headers = self._prepare_headers()
if self._should_log():
self._log_request(method, url, data)
response = self._send_request(method, url, headers, data)
if self._should_cache():
self._cache_response(response)
return self._parse_response(response)
def _build_url(self, endpoint: str) -> str:
return f"{self.base_url}/{endpoint}"
def _prepare_headers(self) -> dict:
return {"Content-Type": "application/json"}
def _should_log(self) -> bool:
return True
def _log_request(self, method: str, url: str, data: dict):
print(f"{method} {url}: {data}")
@abstractmethod
def _send_request(self, method: str, url: str, headers: dict, data: dict):
pass
def _should_cache(self) -> bool:
return False
def _cache_response(self, response):
pass
@abstractmethod
def _parse_response(self, response):
pass
class RequestsClient(APIClient):
def _send_request(self, method: str, url: str, headers: dict, data: dict):
return requests.request(method, url, headers=headers, json=data)
def _parse_response(self, response):
return response.json()
class CachedClient(RequestsClient):
def __init__(self, base_url: str):
super().__init__(base_url)
self._cache = {}
def _should_cache(self) -> bool:
return True
def _cache_response(self, response):
key = response.request.url
self._cache[key] = response.json()
# Использование
client = RequestsClient("https://api.example.com")
response = client.request("GET", "users")# ✅ ETL с Template Method
from abc import ABC, abstractmethod
from typing import List, Any
class ETLPipeline(ABC):
def run(self, source: str, destination: str):
"""Шаблонный метод ETL"""
self._notify_start()
data = self.extract(source)
data = self.transform(data)
self.load(data, destination)
self._notify_complete()
def _notify_start(self):
print("ETL started")
def _notify_complete(self):
print("ETL completed")
@abstractmethod
def extract(self, source: str) -> List[Any]:
pass
@abstractmethod
def transform(self, data: List[Any]) -> List[Any]:
pass
@abstractmethod
def load(self, data: List[Any], destination: str):
pass
class CSVToDatabasePipeline(ETLPipeline):
def extract(self, source: str) -> List[dict]:
print(f"Reading CSV: {source}")
# Чтение CSV
return [{"id": 1, "name": "Alice"}]
def transform(self, data: List[dict]) -> List[dict]:
print("Transforming data")
# Трансформация
return data
def load(self, data: List[dict], destination: str):
print(f"Loading to DB: {destination}")
# Загрузка в БД
# Использование
pipeline = CSVToDatabasePipeline()
pipeline.run("input.csv", "postgresql://localhost/db")# ✅ Генератор отчётов
from abc import ABC, abstractmethod
from datetime import datetime
class ReportGenerator(ABC):
def generate(self, start_date: datetime, end_date: datetime):
"""Шаблонный метод"""
data = self.fetch_data(start_date, end_date)
grouped = self.group_data(data)
formatted = self.format_report(grouped)
if self.include_summary():
formatted = self.add_summary(formatted)
return formatted
@abstractmethod
def fetch_data(self, start: datetime, end: datetime) -> list:
pass
def group_data(self, data: list) -> dict:
"""Hook — может быть переопределён"""
return {"data": data}
@abstractmethod
def format_report(self, grouped: dict) -> str:
pass
def include_summary(self) -> bool:
"""Hook"""
return True
def add_summary(self, report: str) -> str:
"""Hook"""
return report + "\n\n--- Summary ---"
class SalesReport(ReportGenerator):
def fetch_data(self, start: datetime, end: datetime) -> list:
return [{"date": start, "amount": 100}]
def format_report(self, grouped: dict) -> str:
return f"Sales Report: {grouped}"
class PDFReport(ReportGenerator):
def fetch_data(self, start: datetime, end: datetime) -> list:
return []
def format_report(self, grouped: dict) -> str:
return f"%PDF-1.4\n{grouped}"
def include_summary(self) -> bool:
return False
# Использование
report = SalesReport()
result = report.generate(datetime(2024, 1, 1), datetime(2024, 1, 31))# ✅ Template Method в ABC
from collections.abc import Sequence
class MySequence(Sequence):
"""Нужно реализовать только __getitem__ и __len__"""
def __getitem__(self, index):
pass
def __len__(self):
pass
# Остальные методы (index, count, __contains__)
# реализованы через Template Method
# Использование
class MyList(MySequence):
def __init__(self, items):
self._items = items
def __getitem__(self, index):
return self._items[index]
def __len__(self):
return len(self._items)
ml = MyList([1, 2, 3])
print(2 in ml) # True — __contains__ реализован через __getitem__# ✅ Template Method в unittest
import unittest
class MyTest(unittest.TestCase):
"""unittest использует Template Method"""
def setUp(self):
"""Hook — вызывается перед каждым тестом"""
pass
def tearDown(self):
"""Hook — вызывается после каждого теста"""
pass
def test_something(self):
"""Тест"""
pass
# run() — шаблонный метод, который вызывает:
# setUp() → test_*() → tearDown()| Паттерн | Когда использовать | Pythonic-реализация |
|---|---|---|
| Template Method | Скелет алгоритма с варьируемыми шагами | ABC + final метод + abstract методы |
| Hooks | Точки расширения с реализацией по умолчанию | Методы с default implementation |
| ABC | Обязательные методы для подклассов | @abstractmethod |
Главный принцип: Template Method определяет скелет, подклассы заполняют детали через переопределение.
Изучите тему Visitor и Double Dispatch для операций над объектами без изменения классов.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.