什么是策略模式?它解决什么问题?
简化版
策略模式是把一组可替换的算法或业务规则封装成独立策略类,让调用方通过统一接口使用它们。它主要解决大量 if-else、算法频繁变化、运行时需要切换行为的问题。
详细版
策略模式属于行为型设计模式,核心思想是:把“怎么做”的不同方案抽出来,交给不同策略对象实现;调用方只依赖策略接口,不直接写死具体判断。
典型结构包括:
- 策略接口:定义统一行为,比如
calculate()、pay()、compress()。 - 具体策略:实现不同算法,比如普通折扣、会员折扣、满减折扣。
- 上下文对象:持有策略对象,并在合适时机调用策略。
它适合这些场景:
- 同一个业务点有多种算法或规则;
- 规则经常新增、替换、下线;
- 代码中出现大量按类型分支的
if-else或switch; - 需要在运行时根据配置、用户类型、渠道、场景切换行为。
策略模式带来的主要好处是符合开闭原则:新增策略时通常只需要新增一个策略类,并把它注册到选择机制里,不需要反复修改主流程。
完整版教学
一、先抓住策略模式的本质
策略模式不是为了“把代码拆得更碎”,它解决的是“同一个变化点有多种可替换实现”的问题。
比如订单优惠计算:
if (type == NORMAL) {
return price;
} else if (type == VIP) {
return price * 0.8;
} else if (type == FULL_REDUCTION) {
return price - 50;
}
这段代码一开始没问题,但当优惠类型越来越多时,主流程会越来越臃肿。每新增一种优惠,都要改这个判断块,容易影响已有逻辑。
策略模式的做法是把每种优惠计算方式抽成一个策略:
interface DiscountStrategy {
BigDecimal discount(BigDecimal price);
}
普通优惠、会员优惠、满减优惠分别实现这个接口。订单流程只关心“拿到一个优惠策略并执行”,不关心里面到底怎么计算。
二、策略模式为什么能减少分支
大量分支的根源通常是:一个方法同时承担了“选择规则”和“执行规则”两个职责。
策略模式会把职责拆开:
- 选择策略:根据类型、配置、用户身份、渠道等找到合适策略;
- 执行策略:由具体策略对象完成真正业务逻辑。
这样主流程从“判断所有情况”变成“找到策略并执行”:
DiscountStrategy strategy = strategyFactory.get(type);
BigDecimal result = strategy.discount(price);
这不是简单地把 if-else 挪到别处,而是让每个策略拥有独立边界。策略可以单独测试、单独扩展,也更容易被复用。
三、策略模式中的“策略”应该是什么粒度
策略的粒度要围绕一个稳定抽象来切。
如果是支付系统,策略可以是“支付方式”:
- 支付宝支付策略;
- 微信支付策略;
- 银行卡支付策略;
- 余额支付策略。
如果是物流系统,策略可以是“运费计算规则”:
- 普通快递;
- 同城配送;
- 冷链配送;
- 跨境配送。
判断粒度是否合适,可以看两个问题:
- 这些实现是否能共用同一个接口?
- 调用方是否真的只关心结果,而不关心具体算法?
如果答案是肯定的,就很适合抽成策略。
四、策略模式和运行时切换的关系
策略模式的一个关键价值是“运行时切换行为”。也就是说,同一段业务流程可以根据上下文选择不同算法。
例如电商下单时:
用户身份 + 活动类型 + 商品类型 -> 选择优惠策略 -> 计算优惠金额
主流程不必知道每种优惠怎么算,它只需要知道当前订单应该用哪种策略。
如果未来新增“新人券策略”,理想情况下只新增一个策略实现和一条注册关系。订单主流程不需要被迫理解新人券细节。
五、策略模式不等于所有判断都要消灭
面试中容易出现一个误区:以为策略模式就是“不能有 if-else”。
实际上策略模式不是彻底消灭判断,而是把复杂、易变化的业务分支从主流程中隔离出去。选择策略时仍然可能存在判断,只是这个判断应该集中在工厂、注册表、配置中心或依赖注入容器里。
如果只有两三行简单判断,并且未来几乎不会变化,强行上策略模式反而会增加理解成本。
六、稳定接口与可替换算法
订单 200 元:八折策略得 160 元,满 100 减 30 策略得 170 元;输入相同但算法可替换,正是策略边界。
Context(amount=200) -> select key -> Strategy.calculate -> result
这个推演把模式带来的收益和成本放在同一条调用链上。设计评审时既要确认结果正确,也要确认额外层次没有改变原有契约。
七、边界、代价与验证
| 检查维度 | 应确认的内容 |
|---|---|
| 正确性 | 策略封装的是完整规则,不是把 if 分散到多个名字不同但职责模糊的类。 |
| 适用边界 | 只有算法族会独立演进、能共享稳定输入输出时才值得抽象;两三个不会增长的简单分支不必模式化。 |
| 测试证据 | 覆盖正常路径、异常路径、空值或未知类型,并断言调用次数与顺序 |
| 运行成本 | 记录额外对象、调用层数、延迟、线程或内存开销,不用模式名称代替测量 |
模式落地后要用单元测试验证独立职责,用集成测试验证对象装配和真实调用入口。涉及并发、远程或异步时,还要补充竞态、超时、重复执行与资源释放测试。
易错点:策略封装的是完整规则,不是把 if 分散到多个名字不同但职责模糊的类。
八、常见误区与追问
- 误区:策略模式要求运行时必须频繁切换。 运行时切换是能力而非硬条件;配置期选择不同实现同样可以使用策略。
- 误区:用了策略模式,系统里就不该再出现任何条件判断。 策略消除的是反复扩张的业务算法分支;选择策略、校验输入和兜底仍可能需要有限判断。
- 误区:示例代码能运行,就代表模式边界设计正确。 能运行只证明一条路径,仍要检查异常、并发、顺序、生命周期以及新增实现时是否修改稳定代码。
- 追问:怎样判断两个分支属于同一策略族? 它们解决同一问题、接受可统一的上下文并返回同类结果,同时实现可相互替换。
- 追问:策略对象可以保存本次请求的可变状态吗? 容器单例策略通常不应保存请求状态,应把订单、用户等数据作为参数传入,否则并发调用会相互污染。
- 追问:怎样验证这套设计没有改变原有语义? 先做基线行为测试,再对新增层验证返回值、异常类型、调用次数和顺序;性能敏感处还要比较改造前后的量化指标。
九、加强记忆
策略模式要记成“把多种可替换算法封装起来,让主流程面向统一接口调用”。它适合规则多、变化频繁、需要运行时切换的场景,核心收益是减少主流程分支、隔离变化、方便扩展和测试。