一、先来看一段"眼熟"的代码
做订单系统的时候,几乎每个人都写过类似这样的东西:
python
def calc_discount(order, user_level):
if user_level == "normal":
return order.amount * 0.95
elif user_level == "vip":
return order.amount * 0.85
elif user_level == "svip":
return order.amount * 0.7
else:
return order.amount
刚上线时一切正常。三个月后,运营加了"新人券""满减""会员日""渠道专享价",代码变成了这样:
python
if user_level == "normal" and has_coupon and is_member_day:
...
elif ...
嵌套越来越深,改一处崩两处,测试用例越堆越多,最后没人敢动这个文件。
问题的本质不在于逻辑复杂,而在于"判断"和"计算"被绑死在了一起。 每次新增规则,都必须修改同一个函数,违反了开闭原则(对扩展开放,对修改关闭)。
解法就是策略模式------它可能是所有设计模式里最容易落地、性价比最高的一个。
二、策略模式的核心思想
一句话:把每个分支抽成一个独立的类(或函数),让它们实现同一个接口,然后在运行时按需切换。
结构上只有三个角色:
| 角色 | 作用 |
|---|---|
| Strategy(策略接口) | 定义统一的执行方法 |
| ConcreteStrategy(具体策略) | 每个分支一个实现 |
| Context(上下文) | 持有策略引用,负责调用 |
听起来很学术,看代码就明白了。
三、Python 版本:用字典当注册表
Python 里甚至不需要搞一堆类,一等公民的函数加上字典就够了:
python
from dataclasses import dataclass
from typing import Callable, Dict
@dataclass
class Order:
amount: float
channel: str = "app"
# 1. 每个策略就是一个普通函数
def normal_discount(order: Order) -> float:
return order.amount * 0.95
def vip_discount(order: Order) -> float:
return order.amount * 0.85
def svip_discount(order: Order) -> float:
return order.amount * 0.70
# 2. 用字典做策略注册表
DISCOUNT_STRATEGIES: Dict[str, Callable[[Order], float]] = {
"normal": normal_discount,
"vip": vip_discount,
"svip": svip_discount,
}
DEFAULT_STRATEGY = normal_discount
# 3. 上下文:只负责查表和调用,不关心计算细节
def calc_discount(order: Order, user_level: str) -> float:
strategy = DISCOUNT_STRATEGIES.get(user_level, DEFAULT_STRATEGY)
return strategy(order)
# 使用
order = Order(amount=299.0)
print(calc_discount(order, "vip")) # 254.15
print(calc_discount(order, "unknown")) # 284.05,走默认策略
新增一个渠道折扣,只需要加一个函数并注册一行,calc_discount 一个字都不用改:
python
def channel_discount(order: Order) -> float:
return order.amount * (0.9 if order.channel == "miniapp" else 0.95)
DISCOUNT_STRATEGIES["channel"] = channel_discount
如果策略本身有状态(比如要查数据库、要读配置),换成类更合适:
python
from abc import ABC, abstractmethod
class DiscountStrategy(ABC):
@abstractmethod
def apply(self, order: Order) -> float: ...
class CouponStrategy(DiscountStrategy):
def __init__(self, coupon_service):
self.coupon_service = coupon_service # 依赖注入进来
def apply(self, order: Order) -> float:
coupon = self.coupon_service.get_best(order)
return order.amount - coupon.value
四、Java 版本:标准写法
Java 这边是教科书形态,但同样不复杂:
java
import java.math.BigDecimal;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
// 1. 策略接口
public interface DiscountStrategy {
BigDecimal apply(Order order);
}
// 2. 具体策略
public class VipDiscountStrategy implements DiscountStrategy {
@Override
public BigDecimal apply(Order order) {
return order.getAmount().multiply(new BigDecimal("0.85"));
}
}
public class SvipDiscountStrategy implements DiscountStrategy {
@Override
public BigDecimal apply(Order order) {
return order.getAmount().multiply(new BigDecimal("0.70"));
}
}
// 3. 上下文 + 策略注册表(Spring 环境下可以更优雅,见下文)
public class DiscountCalculator {
private static final Map<String, DiscountStrategy> REGISTRY = new ConcurrentHashMap<>();
private static final DiscountStrategy DEFAULT = Order::getAmount;
static {
REGISTRY.put("vip", new VipDiscountStrategy());
REGISTRY.put("svip", new SvipDiscountStrategy());
}
public static BigDecimal calc(Order order, String level) {
return REGISTRY.getOrDefault(level, DEFAULT).apply(order);
}
}
如果项目用了 Spring,连 static 注册表都可以省掉,靠容器自动装配:
java
@Service
public class DiscountCalculator {
private final Map<String, DiscountStrategy> registry;
// 关键:Spring 会把所有 DiscountStrategy 的实现类注入进这个 Map
// key 默认是 bean 名称,正好拿来当策略名
public DiscountCalculator(Map<String, DiscountStrategy> strategies) {
this.registry = new ConcurrentHashMap<>(strategies);
}
public BigDecimal calc(Order order, String level) {
DiscountStrategy strategy = registry.getOrDefault(level, registry.get("default"));
return Optional.ofNullable(strategy).orElse(IdentityStrategy.INSTANCE).apply(order);
}
}
@Component("vip")
public class VipDiscountStrategy implements DiscountStrategy { ... }
@Component("svip")
public class SvipDiscountStrategy implements DiscountStrategy { ... }
到这里你会发现:新增规则 = 新增一个类 + 一个注解,原有代码零修改。这就是开闭原则最直观的体现。
五、几个容易踩的坑
1. 别为了模式而模式。 如果分支只有两个,且半年不会变,if-else 完全没问题。抽象是有成本的,多出来的十几个文件会让新人找入口找半天。我的经验法则是:同一个判断维度超过 4 个分支,或者预计会持续增长,才值得抽策略。
2. 策略之间不要互相依赖。 A 策略里调 B 策略,链路会迅速变成网状。如果确实需要组合,请明确引入"组合策略"(Composite),让它成为显式的一层,而不是在策略内部偷偷耦合。
3. 别忘了兜底。 注册表查不到时直接抛异常是最常见的线上故障来源之一。一定要给默认策略,或者至少记录一条清晰的 warn 日志,把入参和命中的 key 打出来。
4. 策略名不要用魔法字符串裸传。 Python 可以用 Enum,Java 直接用枚举做 key,配合编译期检查,能避免大量手滑导致的 bug。
六、什么时候该上更重的方案
策略模式解决的是"分支多"的问题。但如果你的场景是:
- 规则需要运营在后台动态配置、随时上下架;
- 规则之间存在优先级、互斥、叠加等组合关系;
- 规则数量上百,且变更频率以天计;
那么就该考虑规则引擎(如 Drools、LiteFlow、Aviator)或者把规则外置到配置中心/数据库里了。策略模式是重构的第一步,不是终点。
小结
| 改造前 | 改造后 | |
|---|---|---|
| 新增规则 | 修改主函数,回归测试全量 | 新增一个类/函数,单测覆盖自己 |
| 可读性 | 深层嵌套,要看完整段才知道做什么 | 类名即文档 |
| 可测性 | 构造各种分支组合的入参 | 每个策略独立测试 |
| 风险 | 改一处影响全部 | 影响面隔离 |
设计模式的价值从来不是让代码看起来高级,而是让改动变得安全。策略模式恰好是投入产出比最高的那一个:半小时就能学会,当天就能用上。