← 返回题目列表

策略模式中多个策略都匹配时如何选择优先级?

高频 中等 第 12 / 26 题 更新于 2026/08/02
策略模式策略选择优先级规则匹配

简化版

多个策略都可能命中时,不能靠集合遍历的偶然顺序决定结果,而要把“匹配条件”和“优先级规则”显式建模。常见做法是让策略提供 supports(context)priority(),注册表按优先级排序后选择第一个匹配策略;如果业务要求可解释,还要记录命中原因、冲突策略和兜底策略。

详细版

策略模式经常不是简单的 type -> strategy 映射。比如优惠计算、风控规则、运费计算、路由选择,可能有多个策略同时满足条件:新人优惠、会员优惠、渠道优惠、活动优惠都可能命中同一个订单。

这时有 3 个关键点:

  1. 匹配逻辑要放在策略或规则对象里,不能散落在调用方;
  2. 优先级要显式声明,不能依赖 Spring 注入顺序、Map 遍历顺序或文件加载顺序;
  3. 冲突处理要可观测,至少能回答“为什么选了这个策略,没有选那个策略”。

工程上可以这样设计:

interface DiscountStrategy {
    boolean supports(OrderContext context);
    int priority();
    BigDecimal discount(OrderContext context);
    String name();
}

注册表启动时把策略按 priority 排好,执行时过滤 supports 为 true 的策略,再选择优先级最高的一个。如果策略之间允许叠加,就不再是“选一个策略”,而要改成组合策略、责任链或规则引擎模型。

记忆钩子:策略选择不是“谁先遍历到算谁”,而是“匹配条件、优先级、冲突解释”三件事一起设计。

完整版教学

面试提示:优先级题要重点讲确定性,规则冲突时必须能解释为什么选中某个策略。

一、为什么策略选择会出现冲突

很多教材例子把策略模式写成:

DiscountStrategy strategy = map.get(type);
return strategy.discount(order);

这种写法适合“类型互斥”的场景,比如 WECHAT_PAYALI_PAYBANK_CARD 三选一。

但真实业务经常是“条件叠加”:

订单 A:
  用户: 新用户
  等级: VIP
  渠道: App
  商品: 生鲜
  活动: 周末满减

可能命中的策略:
  NewUserDiscount
  VipDiscount
  AppChannelDiscount
  WeekendFullReduction

如果这些策略只能选择一个,就必须有清晰优先级;如果可以同时生效,就必须有组合规则。

二、先区分“互斥策略”和“可叠加策略”

面试里回答优先级前,最好先反问或主动说明业务语义:

策略关系含义典型处理
互斥同一时刻只能选一个priority 选最高
可叠加多个策略可以依次执行组合策略或责任链
分组互斥每组一个,组间可叠加group + priority
强制排斥A 命中后 B 不能用exclusion rule

例如支付路由通常是互斥策略;优惠系统可能是分组互斥,例如“平台券”和“商家券”可叠加,但同一组内只能选最优。

三、用 supports 表达匹配条件

不要把所有判断都写在调用方:

if (user.isVip() && order.amount().compareTo(new BigDecimal("200")) > 0) {
    return vipStrategy.discount(order);
}

这样调用方会再次膨胀。更好的方式是策略自己声明是否支持:

class VipDiscountStrategy implements DiscountStrategy {
    public boolean supports(OrderContext context) {
        return context.userLevel() == UserLevel.VIP
            && context.amount().compareTo(new BigDecimal("200")) >= 0;
    }

    public int priority() {
        return 80;
    }

    public BigDecimal discount(OrderContext context) {
        return context.amount().multiply(new BigDecimal("0.85"));
    }

    public String name() {
        return "vip-discount";
    }
}

这样新增策略时,调用方只负责“找策略”,具体命中规则跟策略放在一起。

四、用 priority 显式表达选择顺序

策略注册表可以在构造时完成排序:

class DiscountStrategyRegistry {
    private final List<DiscountStrategy> strategies;

    DiscountStrategyRegistry(List<DiscountStrategy> strategies) {
        this.strategies = strategies.stream()
            .sorted(Comparator.comparingInt(DiscountStrategy::priority).reversed())
            .toList();
    }

    DiscountStrategy select(OrderContext context) {
        return strategies.stream()
            .filter(strategy -> strategy.supports(context))
            .findFirst()
            .orElseThrow(() -> new IllegalArgumentException("no discount strategy"));
    }
}

这里的关键不是代码复杂,而是把“谁优先”变成可读、可测、可评审的规则。

五、不要依赖容器注入的偶然顺序

Spring 中 List<Strategy> 可以结合 @Order 使用,但不要让业务优先级变成隐式约定:

@Component
@Order(80)
class VipDiscountStrategy implements DiscountStrategy {
}

如果优先级属于业务规则,更推荐在策略接口里暴露 priority(),或者从配置中心读取。@Order 更适合技术顺序,例如过滤器、拦截器、处理器链顺序。

六、冲突要能解释

只返回最终策略,在排查问题时不够。可以记录候选策略:

requestId=R1001
matched=[vip-discount:80, app-channel:60, default:0]
selected=vip-discount
reason=highest_priority

这类日志对面试很加分,因为它说明你不是只会写模式结构,还考虑了线上可观测性。

七、什么时候应该换成规则引擎

如果策略数量达到几十个,并且条件由运营频繁配置,纯代码策略会开始吃力。判断标准可以是:

现象继续策略模式考虑规则引擎
策略数5 到 20 个50 个以上
变更人开发为主运营/风控为主
条件复杂度少量字段判断多维条件组合
发布方式代码发布可接受需要分钟级生效

但规则引擎也不是万能,它会引入规则调试、权限、回滚、性能和治理成本。

八、常见误区与追问

  • 误区:策略模式就是 Map.get(type),不会有冲突。 只有类型天然互斥时才是简单映射;条件匹配型策略经常会多个同时命中。
  • 误区:谁先注册谁优先就可以。 这会让结果依赖启动顺序或容器顺序,线上排查很困难。
  • 误区:优先级数字越大越好,不需要规范。 团队要约定范围,例如 0 为默认、100 为最高,中间预留插入空间。
  • 误区:所有优惠策略都应该只选一个。 有些业务要求叠加,这时应该用组合策略或责任链,而不是强行 priority。
  • 追问:如果两个策略 priority 一样怎么办? 启动时检测并报错,或者再定义 group、version、createTime 等稳定 tie-breaker,不能随机选。
  • 追问:如何让策略选择可测试? 构造多个 context,断言候选策略、最终策略和优先级排序,不只测最终金额。
  • 追问:优先级放注解还是配置? 技术顺序可用注解,业务顺序更适合配置或策略字段,并配合灰度、审计和回滚。

九、加强记忆

  1. 策略选择先判断关系:互斥、叠加、分组互斥。
  2. 条件型策略要有 supports(context)
  3. 多个命中要有 priority() 或明确冲突规则。
  4. 不要依赖 Map、List、容器注入的偶然顺序。
  5. 线上要记录候选、选中、原因,方便解释和排查。
  6. 策略数量和规则复杂度过高时,再评估规则引擎。