策略模式中的 Context 应该怎么设计?
简化版
策略模式中的 Context 负责承载稳定流程和调用策略,不应该包含大量具体策略判断。它可以做参数校验、流程编排、结果包装,也可以持有策略或策略工厂,但具体算法应交给策略实现。
详细版
Context 的设计重点是边界清晰。
它通常可以承担:
- 接收业务请求;
- 做通用参数校验;
- 根据策略工厂获取策略;
- 调用策略接口;
- 处理通用日志、异常、结果包装。
它不应该承担:
- 判断每种策略的内部细节;
- 把不同策略的业务逻辑写在自己里面;
- 保存请求级可变状态;
- 让策略之间互相耦合。
如果 Context 中又出现大量 if-else,说明策略拆分可能不彻底,或者策略选择逻辑应该单独抽成工厂、路由器、规则选择器。
完整版教学
一、Context 的核心定位
策略模式里的 Context 可以理解为“稳定流程的承载者”。它不是具体算法的实现者,而是策略的使用者。
以价格计算为例,Context 可以叫 PriceCalculateService:
public class PriceCalculateService {
private final PriceStrategyFactory factory;
public PriceResult calculate(PriceRequest request) {
PriceStrategy strategy = factory.get(request.getPriceType());
return strategy.calculate(request);
}
}
这里 Context 负责组织调用,具体价格算法由策略处理。
二、Context 可以持有策略,也可以持有工厂
教材里经常写:
Context context = new Context(strategy);
context.execute();
这种写法适合演示模式本身。
真实项目里更常见的是 Context 持有策略工厂:
PriceStrategy strategy = factory.get(request.getScene());
strategy.calculate(request);
原因是业务请求通常带着类型、渠道、场景等信息,Context 需要在运行时选择策略。
两种写法都可以,重点是不要让 Context 直接依赖大量具体策略类。
三、Context 适合放通用流程
有些逻辑不是具体策略的一部分,而是所有策略都需要的公共流程,这些可以放在 Context。
例如:
- 请求参数基础校验;
- 记录策略执行日志;
- 统计耗时;
- 捕获并转换异常;
- 统一包装返回结果;
- 做权限或幂等校验。
这些逻辑如果放进每个策略,会造成重复。放在 Context 中更合理。
四、Context 不应该重新变成上帝类
策略模式最怕一种写法:表面上抽了策略,Context 里还保留大量业务判断。
例如:
if ("vip".equals(type)) {
// 一半逻辑写这里
strategy.calculate(request);
}
这会让职责重新混乱。遇到这种情况,要判断:
- 这段判断是不是策略选择?如果是,放到工厂或选择器。
- 这段逻辑是不是某个策略独有?如果是,放进具体策略。
- 这段逻辑是不是所有策略通用?如果是,留在 Context。
五、上下文参数对象很重要
策略接口的入参最好不要设计成一长串基础类型:
calculate(userId, amount, level, channel, activityId, goodsType)
参数一多,可读性和扩展性都会变差。更好的方式是封装上下文对象:
public class PriceContext {
private Long userId;
private BigDecimal amount;
private String channel;
private String activityId;
private List<Item> items;
}
策略接口变成:
PriceResult calculate(PriceContext context);
这样新增字段时不需要频繁改接口签名,也更符合业务语义。
六、Context 保持稳定流程而不过度膨胀
结算 Context 固定执行参数校验、选策略、计算、记录结果 4 步;其中第 3 步算法变化,适合委托策略。
validate -> resolve strategy -> calculate -> audit result
这个推演把模式带来的收益和成本放在同一条调用链上。设计评审时既要确认结果正确,也要确认额外层次没有改变原有契约。
七、边界、代价与验证
| 检查维度 | 应确认的内容 |
|---|---|
| 正确性 | 判断 Context 是否过重,要看新增策略是否仍需修改它的核心代码。 |
| 适用边界 | Context 可承载通用编排和不可变上下文,但不应重新出现每种策略的业务分支或保存跨请求状态。 |
| 测试证据 | 覆盖正常路径、异常路径、空值或未知类型,并断言调用次数与顺序 |
| 运行成本 | 记录额外对象、调用层数、延迟、线程或内存开销,不用模式名称代替测量 |
模式落地后要用单元测试验证独立职责,用集成测试验证对象装配和真实调用入口。涉及并发、远程或异步时,还要补充竞态、超时、重复执行与资源释放测试。
易错点:判断 Context 是否过重,要看新增策略是否仍需修改它的核心代码。
八、常见误区与追问
- 误区:Context 越薄越好,最好只剩一行调用。 它可以负责稳定的校验、监控和流程骨架;关键是不要侵入具体算法。
- 误区:用了策略模式,系统里就不该再出现任何条件判断。 策略消除的是反复扩张的业务算法分支;选择策略、校验输入和兜底仍可能需要有限判断。
- 误区:示例代码能运行,就代表模式边界设计正确。 能运行只证明一条路径,仍要检查异常、并发、顺序、生命周期以及新增实现时是否修改稳定代码。
- 追问:请求参数应该拆成很多形参还是上下文对象? 参数较多且共同演进时可用不可变上下文对象,但要避免把无关数据塞成万能 Map。
- 追问:策略对象可以保存本次请求的可变状态吗? 容器单例策略通常不应保存请求状态,应把订单、用户等数据作为参数传入,否则并发调用会相互污染。
- 追问:怎样验证这套设计没有改变原有语义? 先做基线行为测试,再对新增层验证返回值、异常类型、调用次数和顺序;性能敏感处还要比较改造前后的量化指标。
九、加强记忆
Context 要记成“稳定流程的组织者”。它可以做通用校验、编排、日志和结果包装,但不要写具体策略逻辑;策略选择交给工厂,算法执行交给策略,复杂入参用上下文对象承载。