Python 设计模式——策略模式(Strategy Pattern)详解
本文面向零基础新手,从概念到“算法可替换”,从支付方式到排序比较器,全方位讲解策略模式。
一、什么是策略模式?
1.1 通俗理解
策略模式是一种行为型设计模式,核心思想是:
把“做同一件事的多种做法”(多种算法、多种策略)抽象成统一接口,每种做法是一个独立的“策略类”;使用方(上下文)不直接写死某一种做法,而是持有一个“策略”对象,把实际工作委托给策略。这样要换一种做法时,只需换一个策略对象,不必改使用方代码,也避免了满屏的 if-else 分支。
可以这样类比:
- 不用策略:计算从 A 到 B 的路线,代码里写
if 选的是开车 then 按开车算,elif 选的是步行 then 按步行算,elif 选的是公交 then ...,每加一种出行方式就要改这段逻辑。 - 用策略:定义“路线策略”接口(计算路线),有“开车策略”“步行策略”“公交策略”三个实现;使用方只持有一个“当前策略”,算路线时调用 strategy.calculate(A, B)。要换出行方式就 context.set_strategy(公交策略),使用方代码不变。算法可插拔、可替换。
所以,策略模式解决的是:同一类问题有多种解法,希望把每种解法封装成可替换的“策略”,让使用方依赖抽象策略而非具体实现,便于扩展和切换。
1.2 为什么需要策略模式?
场景一:多种算法可互换
例如支付:支付宝、微信、银行卡,都是“支付”这一件事的不同实现;下单时根据用户选择用哪一种,若写 if 支付方式==支付宝 then … elif 微信 then …,代码臃肿且难加新方式。用策略:每种支付方式是一个策略类,下单时注入对应策略,加新支付方式只需加新策略类。
场景二:避免复杂分支
同一操作在不同条件下要不同处理(如不同用户等级用不同折扣),若全塞进一个函数里会充满 if-else。用策略:每种“折扣算法”一个策略,根据用户等级选一个策略注入,逻辑清晰、易测试。
场景三:运行时切换行为
例如编辑器“保存”可以是“保存到本地”“保存到云”;用户可随时切换,使用方只调 strategy.save(doc),换策略就换行为,符合开闭原则。
1.3 适用场景(什么时候用?)
| 场景 | 说明 |
|---|---|
| 同一问题多种算法/做法 | 支付、折扣、排序规则、路线计算等,希望可互换。 |
| 想避免大量 if-else | 用多态代替“根据类型分支”。 |
| 算法要独立于使用方 | 使用方只依赖策略接口,具体算法可单独测试、替换。 |
| 运行时需切换行为 | 用户选择、配置决定用哪种策略。 |
简单记忆:多种做法可互换、希望使用方不写死某一种、要便于扩展和切换时,用策略模式。
二、策略模式的结构(三个角色)
| 角色 | 说明 |
|---|---|
| 上下文(Context) | 使用策略的一方;持有一个策略对象(接口类型),在需要执行“那件事”时调用 strategy.xxx(…),不关心具体是哪种实现。可提供 set_strategy(strategy) 以便切换策略。 |
| 策略(Strategy) | 抽象接口,定义“做那件事”的方法(如 pay、calculate、execute)。 |
| 具体策略(ConcreteStrategy) | 实现策略接口,每种实现代表一种算法/做法。 |
关系可以理解为:
- 客户或上下文在适当时机选择并注入一个具体策略(如根据用户选择、配置)。
- 上下文在执行时只调 self._strategy.xxx(),不写 if 是哪种策略;换策略就换行为。
三、示例一:支付方式(最经典)
需求:下单时要支付,支付方式有支付宝、微信、银行卡;根据用户选择使用不同方式,且便于以后加新方式。
3.1 策略接口 + 具体策略
from abc import ABC, abstractmethod
class PayStrategy(ABC):
@abstractmethod
def pay(self, amount: float) -> bool:
pass
class AlipayStrategy(PayStrategy):
def pay(self, amount: float) -> bool:
print(f" [支付宝] 支付 {amount} 元")
return True
class WechatPayStrategy(PayStrategy):
def pay(self, amount: float) -> bool:
print(f" [微信] 支付 {amount} 元")
return True
class BankCardStrategy(PayStrategy):
def pay(self, amount: float) -> bool:
print(f" [银行卡] 支付 {amount} 元")
return True
3.2 上下文:订单(持有策略,支付时委托)
class Order:
def __init__(self):
self._pay_strategy = None
def set_pay_strategy(self, strategy: PayStrategy):
self._pay_strategy = strategy
def do_pay(self, amount: float) -> bool:
if not self._pay_strategy:
print(" 未选择支付方式")
return False
return self._pay_strategy.pay(amount)
3.3 使用
order = Order()
order.set_pay_strategy(AlipayStrategy())
order.do_pay(99.5) # [支付宝] 支付 99.5 元
order.set_pay_strategy(WechatPayStrategy())
order.do_pay(50.0) # [微信] 支付 50.0 元
要点:Order 不写 if 支付宝/微信/银行卡,只调 self._pay_strategy.pay(amount);换支付方式只换策略对象。
四、示例二:折扣策略(不同用户等级不同折扣)
需求:计算订单应付金额时,普通用户无折扣,VIP 九折,SVIP 八折。把“折扣算法”做成策略,根据用户等级选一个策略。
class DiscountStrategy(ABC):
@abstractmethod
def discount(self, amount: float) -> float:
"""返回折后金额"""
pass
class NoDiscount(DiscountStrategy):
def discount(self, amount: float) -> float:
return amount
class VIPDiscount(DiscountStrategy):
def discount(self, amount: float) -> float:
return amount * 0.9
class SVIPDiscount(DiscountStrategy):
def discount(self, amount: float) -> float:
return amount * 0.8
class PriceCalculator:
def __init__(self, strategy: DiscountStrategy):
self._strategy = strategy
def set_strategy(self, strategy: DiscountStrategy):
self._strategy = strategy
def final_price(self, amount: float) -> float:
return self._strategy.discount(amount)
# 使用
calc = PriceCalculator(VIPDiscount())
print(calc.final_price(100)) # 90.0
calc.set_strategy(SVIPDiscount())
print(calc.final_price(100)) # 80.0
五、示例三:路线计算(不同出行方式)
需求:计算从 A 到 B 的路线(或耗时、距离);开车、步行、公交有不同的计算方式。每种方式一个策略。
class RouteStrategy(ABC):
@abstractmethod
def calculate(self, from_place: str, to_place: str) -> str:
pass
class CarRoute(RouteStrategy):
def calculate(self, from_place: str, to_place: str) -> str:
return f" 驾车: {from_place} -> {to_place}, 约 20 分钟"
class WalkRoute(RouteStrategy):
def calculate(self, from_place: str, to_place: str) -> str:
return f" 步行: {from_place} -> {to_place}, 约 50 分钟"
class TransitRoute(RouteStrategy):
def calculate(self, from_place: str, to_place: str) -> str:
return f" 公交: {from_place} -> {to_place}, 约 35 分钟"
class Navigator:
def __init__(self, strategy: RouteStrategy):
self._strategy = strategy
def set_strategy(self, strategy: RouteStrategy):
self._strategy = strategy
def route(self, from_place: str, to_place: str) -> str:
return self._strategy.calculate(from_place, to_place)
# 使用
nav = Navigator(CarRoute())
print(nav.route("家", "公司"))
nav.set_strategy(TransitRoute())
print(nav.route("家", "公司"))
六、示例四:排序 / 比较策略(Python 的 key 与 cmp)
Python 内置的 sorted(lst, key=…) 就是一种“策略”思想:比较/排序规则通过 key 传入,可随时换 key 就换排序结果。若自己实现一个“可配置比较规则”的排序器,可以这样用策略封装:
class CompareStrategy(ABC):
@abstractmethod
def compare(self, a, b) -> int:
"""<0 表示 a<b, 0 表示 a==b, >0 表示 a>b"""
pass
class IntAscStrategy(CompareStrategy):
def compare(self, a, b) -> int:
return a - b
class IntDescStrategy(CompareStrategy):
def compare(self, a, b) -> int:
return b - a
def sort_by_strategy(arr: list, strategy: CompareStrategy) -> list:
return sorted(arr, key=lambda x: (strategy.compare(x, x), x)) # 简化:这里仅示意
# 更规范的做法是写一个 sort 函数内部用 strategy.compare 做两两比较
实际项目中通常直接用 sorted(lst, key=…) 或 functools.cmp_to_key,这里仅说明“比较规则可替换”即策略思想。
七、策略模式 vs 其他模式
| 模式 | 目的 | 与策略的区别 |
|---|---|---|
| 策略 | 算法/行为可替换,使用方委托给策略 | 策略由客户/上下文选择注入,一般不“自己切换”。 |
| 状态 | 行为随状态变,状态类可切换上下文的状态 | 状态会在处理中主动把上下文切到另一状态;策略通常不改变上下文。 |
| 工厂 | 根据类型创建对象 | 工厂负责“造哪个”;策略负责“用哪个算法”。可结合:用工厂造出策略再注入上下文。 |
简单记忆:策略 = 多种算法封装成可替换对象 + 使用方委托策略 + 换策略即换行为。
八、常见问题与注意点
8.1 策略要不要在运行时切换?
可以。上下文提供 set_strategy,用户选择或配置变化时换一个策略即可;也可以一开始就固定一个策略,由构造或工厂决定。按需求选择。
8.2 策略需要访问上下文怎么办?
若策略执行时需要上下文的数据,可以把上下文(或部分数据)作为参数传给策略方法,如 strategy.pay(amount, order);或让策略持有上下文引用(注意避免循环引用)。通常传参更清晰。
8.3 何时不用策略模式?
若只有一两种固定做法、且几乎不会变,直接写在一个类里或简单 if 即可;策略模式适合多种做法、要互换或扩展的场景。
九、小结
- 策略模式:把同一类问题的多种算法/做法封装成策略接口和多个具体策略;上下文持有策略,把实际工作委托给策略,从而算法可替换、使用方不写死,便于扩展和运行时切换。
- 三个角色:上下文(持有策略、委托调用)、策略(接口)、具体策略(各种实现)。
- 典型用途:支付方式、折扣规则、路线计算、排序/比较规则、加密算法等“多种做法可互换”的场景。
- 实现要点:上下文只依赖策略接口;换行为就 set_strategy(另一个策略);不加 if-else 按类型分支。
建议先写“支付策略”,再试“折扣策略”或“路线策略”,体会“注入谁就用谁、换策略就换行为”,这样对策略模式会掌握得比较扎实。