Python模板方法模式:从入门到实战
前言
上一篇聊了策略模式,核心是"算法可以换着用"。这篇来讲模板方法模式,它俩经常被放在一起比较,因为都是处理"多种实现方式"的问题,但思路完全不同。
模板方法模式的核心思想是:算法的骨架是固定的,但某些步骤可以让子类自己去实现。打个比方,泡茶和泡咖啡的流程差不多------烧水、冲泡、倒进杯子、加调料,但具体冲泡什么、加什么调料是不一样的。模板方法就是把这个"流程"固定下来,具体细节让子类去填。
说实话,这个模式在 Python 里用得比 Java 少一些,因为 Python 有更灵活的方式来实现类似效果。但理解它还是很有必要的,毕竟很多框架源码里都能看到它的影子。
🏠个人主页:你的主页
文章目录
- Python模板方法模式:让代码骨架固定,细节自由发挥
- 一、模板方法模式概念
- 二、不使用模板方法模式的痛点
- 三、Python实现模板方法模式
- [四、模板方法 vs 策略模式](#四、模板方法 vs 策略模式)
- 五、实战:数据处理管道
- 六、使用场景与注意事项
- 七、总结
一、模板方法模式概念
模板方法模式是一种行为设计模式,说白了就是:父类定义做事的步骤,子类负责填充具体内容。
还是用泡饮料的例子,不管你泡茶还是泡咖啡,步骤都差不多:
1. 把水烧开
2. 用热水冲泡(茶叶 or 咖啡粉)
3. 把饮料倒进杯子
4. 加点调料(柠檬 or 糖和牛奶)
这个流程是固定的,但第2步和第4步的具体内容不一样。模板方法模式就是把这个流程定义在父类里,然后让子类去实现那些"不一样"的部分。
模板方法模式通常包含这几个角色:
抽象类 :定义算法骨架,包含模板方法和一些抽象方法
模板方法 :定义算法的步骤顺序,通常是 final 的(Python 里没有 final,靠约定)
抽象方法 :由子类实现的具体步骤
钩子方法:可选的,子类可以选择性重写,通常有默认实现
钩子方法这个概念挺有意思的,它给子类提供了一个"插手"的机会,但不是强制的。比如"要不要加调料"这个决定,可以做成钩子方法让子类自己决定。
二、不使用模板方法模式的痛点
假设我们要做一个数据导出功能,支持导出 CSV、JSON、Excel 格式。不用模板方法的话,可能会写成这样:
python
class CSVExporter:
def export(self, data, filename):
# 1. 验证数据
if not data:
raise ValueError("数据不能为空")
print("验证数据通过")
# 2. 预处理数据
processed = []
for item in data:
processed.append({k: str(v) for k, v in item.items()})
print("数据预处理完成")
# 3. 转换格式(CSV特有逻辑)
import csv
with open(f"{filename}.csv", 'w', newline='', encoding='utf-8') as f:
if processed:
writer = csv.DictWriter(f, fieldnames=processed[0].keys())
writer.writeheader()
writer.writerows(processed)
print("CSV文件生成完成")
# 4. 后处理
print(f"导出完成:{filename}.csv")
return f"{filename}.csv"
class JSONExporter:
def export(self, data, filename):
# 1. 验证数据(重复代码)
if not data:
raise ValueError("数据不能为空")
print("验证数据通过")
# 2. 预处理数据(重复代码)
processed = []
for item in data:
processed.append({k: str(v) for k, v in item.items()})
print("数据预处理完成")
# 3. 转换格式(JSON特有逻辑)
import json
with open(f"{filename}.json", 'w', encoding='utf-8') as f:
json.dump(processed, f, ensure_ascii=False, indent=2)
print("JSON文件生成完成")
# 4. 后处理(重复代码)
print(f"导出完成:{filename}.json")
return f"{filename}.json"
class ExcelExporter:
def export(self, data, filename):
# 又是一堆重复的验证、预处理代码...
# 只有转换格式那部分不一样
pass
问题很明显:
大量重复代码。验证数据、预处理、后处理的逻辑几乎一模一样,复制粘贴了好几遍。哪天要改验证逻辑,得改好几个地方。
流程不统一。万一某个开发者写 XMLExporter 的时候忘了验证数据呢?或者打乱了步骤顺序?没有一个强制的流程约束。
维护成本高。假设要加一个"记录日志"的步骤,你得改所有 Exporter 类。
这种情况就很适合用模板方法模式来重构。
三、Python实现模板方法模式
3.1 经典的抽象基类实现
最标准的实现方式,用 ABC 定义抽象类:
python
from abc import ABC, abstractmethod
from typing import List, Dict, Any
class DataExporter(ABC):
"""数据导出器的抽象基类"""
def export(self, data: List[Dict], filename: str) -> str:
"""
模板方法:定义导出流程
这个方法定义了算法骨架,子类不应该重写它
"""
# 步骤1:验证
self._validate(data)
# 步骤2:预处理
processed_data = self._preprocess(data)
# 步骤3:钩子 - 导出前的准备(可选)
if self._before_export():
print("执行导出前的准备工作...")
# 步骤4:实际导出(子类必须实现)
result = self._do_export(processed_data, filename)
# 步骤5:后处理
self._postprocess(result)
return result
def _validate(self, data: List[Dict]) -> None:
"""验证数据 - 通用逻辑,子类一般不用重写"""
if not data:
raise ValueError("导出数据不能为空")
if not isinstance(data, list):
raise TypeError("数据必须是列表类型")
print(f"✓ 数据验证通过,共 {len(data)} 条记录")
def _preprocess(self, data: List[Dict]) -> List[Dict]:
"""预处理数据 - 通用逻辑,子类可以重写"""
# 默认实现:把所有值转成字符串
processed = []
for item in data:
processed.append({k: str(v) if v is not None else '' for k, v in item.items()})
print("✓ 数据预处理完成")
return processed
def _before_export(self) -> bool:
"""
钩子方法:导出前的准备
返回 True 表示需要执行准备工作,默认不需要
子类可以重写这个方法
"""
return False
@abstractmethod
def _do_export(self, data: List[Dict], filename: str) -> str:
"""
抽象方法:实际的导出逻辑
这是每种格式不一样的地方,子类必须实现
"""
pass
def _postprocess(self, filepath: str) -> None:
"""后处理 - 通用逻辑"""
print(f"✓ 导出完成:{filepath}")
class CSVExporter(DataExporter):
"""CSV导出器"""
def _do_export(self, data: List[Dict], filename: str) -> str:
import csv
filepath = f"{filename}.csv"
with open(filepath, 'w', newline='', encoding='utf-8') as f:
if data:
writer = csv.DictWriter(f, fieldnames=data[0].keys())
writer.writeheader()
writer.writerows(data)
print(f" → CSV格式转换完成")
return filepath
class JSONExporter(DataExporter):
"""JSON导出器"""
def __init__(self, pretty: bool = True):
self.pretty = pretty
def _do_export(self, data: List[Dict], filename: str) -> str:
import json
filepath = f"{filename}.json"
indent = 2 if self.pretty else None
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=indent)
print(f" → JSON格式转换完成")
return filepath
class ExcelExporter(DataExporter):
"""Excel导出器"""
def _before_export(self) -> bool:
"""Excel导出前检查 openpyxl 是否安装"""
try:
import openpyxl
return True
except ImportError:
print(" ! 警告:openpyxl 未安装,将使用 csv 作为备选")
return False
def _do_export(self, data: List[Dict], filename: str) -> str:
try:
import openpyxl
from openpyxl import Workbook
filepath = f"{filename}.xlsx"
wb = Workbook()
ws = wb.active
# 写表头
if data:
headers = list(data[0].keys())
ws.append(headers)
# 写数据
for item in data:
ws.append([item.get(h, '') for h in headers])
wb.save(filepath)
print(f" → Excel格式转换完成")
return filepath
except ImportError:
# 降级为CSV
return CSVExporter()._do_export(data, filename)
使用起来很简单:
python
# 测试数据
data = [
{"name": "张三", "age": 25, "city": "北京"},
{"name": "李四", "age": 30, "city": "上海"},
{"name": "王五", "age": 28, "city": "广州"},
]
# 用不同的导出器,流程完全一样
csv_exporter = CSVExporter()
csv_exporter.export(data, "users")
json_exporter = JSONExporter(pretty=True)
json_exporter.export(data, "users")
excel_exporter = ExcelExporter()
excel_exporter.export(data, "users")
输出类似这样:
✓ 数据验证通过,共 3 条记录
✓ 数据预处理完成
→ CSV格式转换完成
✓ 导出完成:users.csv
这样一来,流程被固定住了,想加新的导出格式只需要继承 DataExporter 并实现 _do_export 方法就行。
3.2 使用 __init_subclass__ 的现代写法
Python 3.6+ 提供了一个挺酷的特性,可以在子类创建时做一些检查:
python
class DataProcessor:
"""使用 __init_subclass__ 的模板类"""
required_methods = ['process_item']
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
# 检查子类是否实现了必要的方法
for method in cls.required_methods:
if not callable(getattr(cls, method, None)):
raise TypeError(f"子类 {cls.__name__} 必须实现 {method} 方法")
def process(self, items: list) -> list:
"""模板方法"""
results = []
for item in items:
# 前置处理
prepared = self.prepare_item(item)
# 核心处理(子类实现)
processed = self.process_item(prepared)
# 后置处理
final = self.finalize_item(processed)
results.append(final)
return results
def prepare_item(self, item):
"""默认的前置处理"""
return item
def process_item(self, item):
"""子类必须实现"""
raise NotImplementedError
def finalize_item(self, item):
"""默认的后置处理"""
return item
# 正确的子类
class UpperCaseProcessor(DataProcessor):
def process_item(self, item):
return item.upper() if isinstance(item, str) else str(item).upper()
# 这个会报错,因为没实现 process_item
# class BrokenProcessor(DataProcessor):
# pass # TypeError: 子类 BrokenProcessor 必须实现 process_item 方法
这种方式比 ABC 的好处是:错误在类定义时就能发现,而不是等到调用时才报错。
3.3 函数式的替代方案
有时候觉得为了一个流程专门搞继承体系太重了,可以用高阶函数来实现类似效果:
python
from typing import Callable, List, Any, TypeVar
T = TypeVar('T')
def create_pipeline(
validate: Callable[[List[T]], None] = None,
preprocess: Callable[[List[T]], List[T]] = None,
process: Callable[[List[T]], Any] = None,
postprocess: Callable[[Any], Any] = None
) -> Callable[[List[T]], Any]:
"""
创建一个处理管道
这其实是模板方法的函数式实现
"""
def default_validate(data):
if not data:
raise ValueError("数据不能为空")
def default_preprocess(data):
return data
def default_postprocess(result):
return result
# 使用默认值
_validate = validate or default_validate
_preprocess = preprocess or default_preprocess
_postprocess = postprocess or default_postprocess
def pipeline(data: List[T]) -> Any:
_validate(data)
processed = _preprocess(data)
if process is None:
raise ValueError("必须提供 process 函数")
result = process(processed)
return _postprocess(result)
return pipeline
# 使用
def to_json(data):
import json
return json.dumps(data, ensure_ascii=False)
def add_timestamp(result):
from datetime import datetime
return {"data": result, "timestamp": datetime.now().isoformat()}
# 创建一个JSON导出管道
json_pipeline = create_pipeline(
process=to_json,
postprocess=add_timestamp
)
result = json_pipeline([{"name": "test"}])
print(result)
这种方式更加灵活,适合那种步骤简单、不需要复杂继承关系的场景。
四、模板方法 vs 策略模式
这俩模式经常被拿来比较,因为它们都是处理"变化"的。但思路完全不一样:
4.1 核心区别
| 方面 | 模板方法模式 | 策略模式 |
|---|---|---|
| 关注点 | 算法的骨架(流程) | 算法的整体替换 |
| 实现方式 | 继承 | 组合 |
| 变化的粒度 | 算法中的某些步骤 | 整个算法 |
| 控制反转 | 父类控制流程,子类实现细节 | 客户端选择策略 |
| 扩展方式 | 继承父类 | 实现策略接口 |
打个比方:
模板方法像是填空题,框架给你出好了,你只需要填几个空。比如"做菜的步骤是:洗菜→切菜→___→装盘",你只需要填"炒"还是"蒸"还是"煮"。
策略模式像是选择题,有几个完整的选项让你选。比如"去公司的方式:A.开车 B.地铁 C.骑车",每个选项都是完整的方案。
4.2 选择建议
用模板方法的场景:
- 多个类有相同的算法流程,只是某些步骤不同
- 需要控制子类的扩展点,不让子类乱来
- 公共逻辑比较多,私有逻辑比较少
用策略模式的场景:
- 算法之间差异很大,没什么公共流程
- 需要在运行时动态切换算法
- 不想用继承,更喜欢组合
4.3 两者配合使用
很多时候这俩是可以一起用的。比如一个数据处理框架:
python
from abc import ABC, abstractmethod
from typing import List, Dict, Any, Callable
# 策略接口:定义不同的处理策略
class ProcessStrategy(ABC):
@abstractmethod
def process(self, item: Dict) -> Dict:
pass
class CleanStrategy(ProcessStrategy):
"""数据清洗策略"""
def process(self, item: Dict) -> Dict:
return {k: v.strip() if isinstance(v, str) else v for k, v in item.items()}
class EnrichStrategy(ProcessStrategy):
"""数据增强策略"""
def process(self, item: Dict) -> Dict:
item['processed'] = True
item['source'] = 'pipeline'
return item
# 模板方法:定义处理流程
class DataPipeline(ABC):
"""
数据处理管道
模板方法定义流程,策略模式处理具体逻辑
"""
def __init__(self, strategy: ProcessStrategy = None):
self.strategy = strategy or CleanStrategy()
def set_strategy(self, strategy: ProcessStrategy):
"""运行时切换策略"""
self.strategy = strategy
def run(self, data: List[Dict]) -> List[Dict]:
"""模板方法:定义管道流程"""
# 1. 加载数据(子类实现)
loaded = self._load(data)
# 2. 验证
self._validate(loaded)
# 3. 处理每条数据(使用策略)
processed = []
for item in loaded:
result = self.strategy.process(item)
processed.append(result)
# 4. 保存(子类实现)
self._save(processed)
return processed
def _validate(self, data: List[Dict]):
"""通用验证逻辑"""
if not data:
raise ValueError("数据为空")
print(f"验证通过,共 {len(data)} 条")
@abstractmethod
def _load(self, data: List[Dict]) -> List[Dict]:
"""加载数据 - 子类实现"""
pass
@abstractmethod
def _save(self, data: List[Dict]) -> None:
"""保存数据 - 子类实现"""
pass
class FilePipeline(DataPipeline):
"""文件处理管道"""
def __init__(self, output_file: str, strategy: ProcessStrategy = None):
super().__init__(strategy)
self.output_file = output_file
def _load(self, data: List[Dict]) -> List[Dict]:
print("从内存加载数据...")
return data
def _save(self, data: List[Dict]) -> None:
import json
with open(self.output_file, 'w') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
print(f"数据已保存到 {self.output_file}")
class DatabasePipeline(DataPipeline):
"""数据库处理管道"""
def __init__(self, table_name: str, strategy: ProcessStrategy = None):
super().__init__(strategy)
self.table_name = table_name
def _load(self, data: List[Dict]) -> List[Dict]:
print(f"准备写入表 {self.table_name}...")
return data
def _save(self, data: List[Dict]) -> None:
# 模拟数据库操作
print(f"已将 {len(data)} 条数据写入 {self.table_name}")
使用:
python
data = [
{"name": " 张三 ", "age": 25},
{"name": "李四", "age": 30},
]
# 文件管道 + 清洗策略
file_pipeline = FilePipeline("output.json", strategy=CleanStrategy())
file_pipeline.run(data)
# 同一个管道,换个策略
file_pipeline.set_strategy(EnrichStrategy())
file_pipeline.run(data)
# 数据库管道
db_pipeline = DatabasePipeline("users", strategy=EnrichStrategy())
db_pipeline.run(data)
这样,模板方法 控制了"加载→验证→处理→保存"的流程,策略模式控制了"处理"这一步的具体逻辑。两者各司其职,代码既有章法又灵活。
五、实战:数据处理管道
来个更完整的例子,做一个通用的数据爬取和处理框架。这种场景特别适合模板方法,因为爬虫的流程基本是固定的。
5.1 定义爬虫基类
python
from abc import ABC, abstractmethod
from typing import List, Dict, Any, Optional
from dataclasses import dataclass, field
from datetime import datetime
import time
import random
@dataclass
class CrawlResult:
"""爬取结果"""
success: bool
data: List[Dict[str, Any]] = field(default_factory=list)
error: Optional[str] = None
stats: Dict[str, Any] = field(default_factory=dict)
class BaseCrawler(ABC):
"""
爬虫基类 - 模板方法模式
定义了爬取流程:初始化 → 获取URL列表 → 爬取 → 解析 → 存储 → 清理
"""
def __init__(self, name: str = "BaseCrawler"):
self.name = name
self.stats = {
"start_time": None,
"end_time": None,
"total_urls": 0,
"success_count": 0,
"fail_count": 0,
}
def crawl(self) -> CrawlResult:
"""
模板方法:定义爬虫流程
子类不应该重写这个方法
"""
all_data = []
try:
# 1. 初始化
print(f"\n{'='*50}")
print(f"🚀 爬虫 [{self.name}] 开始运行")
print(f"{'='*50}")
self.stats["start_time"] = datetime.now()
self._initialize()
# 2. 获取要爬取的URL列表
urls = self._get_urls()
self.stats["total_urls"] = len(urls)
print(f"📋 获取到 {len(urls)} 个URL")
# 3. 逐个爬取和解析
for i, url in enumerate(urls, 1):
print(f"\n[{i}/{len(urls)}] 正在处理: {url}")
try:
# 请求前的钩子
if self._before_request(url):
# 发起请求
response = self._fetch(url)
# 解析数据
data = self._parse(response)
if data:
all_data.extend(data if isinstance(data, list) else [data])
self.stats["success_count"] += 1
print(f" ✓ 解析成功,获取 {len(data) if isinstance(data, list) else 1} 条数据")
# 请求后的钩子
self._after_request(url, response)
# 礼貌性延迟
self._delay()
except Exception as e:
self.stats["fail_count"] += 1
print(f" ✗ 处理失败: {e}")
self._on_error(url, e)
# 4. 存储数据
if all_data:
self._save(all_data)
# 5. 清理工作
self._cleanup()
self.stats["end_time"] = datetime.now()
duration = (self.stats["end_time"] - self.stats["start_time"]).total_seconds()
print(f"\n{'='*50}")
print(f"✅ 爬虫完成!耗时 {duration:.2f} 秒")
print(f" 成功: {self.stats['success_count']}, 失败: {self.stats['fail_count']}")
print(f" 共获取 {len(all_data)} 条数据")
print(f"{'='*50}\n")
return CrawlResult(
success=True,
data=all_data,
stats=self.stats.copy()
)
except Exception as e:
return CrawlResult(
success=False,
error=str(e),
stats=self.stats.copy()
)
# ============ 子类必须实现的方法 ============
@abstractmethod
def _get_urls(self) -> List[str]:
"""获取要爬取的URL列表 - 子类必须实现"""
pass
@abstractmethod
def _parse(self, response: Any) -> List[Dict[str, Any]]:
"""解析响应数据 - 子类必须实现"""
pass
# ============ 可选重写的方法(钩子) ============
def _initialize(self) -> None:
"""初始化 - 可选重写"""
print("📦 初始化...")
def _fetch(self, url: str) -> Any:
"""
发起请求 - 默认实现
子类可以重写以使用不同的请求方式
"""
# 这里简单模拟,实际项目中用 requests 或 aiohttp
print(f" → 请求: {url}")
time.sleep(0.1) # 模拟网络延迟
return {"url": url, "html": f"<html>Mock content for {url}</html>"}
def _save(self, data: List[Dict[str, Any]]) -> None:
"""存储数据 - 默认打印,子类可重写"""
print(f"\n💾 保存 {len(data)} 条数据...")
for item in data[:3]: # 只打印前3条
print(f" {item}")
if len(data) > 3:
print(f" ... 还有 {len(data) - 3} 条")
def _cleanup(self) -> None:
"""清理工作 - 可选重写"""
print("🧹 清理资源...")
def _before_request(self, url: str) -> bool:
"""请求前的钩子,返回False可跳过该URL"""
return True
def _after_request(self, url: str, response: Any) -> None:
"""请求后的钩子"""
pass
def _on_error(self, url: str, error: Exception) -> None:
"""错误处理钩子"""
pass
def _delay(self) -> None:
"""请求间隔 - 默认随机延迟"""
delay = random.uniform(0.5, 1.5)
time.sleep(delay)
5.2 实现具体爬虫
python
import json
from typing import List, Dict, Any
class NewsCrawler(BaseCrawler):
"""新闻爬虫"""
def __init__(self, category: str = "tech"):
super().__init__(name=f"NewsCrawler-{category}")
self.category = category
self.output_file = f"news_{category}.json"
def _get_urls(self) -> List[str]:
"""生成新闻URL列表"""
# 实际项目中这里可能是从种子页面解析出来的
base_url = f"https://news.example.com/{self.category}"
return [f"{base_url}/page/{i}" for i in range(1, 4)]
def _parse(self, response: Any) -> List[Dict[str, Any]]:
"""解析新闻数据"""
# 实际项目中这里用 BeautifulSoup 或 lxml 解析
# 这里模拟解析结果
url = response.get("url", "")
return [
{
"title": f"新闻标题 - 来自 {url}",
"category": self.category,
"publish_time": datetime.now().isoformat(),
"source_url": url,
}
]
def _save(self, data: List[Dict[str, Any]]) -> None:
"""保存到JSON文件"""
with open(self.output_file, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
print(f"💾 数据已保存到 {self.output_file}")
def _before_request(self, url: str) -> bool:
"""过滤某些URL"""
# 比如跳过广告页面
if "ad" in url or "sponsor" in url:
print(f" ⏭ 跳过广告页: {url}")
return False
return True
class ProductCrawler(BaseCrawler):
"""商品爬虫"""
def __init__(self, keywords: List[str]):
super().__init__(name="ProductCrawler")
self.keywords = keywords
self.products = []
def _initialize(self) -> None:
"""初始化 - 可以在这里登录啥的"""
print("📦 初始化商品爬虫...")
print(f" 关键词: {', '.join(self.keywords)}")
# 实际项目中这里可能要初始化session、登录等
def _get_urls(self) -> List[str]:
"""根据关键词生成搜索URL"""
urls = []
for keyword in self.keywords:
urls.append(f"https://shop.example.com/search?q={keyword}")
return urls
def _parse(self, response: Any) -> List[Dict[str, Any]]:
"""解析商品列表"""
# 模拟解析
return [
{"name": "商品A", "price": 99.9, "url": response["url"]},
{"name": "商品B", "price": 199.9, "url": response["url"]},
]
def _save(self, data: List[Dict[str, Any]]) -> None:
"""保存到数据库(这里模拟)"""
print(f"💾 准备写入数据库...")
for item in data:
print(f" INSERT INTO products VALUES ('{item['name']}', {item['price']})")
def _on_error(self, url: str, error: Exception) -> None:
"""错误处理 - 可以发告警"""
print(f" ⚠ 错误已记录,稍后重试: {url}")
# 实际项目中这里可以发邮件、写日志、加入重试队列等
5.3 运行爬虫
python
# 运行新闻爬虫
news_crawler = NewsCrawler(category="tech")
result = news_crawler.crawl()
print(f"爬取结果: 成功={result.success}, 数据条数={len(result.data)}")
# 运行商品爬虫
product_crawler = ProductCrawler(keywords=["手机", "耳机", "充电器"])
result = product_crawler.crawl()
这个例子展示了模板方法的威力:流程被固定住了(初始化→获取URL→爬取→解析→保存→清理),但每个爬虫可以有自己的实现。想加新爬虫只需要继承 BaseCrawler,实现 _get_urls 和 _parse 两个方法就行。
六、使用场景与注意事项
6.1 适用场景
模板方法模式特别适合这些情况:
有固定流程但步骤实现不同。比如数据导入导出、文档生成、测试框架的 setup/teardown。
想限制子类的扩展点。只让子类重写特定方法,其他地方不能动。这在框架设计里很常见。
公共代码太多重复了。把公共逻辑放父类,差异化的部分让子类实现。
多个子类有相同的行为序列。比如不同类型的报表生成,都是"收集数据→处理数据→生成图表→导出文件"。
6.2 不适合的场景
当然也有不适合的时候:
如果算法之间差异太大,根本没什么公共流程,那强行抽模板反而别扭。这时候用策略模式更合适。
如果继承层级太深,或者子类太多,维护起来会头疼。Python 社区一般更推荐组合而非继承。
如果步骤需要动态组合,比如今天要A→B→C,明天要B→C→A,模板方法就搞不定了,得用其他模式(比如责任链)。
6.3 Python 特有的注意事项
关于"私有"方法 :Python 没有真正的私有方法,单下划线 _method 只是约定。如果担心子类乱重写,可以用双下划线 __method,但这样也有名称改写(name mangling)的问题,用之前想清楚。
钩子方法要有默认实现:让钩子方法返回合理的默认值,这样子类不重写也能正常工作。
考虑用 __init_subclass__:Python 3.6+ 可以在子类定义时做检查,比等到运行时报错体验好。
能用组合就别用继承:Python 社区有句话叫"扁平胜于嵌套"。如果继承层级超过2-3层,考虑换种实现方式。
6.4 与其他模式的关系
与工厂模式配合:工厂方法其实就是模板方法的一种特例,用来创建对象。
与策略模式互补:模板方法用继承改变部分实现,策略模式用组合改变整体实现。前面已经演示过两者配合使用了。
与钩子概念:模板方法里的钩子方法,本质上是给子类提供的扩展点。很多框架(Django、Flask)的中间件机制都有这个影子。
七、总结
模板方法模式的核心就一句话:父类定流程,子类填细节。这种"控制反转"在框架设计里特别常见,你用过 Django 的类视图、unittest 的 TestCase,其实都在用这个模式。
相比策略模式,模板方法更强调流程的统一性。策略模式是"我有几种方案你选一个",模板方法是"做事步骤就这些,具体怎么做你来定"。两者经常配合使用,模板方法控制大流程,策略模式处理某个步骤的变化,代码写起来会很清爽。
Python 实现模板方法虽然没有 Java 那么"仪式感",但灵活性更高。可以用 ABC 正经做,也可以用函数式的方式替代。关键是理解它的思想------把变化封装在特定的地方,让扩展变得可控。
最后提醒一句,设计模式是工具不是目的。如果你的场景简单,if-else 写几行就完事了,没必要非得套模式。模板方法最适合那种"流程固定、步骤多、变化点明确"的场景,用对地方才能发挥它的价值。
热门专栏推荐
- Agent小册
- 服务器部署
- Java基础合集
- Python基础合集
- Go基础合集
- 大数据合集
- 前端小册
- 数据库合集
- Redis 合集
- Spring 全家桶
- 微服务全家桶
- 数据结构与算法合集
- 设计模式小册
- 消息队列合集
等等等还有许多优秀的合集在主页等着大家的光顾,感谢大家的支持
文章到这里就结束了,如果有什么疑问的地方请指出,诸佬们一起来评论区一起讨论
希望能和诸佬们一起努力,今后我们一起观看感谢您的阅读
如果帮助到您不妨3连支持一下,创造不易您们的支持是我的动力