策略模式如何处理多个策略组合执行?
简化版
如果业务不是“从多个策略中选一个”,而是“多个策略按规则一起执行”,就要使用组合策略。组合策略可以把多个子策略封装成一个整体,对外仍然暴露同一个策略接口;内部负责排序、过滤、短路、结果合并和异常处理。优惠叠加、价格计算、风控评分、推荐排序都常见这种设计。
详细版
策略模式的基本形态是选择一个算法。但真实业务里经常需要多个策略协作:
- 多个优惠同时计算,再选择最优或叠加;
- 多个风控模型打分,合并成一个风险等级;
- 多个推荐召回策略返回候选集,再合并去重;
- 多个运费规则依次修正价格。
这时可以设计组合策略:
class CompositeDiscountStrategy implements DiscountStrategy {
private final List<DiscountStrategy> delegates;
public BigDecimal discount(OrderContext context) {
BigDecimal price = context.originalPrice();
for (DiscountStrategy strategy : delegates) {
if (strategy.supports(context)) {
price = strategy.discount(context.withPrice(price));
}
}
return price;
}
}
组合策略的重点不是把多个类放进 List,而是明确“顺序、是否短路、结果怎么合并、失败怎么办”。如果这些规则不清楚,组合策略会变成隐藏的业务黑箱。
记忆钩子:普通策略是“选一个”,组合策略是“把多个策略包装成一个策略”。
完整版教学
面试提示:组合策略题要强调“组合规则本身也需要建模”,不要把多个策略随便循环调用就当成设计完成。
一、为什么单策略选择不够用
以优惠计算为例,业务可能要求:
原价 1000
会员折扣 9 折
平台券 -50
满减 -100
运费券 -20
最终价 = 1000 * 0.9 - 50 - 100 - 20 = 730
如果仍然只选一个策略,就无法表达这种叠加规则。策略模式本身不限制只能选一个,关键看你如何组织策略。
二、组合策略仍然实现同一个接口
组合策略可以对外保持统一接口:
interface PriceStrategy {
Money calculate(PriceContext context);
}
class CompositePriceStrategy implements PriceStrategy {
private final List<PriceStrategy> strategies;
public Money calculate(PriceContext context) {
Money current = context.basePrice();
for (PriceStrategy strategy : strategies) {
current = strategy.calculate(context.withCurrentPrice(current));
}
return current;
}
}
调用方看到的还是一个 PriceStrategy,不会感知里面有 3 个、5 个还是 10 个子策略。
三、组合执行必须定义顺序
顺序不同,结果可能不同:
方案 A: 1000 * 0.9 - 100 = 800
方案 B: (1000 - 100) * 0.9 = 810
所以组合策略里要显式排序:
this.strategies = strategies.stream()
.sorted(Comparator.comparingInt(PriceStrategy::order))
.toList();
并且要把顺序写进规则文档或配置。例如“先商品级优惠,再订单级优惠,最后支付优惠”。
四、组合策略有多种结果合并方式
组合策略不一定都是串行修改同一个结果,还可能是:
| 合并方式 | 含义 | 例子 |
|---|---|---|
| sequential | 前一个结果传给后一个 | 价格逐步修正 |
| max/min | 多个结果取最优 | 多张券选优惠最大 |
| merge | 多个集合合并去重 | 推荐召回 |
| vote | 多个策略投票 | 风控是否通过 |
| score | 多个分数加权 | 风险评分 |
面试中讲清“结果合并模型”,比只说“用 List 循环”更有工程味。
五、组合策略和责任链的区别
组合策略和责任链都可能按顺序执行,但关注点不同:
| 对比项 | 组合策略 | 责任链 |
|---|---|---|
| 核心意图 | 多个算法合成一个结果 | 请求沿链传递处理 |
| 是否都执行 | 通常多个参与 | 可短路,也可继续 |
| 对外身份 | 仍然是一个策略 | 是一条处理链 |
| 常见例子 | 优惠叠加、评分合成 | 过滤器、审批、拦截 |
如果你关注“多个算法怎么合并”,偏组合策略;如果你关注“请求谁处理、是否继续传递”,偏责任链。
六、异常处理不能含糊
组合策略里一个子策略失败,不能默认跳过。需要按策略类型定义:
try {
current = strategy.calculate(context.withCurrentPrice(current));
} catch (Exception ex) {
if (strategy.required()) {
throw ex;
}
log.warn("optional strategy failed: {}", strategy.name(), ex);
}
例如支付优惠失败可以跳过,但库存锁定失败不能跳过。组合策略里最好给每个子策略标记 required、order、name 和 timeout。
七、组合策略要防止重复执行
如果同一个策略被注册两次,可能造成金额重复扣减:
strategies:
coupon
vip
coupon <- 重复,可能多扣一次
启动时可以检测:
Set<String> names = new HashSet<>();
for (PriceStrategy strategy : strategies) {
if (!names.add(strategy.name())) {
throw new IllegalStateException("duplicate strategy: " + strategy.name());
}
}
这类校验对金额、积分、库存等场景尤其重要。
八、常见误区与追问
- 误区:策略模式只能每次选一个策略。 策略可以被组合,对外仍然表现为一个统一策略接口。
- 误区:组合策略就是简单 for 循环。 真正难点是顺序、短路、合并、异常和幂等。
- 误区:策略顺序无所谓。 价格、风控、路由等场景顺序会直接影响结果。
- 误区:子策略失败就跳过。 必选策略失败应中断,可选策略失败才考虑降级。
- 追问:组合策略和责任链怎么区分? 组合策略重在多个算法合成结果,责任链重在请求传递和处理权。
- 追问:如何测试组合策略? 测顺序、重复注册、部分失败、空策略列表、边界金额和结果合并。
- 追问:组合策略会不会过度设计? 策略少且规则稳定时没必要;规则频繁变化、结果合并复杂时才值得。
九、加强记忆
- 普通策略是选一个,组合策略是多个策略合成一个结果。
- 组合策略本身也实现策略接口。
- 组合执行必须定义顺序和合并方式。
- 子策略失败要区分必选和可选。
- 金额类策略要防重复执行和重复注册。
- 与责任链的区别在于重点是“结果合成”还是“请求传递”。