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