策略模式有哪些角色?调用流程是什么?
简化版
策略模式通常有三个角色:策略接口、具体策略、上下文。调用流程是客户端选择或传入策略,上下文持有策略并调用统一方法,真正的算法由具体策略完成。
详细版
策略模式的角色可以这样理解:
- Strategy:策略接口,定义所有策略必须实现的方法。
- ConcreteStrategy:具体策略,实现某一种算法或业务规则。
- Context:上下文,持有策略对象,对外提供业务入口,并把可变行为委托给策略。
调用流程通常是:
- 客户端或工厂根据业务条件选择一个策略;
- 把策略传给上下文,或者上下文从策略注册表中获取;
- 上下文调用策略接口;
- 具体策略执行算法并返回结果。
策略模式的关键是“上下文依赖抽象策略接口”,而不是依赖具体策略类。这样新增策略时,上下文不需要知道新策略的内部实现。
完整版教学
一、策略接口负责定义稳定抽象
策略接口是整个模式的中心,它定义的是“所有策略都能做什么”。
以运费计算为例:
public interface FreightStrategy {
BigDecimal calculate(Order order);
}
这个接口不关心是同城配送、普通快递还是跨境物流,只规定它们都必须能根据订单计算运费。
一个好的策略接口应该满足:
- 方法含义清晰;
- 入参尽量稳定;
- 返回值统一;
- 不暴露某个具体策略独有的细节。
如果接口为了兼容某个策略不断加参数,说明抽象可能没有切好。
二、具体策略负责封装变化
具体策略承载不同业务规则:
public class LocalFreightStrategy implements FreightStrategy {
public BigDecimal calculate(Order order) {
return BigDecimal.valueOf(8);
}
}
public class ExpressFreightStrategy implements FreightStrategy {
public BigDecimal calculate(Order order) {
return order.getWeight().multiply(BigDecimal.valueOf(3));
}
}
每个策略只处理自己的算法,不关心其他策略。这让代码结构更接近业务语言:看到类名就知道它处理哪种规则。
策略类通常应该保持单一职责。不要把“策略选择逻辑”也写进具体策略里,否则策略之间会互相知道对方存在,耦合又回来了。
三、上下文负责组织调用
上下文对象承接业务入口:
public class FreightContext {
private final FreightStrategy strategy;
public FreightContext(FreightStrategy strategy) {
this.strategy = strategy;
}
public BigDecimal calculate(Order order) {
return strategy.calculate(order);
}
}
在简单版本中,上下文只持有一个策略对象;在工程版本中,上下文也可能依赖一个策略工厂,由工厂根据订单信息选择策略。
上下文的边界要清楚:它可以做流程编排、参数校验、结果包装,但不要重新写一堆分支去判断具体策略细节。
四、客户端不一定直接 new 策略
教材里常见写法是:
new FreightContext(new ExpressFreightStrategy()).calculate(order);
但真实项目里通常不会到处 new。更常见的是:
- Spring 注入所有策略;
- 使用 Map 注册策略;
- 从配置读取策略类型;
- 用工厂统一创建或获取策略。
这样可以避免客户端散落大量创建逻辑,也便于统一处理默认策略和异常策略。
五、调用链可以画成一条线
策略模式的调用链可以简化为:
客户端/工厂
-> 选择具体策略
-> 上下文 Context
-> 调用 Strategy 接口
-> ConcreteStrategy 执行
这条链的重点是:选择和执行被拆开,调用方和算法实现被接口隔离。
六、Strategy、ConcreteStrategy 与 Context 的协作
Context 接收 1 个订单上下文,注册表从 3 个策略中选 1 个执行;策略不需要知道其余两个实现。
client -> Context -> Strategy interface -> ConcreteStrategy
这个推演把模式带来的收益和成本放在同一条调用链上。设计评审时既要确认结果正确,也要确认额外层次没有改变原有契约。
七、边界、代价与验证
| 检查维度 | 应确认的内容 |
|---|---|
| 正确性 | 类名不重要,重要的是选择与执行分离,具体策略可在相同契约下替换。 |
| 适用边界 | Context 组织稳定流程,策略负责变化算法;选择逻辑可由客户端、工厂或注册表承担,不应混淆角色。 |
| 测试证据 | 覆盖正常路径、异常路径、空值或未知类型,并断言调用次数与顺序 |
| 运行成本 | 记录额外对象、调用层数、延迟、线程或内存开销,不用模式名称代替测量 |
模式落地后要用单元测试验证独立职责,用集成测试验证对象装配和真实调用入口。涉及并发、远程或异步时,还要补充竞态、超时、重复执行与资源释放测试。
易错点:类名不重要,重要的是选择与执行分离,具体策略可在相同契约下替换。
八、常见误区与追问
- 误区:Context 必须自己 new 所有策略。 它可以通过构造器注入、注册表或容器获得策略,依赖注入通常更易测试。
- 误区:用了策略模式,系统里就不该再出现任何条件判断。 策略消除的是反复扩张的业务算法分支;选择策略、校验输入和兜底仍可能需要有限判断。
- 误区:示例代码能运行,就代表模式边界设计正确。 能运行只证明一条路径,仍要检查异常、并发、顺序、生命周期以及新增实现时是否修改稳定代码。
- 追问:策略能否回调 Context? 技术上可以,但会增强双向耦合;优先通过明确参数和返回值协作。
- 追问:策略对象可以保存本次请求的可变状态吗? 容器单例策略通常不应保存请求状态,应把订单、用户等数据作为参数传入,否则并发调用会相互污染。
- 追问:怎样验证这套设计没有改变原有语义? 先做基线行为测试,再对新增层验证返回值、异常类型、调用次数和顺序;性能敏感处还要比较改造前后的量化指标。
九、加强记忆
策略模式的角色记成“接口定规则,策略做实现,上下文负责编排”。调用流程是先选策略,再通过统一接口执行策略;只要上下文不依赖具体实现,新增策略时主流程就能保持稳定。