← 返回题目列表

策略模式中如何设计默认策略和兜底策略?

高频 中等 第 14 / 26 题 更新于 2026/08/02
策略模式默认策略兜底策略异常处理

简化版

策略模式不能只考虑正常命中,还要设计没有匹配、配置错误、策略执行失败时怎么办。常见做法是提供默认策略处理普通场景,提供兜底策略保护主流程,并区分“业务不支持”与“系统异常”。默认策略可以正常返回结果,兜底策略则要配合告警、日志和降级边界,避免悄悄吞掉严重问题。

详细版

面试中常见追问是:strategyFactory.get(type) 找不到策略怎么办?如果直接返回 null,调用方容易 NullPointerException;如果随便返回默认策略,又可能掩盖配置错误。

更稳的设计是把失败场景分开:

  1. 业务允许无匹配:走默认策略,例如普通用户无优惠;
  2. 业务不允许无匹配:明确抛业务异常,例如未知支付方式;
  3. 策略执行失败:根据业务重要性决定失败、重试或降级;
  4. 配置错误:启动时或变更时校验,尽量不要到运行时才爆。
DiscountStrategy strategy = registry.find(context)
    .orElse(defaultDiscountStrategy);

但支付、扣款、风控这类强一致场景不适合随便兜底。兜底策略的核心原则是:能兜业务默认值,不能兜数据正确性。

记忆钩子:默认策略是“业务正常分支”,兜底策略是“异常保护网”,两者不能混成一个沉默的黑洞。

完整版教学

面试提示:默认策略和兜底策略不是偷懒分支,而是系统可用性设计的一部分,要说明触发条件和副作用。

一、为什么策略模式必须考虑兜底

很多策略模式示例只写正常路径:

Strategy strategy = strategies.get(type);
return strategy.execute(request);

但线上系统会遇到:

  • 前端传了未知 type;
  • 配置中心漏配了策略 key;
  • 某个策略依赖的外部接口超时;
  • 新策略灰度时只对部分用户开放;
  • 老版本客户端仍然发送旧枚举。

如果没有兜底设计,策略模式会从“优雅扩展”变成“运行时炸点”。

二、默认策略和兜底策略不是一回事

类型触发原因是否正常例子
默认策略业务允许没有特殊规则正常普通用户无折扣
兜底策略异常或无法选择异常保护推荐失败返回热门榜
空对象策略不做任何增强正常或保护NoOpLogger
降级策略依赖失败后简化处理异常保护风控超时转人工

例如优惠计算中,普通用户使用 NoDiscountStrategy 是默认策略;配置中心读取失败后使用本地旧配置,才是兜底策略。

三、找不到策略时的三种处理方式

第一种是业务异常:

PaymentStrategy strategy = registry.find(payType)
    .orElseThrow(() -> new UnsupportedPayTypeException(payType));

支付方式未知,不能自动用余额支付兜底,因为这会造成严重资金风险。

第二种是默认策略:

DiscountStrategy strategy = registry.find(context)
    .orElse(NoDiscountStrategy.INSTANCE);

没有命中特殊优惠时,原价结算是业务可接受结果。

第三种是降级结果:

RecommendStrategy strategy = registry.find(scene)
    .orElse(hotListFallbackStrategy);

推荐场景更关注可用性,个性化失败后返回热门榜通常可以接受。

四、策略执行失败时不能一刀切

策略执行失败的处理要按业务风险分层:

场景失败处理原因
支付扣款失败并回滚资金正确性优先
库存锁定失败或补偿防止超卖
推荐排序降级默认列表可用性优先
日志增强忽略或异步补偿不阻断主流程
风控校验保守拒绝或人工安全优先

这说明兜底策略不是固定答案,要根据业务的正确性、可用性和用户体验取舍。

五、用 Optional 或 Result 表达选择结果

不要让 null 在策略工厂里流动:

class StrategyRegistry {
    Optional<DiscountStrategy> find(OrderContext context) {
        return strategies.stream()
            .filter(strategy -> strategy.supports(context))
            .findFirst();
    }
}

调用方必须显式处理缺失:

DiscountStrategy strategy = registry.find(context)
    .orElseGet(() -> defaultStrategy);

如果还要带错误原因,可以返回 Result<Strategy>,包含 matchedreasonfallbackUsed 等字段。

六、启动校验能减少运行时兜底

如果策略是按枚举注册的,可以启动时校验完整性:

for (PayType type : PayType.values()) {
    if (!registry.contains(type)) {
        throw new IllegalStateException("missing strategy for " + type);
    }
}

对于配置化策略,也要在配置发布时校验 key 是否存在、优先级是否冲突、灰度条件是否合法。好的兜底设计不是鼓励运行时随便兜,而是让系统在错误进入主流程前就发现问题。

七、兜底策略必须可观测

兜底一旦发生,至少要记录:

strategy_scene=discount
selected=no-discount
fallback=true
fallback_reason=no_matched_strategy
request_id=R202
user_id=U100

对于高风险场景还要报警。否则兜底策略会把故障藏起来,短期看系统没挂,长期看业务结果已经偏了。

八、常见误区与追问

  • 误区:找不到策略就返回 null。 这会把错误推迟到调用处,通常变成更难定位的空指针。
  • 误区:所有场景都可以走默认策略。 支付、扣款、库存、权限、风控等场景不能随便默认通过。
  • 误区:兜底策略就是吞异常。 兜底要记录原因、指标和告警,必要时还要补偿。
  • 误区:默认策略和降级策略没有区别。 默认策略是正常业务分支,降级策略是异常保护手段。
  • 追问:NoOp 策略适合什么场景? 适合日志、指标、可选增强、无折扣等不影响核心正确性的场景。
  • 追问:如何防止配置漏配策略? 启动校验、配置发布校验、枚举覆盖测试、灰度前预检查都要做。
  • 追问:兜底后要不要继续重试? 看幂等性和业务风险;读请求可以有限重试,写请求要防重复执行。

九、加强记忆

  1. 找不到策略先区分:业务允许、业务不允许、系统异常。
  2. 默认策略是正常分支,兜底策略是异常保护。
  3. null 不是策略选择结果,优先用 Optional 或明确异常。
  4. 高风险业务不能随便兜底通过。
  5. 兜底必须有日志、指标、告警和原因。
  6. 启动校验和配置校验能减少运行时事故。