← 返回题目列表

策略模式的策略接口粒度应该如何设计?

高频 中等 第 3 / 26 题 更新于 2026/08/02
策略模式接口设计抽象粒度可扩展性

简化版

策略接口粒度要围绕稳定变化点设计:一组策略应该解决同一类问题,输入输出语义一致,调用方不需要知道具体策略差异。接口太粗会迫使策略实现很多无关方法,太细会导致类爆炸和编排复杂。常见做法是用上下文参数对象承载输入,用统一结果对象表达输出,把差异留在具体策略内部。

详细版

策略模式最容易被写坏的地方不是类图,而是接口抽象。比如把支付、优惠、库存、物流都塞进一个 BusinessStrategy,会导致策略职责混乱;反过来,每个小判断都拆一个策略,又会制造过度设计。

好的策略接口通常满足 4 点:

  1. 语义单一,例如只负责“计算运费”或“选择支付路由”;
  2. 输入稳定,使用上下文对象而不是越来越长的参数列表;
  3. 输出统一,调用方可以按同一种方式处理结果;
  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 可以让旧策略忽略新字段,减少接口签名变更。

九、加强记忆

  1. 策略接口围绕同一稳定变化点设计。
  2. 太粗会空实现,太细会编排复杂。
  3. 输入用 context,避免参数列表膨胀。
  4. 输出用 result,保留解释和监控信息。
  5. 新增策略不应频繁修改接口。
  6. 大幅语义变化时新建接口,不硬塞旧接口。