策略模式的策略接口粒度应该如何设计?
简化版
策略接口粒度要围绕稳定变化点设计:一组策略应该解决同一类问题,输入输出语义一致,调用方不需要知道具体策略差异。接口太粗会迫使策略实现很多无关方法,太细会导致类爆炸和编排复杂。常见做法是用上下文参数对象承载输入,用统一结果对象表达输出,把差异留在具体策略内部。
详细版
策略模式最容易被写坏的地方不是类图,而是接口抽象。比如把支付、优惠、库存、物流都塞进一个 BusinessStrategy,会导致策略职责混乱;反过来,每个小判断都拆一个策略,又会制造过度设计。
好的策略接口通常满足 4 点:
- 语义单一,例如只负责“计算运费”或“选择支付路由”;
- 输入稳定,使用上下文对象而不是越来越长的参数列表;
- 输出统一,调用方可以按同一种方式处理结果;
- 扩展策略时不频繁修改接口。
interface FreightStrategy {
FreightResult calculate(FreightContext context);
}
如果新增一个策略就要给接口加参数或加方法,说明抽象粒度可能不稳。
记忆钩子:策略接口要抽“同一变化点”,不是把所有变化都塞进一个万能接口。
完整版教学
面试提示:接口粒度题要围绕“稳定抽象”和“变化点隔离”展开,太粗太细都要指出代价。
一、策略接口决定模式质量
很多人写策略模式时只关注:
interface Strategy {
void execute();
}
但真实项目中,接口签名决定了策略是否可扩展、可测试、可组合。接口设计不好,新增策略时会不断修改公共接口,反而破坏开闭原则。
二、先找稳定变化点
策略模式适合封装“同一问题的不同解法”。例如:
| 稳定变化点 | 策略例子 |
|---|---|
| 支付路由 | 微信、支付宝、银行卡 |
| 优惠计算 | 会员折扣、满减、券 |
| 文件压缩 | zip、gzip、tar |
| 排序算法 | 快排、归并、堆排 |
| 推荐召回 | 热门、协同过滤、标签召回 |
如果两个策略解决的问题不属于同一变化点,就不要强行放进一个接口。
三、接口太粗会导致空实现
坏例子:
interface OrderStrategy {
void discount(Order order);
void lockStock(Order order);
void pay(Order order);
void sendCoupon(Order order);
}
有的策略只关心优惠,却被迫实现库存和支付;有的策略只能空实现。这说明接口违反接口隔离原则。
更好的做法是拆成多个稳定接口:
interface DiscountStrategy {
Money discount(OrderContext context);
}
interface StockStrategy {
StockResult lock(StockContext context);
}
四、接口太细会造成编排复杂
另一种极端是每个判断都一个策略:
IsVipStrategy
IsNewUserStrategy
IsAppChannelStrategy
AmountGreaterThan200Strategy
这些更像规则条件,不一定适合都做成完整策略类。策略粒度太细时,调用方需要编排大量小对象,复杂度没有消失,只是换了位置。
如果是大量布尔条件组合,可以考虑规则引擎、规格模式或配置化规则,而不是把每个条件都包装成策略。
五、使用上下文对象稳定参数
如果接口一开始写成:
Money discount(User user, Order order, Coupon coupon);
后来新增渠道、设备、活动、地域,参数会越来越长。更稳的是:
record DiscountContext(
User user,
Order order,
Coupon coupon,
String channel,
String region
) {}
interface DiscountStrategy {
Money discount(DiscountContext context);
}
上下文对象可以演进,也能承载 traceId、租户、灰度分组等工程信息。
六、返回值也要统一建模
不要让不同策略随意返回不同含义的结果:
record DiscountResult(
Money finalPrice,
Money discountAmount,
String strategyName,
List<String> reasons
) {}
统一结果对象有几个好处:
- 调用方处理逻辑稳定;
- 日志和监控字段稳定;
- 策略效果可解释;
- 便于组合策略合并结果。
如果返回值只是 BigDecimal,后续想解释“为什么优惠 80 元”会很困难。
七、接口演进要向后兼容
当策略接口已经有很多实现时,修改接口成本很高。可以采用:
| 变化需求 | 处理方式 |
|---|---|
| 增加输入字段 | 放进 context,旧策略可忽略 |
| 增加输出信息 | 扩展 result 字段或 metadata |
| 新增可选能力 | 新接口或 capability 判断 |
| 大幅改变语义 | 新建策略接口,不硬改旧接口 |
策略接口越稳定,策略模式的扩展收益越明显。
八、常见误区与追问
- 误区:策略接口越通用越好。 过度通用会让语义模糊,最后变成
execute(Object)。 - 误区:所有业务动作都放进一个 Strategy。 这会违反单一职责和接口隔离,导致大量空实现。
- 误区:每个 if 条件都要拆成策略。 条件过细时可能更适合规则模型,不一定适合策略模式。
- 误区:返回一个数字就够了。 复杂业务通常需要结果原因、命中策略、明细和可观测字段。
- 追问:策略接口要不要加
supports? 条件匹配型策略可以加;类型映射型策略可以由注册表按 key 查找。 - 追问:Context 会不会变成大杂烩? 会,所以要按业务场景拆 context,并控制字段属于同一调用语义。
- 追问:新增字段会不会影响旧策略? 使用 context 可以让旧策略忽略新字段,减少接口签名变更。
九、加强记忆
- 策略接口围绕同一稳定变化点设计。
- 太粗会空实现,太细会编排复杂。
- 输入用 context,避免参数列表膨胀。
- 输出用 result,保留解释和监控信息。
- 新增策略不应频繁修改接口。
- 大幅语义变化时新建接口,不硬塞旧接口。