Java策略模式实战:从if-else腐烂到可配置、可扩展、可运维的工业级设计
1. 什么是策略模式?它真不是“写一堆if-else再包个壳”那么简单
策略模式(Strategy Design Pattern)在Java面试里出现频率高得离谱,但绝大多数人讲完定义就直接贴代码,结果听的人一头雾水:这不就是把if-else拆成几个类吗?有啥高级的?我干了十年Java,带过二十多个校招生,每次问到策略模式,八成回答都卡在“解耦”“可扩展”这种空泛词上——直到他们第一次在真实项目里为促销规则加第7种折扣逻辑时,凌晨两点还在改同一个Service里的十几个else if分支,才真正明白策略模式不是教科书里的玩具,而是防止业务代码腐烂的防腐剂。
核心就一句话: 把算法的定义和使用彻底分开,让算法可以独立变化、自由替换,而调用方完全感知不到底层换了哪种实现。
注意,这里说的“算法”,远不止数学计算——它可以是支付方式(微信/支付宝/银联)、日志输出策略(控制台/文件/ELK)、风控规则引擎(规则A/规则B/机器学习模型)、甚至是一段文本清洗逻辑(去HTML标签/转义特殊字符/敏感词过滤)。只要这段逻辑满足两个条件:① 有多种实现方式;② 运行时需要动态切换,它就天然适合策略模式。
我去年重构一个电商后台的优惠券系统时,原始代码里 CouponService.calculateDiscount() 方法长达400多行,里面嵌套着if-else、switch-case,还混着硬编码的折扣系数。新加一种“老用户复购满减”规则?得先读懂整个方法的执行路径,再在合适位置插入新逻辑,改完还得通读全方法确认没破坏原有逻辑。上线后发现订单金额计算异常,回滚排查两小时才发现是某个else分支漏写了break。这种代码,别说维护,光是阅读成本就让人想辞职。
策略模式解决的从来不是“怎么写代码”,而是“怎么让代码不变成债务”。它强制你思考:这个逻辑的边界在哪?哪些部分会变?哪些部分不变?变的部分能不能抽出来独立演进?比如支付策略,微信支付的签名验签流程、支付宝的异步通知处理、银联的证书加密方式,三者差异巨大,但对外暴露的接口必须一致: pay(Order order) 。这就是策略模式的威力——它用接口划出清晰的契约边界,把变化关进笼子,让主流程像流水线一样稳定运转。
关键词“Strategy Design Pattern”“Java”“Examples”“Tutorial”背后,藏着的是开发者最痛的三个现实:面试要答得漂亮、日常要写得安全、上线要跑得稳。所以这篇内容不讲UML图,不画类关系箭头,只聚焦一件事: 怎么用Java写出真正能落地、能维护、能扛住业务迭代的策略模式代码。 后面所有内容,都来自我在支付中台、风控引擎、报表生成等6个高并发项目中的实操沉淀,包括那些官网文档绝不会写的坑——比如Spring Bean循环依赖怎么破、策略加载时机导致的NPE、以及为什么你按教程写了却还是被同事吐槽“过度设计”。
2. 策略模式的骨架拆解:为什么非得用接口+Map+工厂,而不是直接new?
很多人学策略模式,第一步就栽在结构设计上。网上教程清一色教你:“定义Strategy接口→写ConcreteStrategyA/B/C→建Context类持有Strategy引用→客户端根据条件new不同实现”。这没错,但放到真实项目里,问题立刻暴露:
- 新增策略要改Context的if-else判断逻辑,违背开闭原则;
- 所有策略类手动new,无法享受Spring的依赖注入(比如策略里要用RedisTemplate或数据库连接);
- 客户端要自己决定用哪个策略,耦合严重;
- 策略类散落在各处,运行时想查当前用了哪个策略?得翻日志或者打断点。
真正的工业级策略模式,骨架必须包含三个核心组件,缺一不可:
2.1 策略接口:契约比实现更重要
接口设计不是为了“看起来面向接口编程”,而是为了 定义最小完备契约 。以支付策略为例,初学者常写:
public interface PaymentStrategy {
void pay(Order order);
String getChannelName();
}
这就有问题—— getChannelName() 是给谁用的?如果只是日志打印,完全不该放在这里;如果是为了路由,那应该叫 getSupportChannel() 并返回枚举。我见过最糟的案例是接口里塞了12个方法,结果80%的实现类只用其中2个,其他全抛UnsupportedOperationException。
我的实践标准:接口只声明调用方绝对必需的方法,且方法名必须体现业务语义。
比如风控策略接口:
public interface RiskStrategy {
/**
* 执行风控检查,返回是否通过及原因
* @param context 风控上下文(含用户ID、订单金额、设备指纹等)
* @return 检查结果(通过/拒绝/需人工审核)
*/
RiskCheckResult check(RiskContext context);
/**
* 返回该策略的唯一标识,用于配置中心动态加载
* 例如:"rule_based_v2", "ml_model_2024_q3"
*/
String strategyId();
}
注意两点:① check() 方法参数是封装好的 RiskContext 对象,而不是零散的String userId, BigDecimal amount... 这避免每次调用都要构造参数;② strategyId() 强制每个策略自报家门,为后续动态加载埋下伏笔。接口越瘦,实现越自由;契约越准,扩展越稳。
2.2 策略容器:用Map替代if-else的底层逻辑
为什么不用if-else?因为if-else是编译期绑定,改逻辑要发版;Map是运行时查找,加策略只需配个key。但Map怎么初始化?新手常犯的错是:
// ❌ 危险!Spring Bean未初始化完成就调用
@Component
public class RiskStrategyContext {
private final Map<String, RiskStrategy> strategyMap = new HashMap<>();
public RiskStrategyContext() {
// 构造函数里直接new,失去Spring管理
strategyMap.put("rule_v1", new RuleBasedStrategyV1());
strategyMap.put("ml_v1", new MLModelStrategyV1());
}
}
这会导致策略类无法注入任何Spring Bean(如 @Autowired RedisTemplate ),且违反Spring生命周期。
正确姿势是利用Spring的 ApplicationContextAware 或 InitializingBean :
@Component
public class RiskStrategyContext implements ApplicationContextAware, InitializingBean {
private ApplicationContext applicationContext;
private Map<String, RiskStrategy> strategyMap;
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@Override
public void afterPropertiesSet() {
// 从Spring容器中获取所有RiskStrategy实现类
Map<String, RiskStrategy> beans = applicationContext.getBeansOfType(RiskStrategy.class);
this.strategyMap = beans.entrySet().stream()
.collect(Collectors.toMap(
entry -> entry.getValue().strategyId(), // key用strategyId()
Map.Entry::getValue // value就是策略实例
));
}
public RiskStrategy getStrategy(String strategyId) {
RiskStrategy strategy = strategyMap.get(strategyId);
if (strategy == null) {
throw new IllegalArgumentException("No strategy found for id: " + strategyId);
}
return strategy;
}
}
这里的关键洞察是: Spring容器本身就是个天然的策略注册中心。 我们不需要自己维护Map,而是让Spring帮我们发现所有实现了 RiskStrategy 的Bean,再用 strategyId() 作为key组织起来。这样新增策略只需写个新类+ @Component ,重启服务自动生效,连配置都不用改。
2.3 策略选择器:让业务代码告别硬编码
客户端代码如果还这样写:
// ❌ 业务代码里硬编码策略ID
String strategyId = "rule_v1";
if (user.isVip()) {
strategyId = "vip_rule_v1";
}
RiskStrategy strategy = context.getStrategy(strategyId);
strategy.check(context);
那策略模式就白搭了——业务规则又散落到各处。真正的解法是 把策略选择逻辑单独封装 :
@Service
public class RiskStrategySelector {
@Autowired
private RiskStrategyContext strategyContext;
/**
* 根据风控上下文智能选择策略
* 规则:VIP用户用VIP专用策略;新用户用轻量规则;大额订单触发机器学习模型
*/
public RiskStrategy select(RiskContext context) {
if (context.getUser().isVip()) {
return strategyContext.getStrategy("vip_rule_v2");
}
if (context.getOrderAmount().compareTo(new BigDecimal("1000")) > 0) {
return strategyContext.getStrategy("ml_model_2024_q3");
}
return strategyContext.getStrategy("rule_based_v2");
}
}
业务Service里就干净了:
@Service
public class OrderService {
@Autowired
private RiskStrategySelector selector;
public void createOrder(Order order) {
RiskContext context = buildRiskContext(order);
RiskStrategy strategy = selector.select(context); // 一行代码搞定选择
RiskCheckResult result = strategy.check(context);
if (!result.isPass()) {
throw new RiskRejectException(result.getReason());
}
// ...继续下单流程
}
}
看到没?业务代码里再也看不到 "rule_v1" 这种魔法字符串,所有策略决策集中在 RiskStrategySelector 里,测试、修改、灰度都极其方便。这才是策略模式该有的样子—— 策略的“变”与业务的“稳”彻底隔离。
3. 实战案例:从零实现一个可配置的优惠券计算策略
光讲理论容易飘,现在用一个真实场景——电商优惠券计算——手把手带你写出生产可用的策略模式代码。这个案例覆盖了策略模式90%的痛点:多策略共存、运行时动态切换、Spring集成、配置驱动、异常兜底。
3.1 业务需求与策略划分
假设我们的优惠券系统支持三种计算方式:
- 满减券 :订单满300减50,门槛和减免值可配置;
- 折扣券 :打8折,折扣率可配置;
- 买赠券 :买iPhone 15送AirPods,需校验商品组合。
关键约束:
① 同一订单可能同时使用多张券,需按顺序应用;
② 运营后台要能随时开关某种策略;
③ 新增策略不能改现有代码。
3.2 策略接口定义与基础抽象
先定义核心接口,注意泛型和异常处理:
/**
* 优惠券计算策略接口
* @param <T> 计算输入参数类型(不同策略输入不同,如满减需金额,买赠需商品列表)
* @param <R> 计算结果类型(统一返回CouponCalculationResult)
*/
public interface CouponCalculationStrategy<T, R> {
/**
* 执行计算
* @param input 输入参数
* @return 计算结果(含是否成功、最终金额、明细等)
*/
CouponCalculationResult<R> calculate(T input);
/**
* 策略唯一ID,对应配置中心的key
* 例如:"coupon_strategy_full_reduction", "coupon_strategy_discount"
*/
String strategyId();
/**
* 策略名称,用于监控和日志
*/
String strategyName();
/**
* 是否启用此策略(支持配置中心动态开关)
*/
boolean isEnabled();
}
接着写一个抽象基类,封装公共能力:
public abstract class AbstractCouponStrategy<T, R> implements CouponCalculationStrategy<T, R> {
@Value("${coupon.strategy.enabled:true}") // 默认启用
protected boolean enabled;
@Override
public boolean isEnabled() {
return enabled;
}
/**
* 模板方法:统一的日志记录和异常包装
*/
@Override
public CouponCalculationResult<R> calculate(T input) {
long start = System.currentTimeMillis();
try {
log.info("Start calculating with strategy: {}, input: {}", strategyId(), input);
CouponCalculationResult<R> result = doCalculate(input);
log.info("Calculation completed in {}ms, result: {}",
System.currentTimeMillis() - start, result);
return result;
} catch (Exception e) {
log.error("Calculation failed for strategy: {}", strategyId(), e);
return CouponCalculationResult.<R>failed("策略执行异常: " + e.getMessage());
}
}
/**
* 子类必须实现的具体计算逻辑
*/
protected abstract CouponCalculationResult<R> doCalculate(T input);
}
这个抽象类做了三件事:① 统一开关控制( @Value 读配置);② 统一日志(记录耗时、输入、结果);③ 统一异常处理(避免策略内部异常炸掉整个流程)。所有具体策略继承它,专注写 doCalculate() 即可。
3.3 具体策略实现:满减、折扣、买赠
满减策略(FullReductionStrategy):
@Component
public class FullReductionStrategy extends AbstractCouponStrategy<FullReductionInput, FullReductionResult> {
// 从配置中心读取,支持动态刷新
@Value("${coupon.full.reduction.threshold:300.0}")
private BigDecimal threshold;
@Value("${coupon.full.reduction.amount:50.0}")
private BigDecimal reductionAmount;
@Override
public String strategyId() {
return "coupon_strategy_full_reduction";
}
@Override
public String strategyName() {
return "满减策略";
}
@Override
protected CouponCalculationResult<FullReductionResult> doCalculate(FullReductionInput input) {
if (input.getOrderAmount().compareTo(threshold) < 0) {
return CouponCalculationResult.failed("订单金额" + input.getOrderAmount() +
"未达到满减门槛" + threshold);
}
BigDecimal finalAmount = input.getOrderAmount().subtract(reductionAmount);
FullReductionResult result = new FullReductionResult();
result.setFinalAmount(finalAmount);
result.setReductionAmount(reductionAmount);
result.setThreshold(threshold);
return CouponCalculationResult.success(result);
}
}
折扣策略(DiscountStrategy):
@Component
public class DiscountStrategy extends AbstractCouponStrategy<DiscountInput, DiscountResult> {
@Value("${coupon.discount.rate:0.8}")
private BigDecimal discountRate; // 0.8表示8折
@Override
public String strategyId() {
return "coupon_strategy_discount";
}
@Override
public String strategyName() {
return "折扣策略";
}
@Override
protected CouponCalculationResult<DiscountResult> doCalculate(DiscountInput input) {
BigDecimal finalAmount = input.getOrderAmount().multiply(discountRate);
DiscountResult result = new DiscountResult();
result.setFinalAmount(finalAmount);
result.setDiscountRate(discountRate);
return CouponCalculationResult.success(result);
}
}
买赠策略(BuyGiftStrategy)——展示复杂业务逻辑:
@Component
public class BuyGiftStrategy extends AbstractCouponStrategy<BuyGiftInput, BuyGiftResult> {
@Autowired
private ProductService productService; // 注入Spring Bean,满减/折扣策略用不到,但买赠需要查商品库
@Autowired
private InventoryService inventoryService;
@Value("${coupon.buy.gift.main.sku:IPHONE15}")
private String mainSku;
@Value("${coupon.buy.gift.gift.sku:AIRPODS}")
private String giftSku;
@Override
public String strategyId() {
return "coupon_strategy_buy_gift";
}
@Override
public String strategyName() {
return "买赠策略";
}
@Override
protected CouponCalculationResult<BuyGiftResult> doCalculate(BuyGiftInput input) {
// 1. 校验主商品是否存在且在购物车
Product mainProduct = productService.getBySku(mainSku);
if (mainProduct == null || !input.getCartItems().containsKey(mainSku)) {
return CouponCalculationResult.failed("未购买主商品: " + mainSku);
}
// 2. 校验赠品库存
int giftStock = inventoryService.getStock(giftSku);
if (giftStock <= 0) {
return CouponCalculationResult.failed("赠品" + giftSku + "库存不足");
}
// 3. 计算结果:赠品不减金额,但需记录
BuyGiftResult result = new BuyGiftResult();
result.setGiftSku(giftSku);
result.setGiftQuantity(1);
result.setFinalAmount(input.getOrderAmount()); // 买赠不改变订单金额
return CouponCalculationResult.success(result);
}
}
看到区别了吗?买赠策略因为要查商品和库存,自然需要注入 ProductService 和 InventoryService ,而满减、折扣策略完全不需要。这就是策略模式的价值—— 每个策略只关心自己的事,该用什么Bean就注入什么,互不干扰。 如果用if-else写,这些依赖注入逻辑全得堆在同一个Service里,代码会臃肿不堪。
3.4 策略上下文与选择器:让业务代码一行调用
策略容器 CouponStrategyContext 沿用前文思路,用Spring自动装配:
@Component
public class CouponStrategyContext {
private final Map<String, CouponCalculationStrategy> strategyMap;
public CouponStrategyContext(Map<String, CouponCalculationStrategy> strategies) {
// Spring会自动把所有CouponCalculationStrategy实现类注入进来
this.strategyMap = strategies;
}
public <T, R> CouponCalculationStrategy<T, R> getStrategy(String strategyId) {
CouponCalculationStrategy strategy = strategyMap.get(strategyId);
if (strategy == null) {
throw new IllegalArgumentException("No coupon strategy found for id: " + strategyId);
}
if (!strategy.isEnabled()) {
throw new IllegalStateException("Coupon strategy is disabled: " + strategyId);
}
return strategy;
}
}
策略选择器 CouponStrategySelector 更进一步,支持配置驱动:
@Service
public class CouponStrategySelector {
@Autowired
private CouponStrategyContext strategyContext;
/**
* 从配置中心读取策略优先级列表,例如:["coupon_strategy_full_reduction", "coupon_strategy_discount"]
* 运营可随时调整顺序
*/
@Value("#{'${coupon.strategy.priority}'.split(',')}")
private List<String> priorityList;
/**
* 根据优惠券类型和订单特征选择策略
* 支持降级:当首选策略不可用时,自动尝试次选
*/
public <T, R> CouponCalculationStrategy<T, R> select(String couponType, T input) {
for (String strategyId : priorityList) {
try {
CouponCalculationStrategy<T, R> strategy = strategyContext.getStrategy(strategyId);
// 额外校验:策略是否支持当前优惠券类型
if (supportsCouponType(strategy, couponType)) {
return strategy;
}
} catch (Exception e) {
// 当前策略异常或禁用,跳过,尝试下一个
log.warn("Skip strategy {} due to error: {}", strategyId, e.getMessage());
}
}
throw new IllegalStateException("No available strategy for coupon type: " + couponType);
}
private boolean supportsCouponType(CouponCalculationStrategy strategy, String couponType) {
// 简单示例:策略ID包含类型关键词
return strategy.strategyId().contains(couponType.toLowerCase());
}
}
业务Service调用就极简:
@Service
public class CouponService {
@Autowired
private CouponStrategySelector selector;
public CouponCalculationResult calculate(CouponRequest request) {
// 根据请求中的couponType选择策略
CouponCalculationStrategy strategy = selector.select(
request.getCouponType(),
request.getInput()
);
return strategy.calculate(request.getInput());
}
}
配置文件 application.yml 示例:
coupon:
strategy:
priority: "coupon_strategy_full_reduction,coupon_strategy_discount,coupon_strategy_buy_gift"
full:
reduction:
threshold: 300.0
amount: 50.0
discount:
rate: 0.85
buy:
gift:
main.sku: IPHONE15
gift.sku: AIRPODS
运营想把折扣策略提到第一位?改个配置就行,不用发版。想临时禁用买赠策略?把 coupon.strategy.buy_gift.enabled=false 加进去,瞬间生效。这才是策略模式在真实世界该有的样子。
4. 高阶技巧与避坑指南:那些只有踩过坑才知道的事
策略模式看似简单,但真正在高并发、多团队协作的项目里落地,会遇到一堆教科书不提的坑。下面这些全是血泪教训换来的经验,每一条都值得你记在小本本上。
4.1 坑一:Spring Bean循环依赖——策略里调用Service,Service又依赖策略容器
这是新手必踩的雷。典型场景:
OrderService需要调用CouponService计算优惠;CouponService里用到了CouponStrategyContext;- 某个策略
BuyGiftStrategy又需要注入OrderService(比如要查订单历史);
结果启动报错:BeanCurrentlyInCreationException。
根本原因: Spring创建Bean时形成闭环:OrderService → CouponService → CouponStrategyContext → BuyGiftStrategy → OrderService。
解决方案分三级:
-
初级:构造器注入 + @Lazy(推荐)
@Component public class BuyGiftStrategy extends AbstractCouponStrategy<BuyGiftInput, BuyGiftResult> { private final ProductService productService; private final InventoryService inventoryService; private final OrderService orderService; // 关键:加@Lazy public BuyGiftStrategy(ProductService productService, InventoryService inventoryService, @Lazy OrderService orderService) { this.productService = productService; this.inventoryService = inventoryService; this.orderService = orderService; } }@Lazy告诉Spring:别急着创建OrderService,等第一次调用时再代理创建。这是最轻量的解法。 -
中级:策略内通过ApplicationContext获取Bean(慎用)
@Component public class BuyGiftStrategy extends AbstractCouponStrategy<...> { @Autowired private ApplicationContext context; private OrderService getOrderService() { return context.getBean(OrderService.class); } }缺点:破坏了依赖注入的透明性,单元测试难mock。
-
高级:领域事件解耦(终极方案)
买赠策略不该直接调用OrderService,而应发布事件:public class GiftGrantedEvent { private String orderId; private String giftSku; }OrderService监听此事件,异步处理赠品发放。这样策略和订单服务彻底解耦,还能提升性能。
4.2 坑二:策略加载时机导致的NPE——Context还没初始化完,策略就急着用
现象:服务刚启动,第一个请求就报 NullPointerException ,堆栈指向 strategyContext.getStrategy() 。
原因: CouponStrategyContext 的构造函数执行时, strategyMap 还是null,而某个Service的 @PostConstruct 方法或 init() 方法在Context初始化前就调用了它。
根治方案:用 SmartInitializingSingleton 确保初始化完成
@Component
public class CouponStrategyContext implements SmartInitializingSingleton {
private Map<String, CouponCalculationStrategy> strategyMap;
@Override
public void afterSingletonsInstantiated() {
// 此方法在所有单例Bean初始化完成后才调用
Map<String, CouponCalculationStrategy> strategies =
applicationContext.getBeansOfType(CouponCalculationStrategy.class);
this.strategyMap = strategies.entrySet().stream()
.filter(entry -> entry.getValue().isEnabled())
.collect(Collectors.toMap(
entry -> entry.getValue().strategyId(),
Map.Entry::getValue
));
log.info("Loaded {} coupon strategies", strategyMap.size());
}
}
SmartInitializingSingleton 是Spring提供的钩子,比 InitializingBean 更可靠,确保所有依赖Bean都ready了才执行。
4.3 坑三:策略ID冲突——两个团队各自开发,起了一样的strategyId
线上事故现场:A团队开发的“新人专享券”策略ID设为 "new_user_coupon" ,B团队开发的“新客首单立减”也用了同样ID。结果B团队上线后,A团队的策略被悄悄替换了,大量新人优惠失效。
防御措施:强制命名规范 + 启动校验
@Component
public class StrategyIdValidator implements ApplicationContextAware {
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
Map<String, CouponCalculationStrategy> strategies =
applicationContext.getBeansOfType(CouponCalculationStrategy.class);
Map<String, List<String>> idToClasses = strategies.entrySet().stream()
.collect(Collectors.groupingBy(
entry -> entry.getValue().strategyId(),
Collectors.mapping(Map.Entry::getKey, Collectors.toList())
));
idToClasses.entrySet().stream()
.filter(entry -> entry.getValue().size() > 1)
.findFirst()
.ifPresent(duplicate -> {
throw new IllegalStateException(
"Duplicate strategyId detected: " + duplicate.getKey() +
", classes: " + duplicate.getValue());
});
}
}
服务启动时自动扫描,发现重复ID直接抛异常,阻止带病上线。
4.4 坑四:策略执行超时——某个策略卡死,拖垮整个下单链路
某次大促,买赠策略因库存服务抖动, inventoryService.getStock() 阻塞30秒,导致所有订单创建超时。
熔断与降级方案:
@Component
public class ResilientCouponStrategyContext {
private final Map<String, CouponCalculationStrategy> strategyMap;
// 使用Hystrix或Sentinel做熔断(此处用伪代码)
private final CircuitBreaker circuitBreaker =
CircuitBreaker.ofDefaults("coupon_calculation");
public <T, R> CouponCalculationResult<R> calculateWithFallback(
String strategyId, T input, Supplier<CouponCalculationResult<R>> fallback) {
return Try.ofSupplier(() -> {
CouponCalculationStrategy<T, R> strategy = strategyMap.get(strategyId);
if (strategy == null) {
throw new IllegalArgumentException("Unknown strategy: " + strategyId);
}
return strategy.calculate(input);
})
.recover(throwable -> {
log.warn("Strategy {} failed, using fallback", strategyId, throwable);
return fallback.get();
})
.get();
}
}
更专业的做法是集成Sentinel,对每个策略ID设置独立QPS限流和熔断规则,确保一个策略故障不影响全局。
4.5 坑五:策略测试覆盖率低——只测了happy path,没测异常分支
很多团队的策略单元测试长这样:
@Test
public void testFullReductionSuccess() {
FullReductionInput input = new FullReductionInput(new BigDecimal("500"));
FullReductionStrategy strategy = new FullReductionStrategy();
CouponCalculationResult result = strategy.calculate(input);
assertTrue(result.isSuccess());
}
这只能证明“能跑”,不能证明“跑得稳”。 必须覆盖的测试场景:
- 输入边界:金额=阈值、金额<阈值、金额为负数;
- 配置异常:
@Value注入的配置为空或非法(如discount.rate=1.5); - 依赖异常:
productService.getBySku()抛出ProductNotFoundException; - 并发安全:多个线程同时调用同一策略实例(策略类必须是无状态的!)。
我的测试模板:
@Test
public void testFullReductionWhenAmountBelowThreshold() {
// Given
FullReductionInput input = new FullReductionInput(new BigDecimal("200")); // 低于300
FullReductionStrategy strategy = new FullReductionStrategy();
ReflectionTestUtils.setField(strategy, "threshold", new BigDecimal("300"));
// When
CouponCalculationResult result = strategy.calculate(input);
// Then
assertFalse(result.isSuccess());
assertThat(result.getErrorMessage()).contains("未达到满减门槛");
}
用 ReflectionTestUtils 模拟各种配置,确保策略在任何条件下都有明确行为。
5. 策略模式的延伸与进化:当它不再只是“策略”
策略模式不是终点,而是架构演进的起点。在真实项目中,它往往会自然生长出更强大的形态。
5.1 策略+规则引擎:从静态策略到动态规则
上面的优惠券例子,策略逻辑是硬编码在Java类里的。但运营需求千变万化:今天要“满300减50”,明天要“满500减80再送积分”,后天要“新用户首单额外95折”。如果每次改都发版,产品同学会天天堵你工位。
升级方案:策略接口不变,但实现类变成规则引擎的适配器。
@Component
public class DroolsRuleStrategy extends AbstractCouponStrategy<RuleInput, RuleResult> {
@Autowired
private KieSession kieSession; // Drools规则引擎会话
@Override
protected CouponCalculationResult<RuleResult> doCalculate(RuleInput input) {
// 将输入数据插入规则引擎
kieSession.insert(input);
kieSession.fireAllRules(); // 执行所有匹配规则
// 从规则引擎获取结果
RuleResult result = input.getResult();
return CouponCalculationResult.success(result);
}
}
运营在后台配置Drools规则(DRL文件),策略类只是个搬运工。规则变更无需发版,实时生效。策略模式在这里成了 业务规则与技术实现的翻译层 。
5.2 策略+状态机:处理有状态的复杂流程
有些场景策略不是一次性的,而是有状态的。比如风控:
- 初始状态:用规则策略快速拦截明显黑产;
- 若通过,进入“机器学习模型”策略;
- 若模型分数在灰色地带,触发“人工审核”策略;
- 人工审核后,状态变为“已通过”或“已拒绝”。
这时策略模式要和状态机结合:
public enum RiskState {
RULE_CHECK, ML_MODEL_CHECK, MANUAL_REVIEW, APPROVED, REJECTED
}
public interface RiskStrategy {
RiskState nextState();
RiskCheckResult execute(RiskContext context);
}
每个策略不仅计算,还决定下一步走哪个状态。Spring State Machine框架能完美支撑这种模式。
5.3 策略+插件化:让第三方也能扩展你的系统
如果你的系统要开放给合作伙伴(如ISV),让他们写自己的策略,就得考虑插件化。核心是:
- 策略接口定义为独立jar包,供所有方依赖;
- 主程序通过
ServiceLoader或自定义ClassLoader加载外部jar中的策略类; - 策略类必须实现
PluginMetadata接口,提供版本、作者、描述等元信息; - 加载时做安全沙箱(禁止反射、网络、文件IO等危险操作)。
这已经超出策略模式本身,但它的接口契约是插件化的基石。没有清晰的 RiskStrategy 接口,插件化就是空中楼阁。
5.4 策略模式的“反模式”警示:什么时候不该用?
最后必须强调:策略模式不是银弹。滥用它反而制造复杂度。以下情况请三思:
- 策略只有两种且永不增加 :比如“同步调用”和“异步调用”,直接用
if (async) { ... } else { ... }更直白; - 策略间共享大量状态 :比如所有策略都要读写同一个缓存Map,强行拆分会增加同步开销;
- 策略逻辑极度简单 :如“返回true”和“返回false”的两个策略,纯属过度设计;
- 团队成员Java基础薄弱 :策略模式增加了类数量和调用链路,新人理解成本高,不如先用清晰的if-else,等业务复杂了再重构。
我坚持一个原则: 策略模式的价值,必须大于它引入的复杂度。 衡量标准很简单——当你为第3种策略写代码时,是否比写第2种时更快、更安全、更自信?如果是,说明它正发挥价值;如果每次新增都要重读Context源码,那赶紧停下来,重新审视设计。
6. 总结:策略模式的本质,是给变化装上方向盘
写完这篇,我重新翻了翻Gang of Four那本《设计模式》,里面对策略模式的定义只有一页纸。但真实世界的复杂度,远超那页纸的想象。策略模式教给我们的,从来不是怎么写 interface 和 implements ,而是 如何识别变化、隔离变化、并优雅地驾驭变化。
它逼你问自己:这个逻辑的边界在哪?哪些部分会变?变的频率有多高?变的时候,影响范围有多大?当这些问题有了答案,策略模式的结构就自然浮现了——接口是边界的宣言,策略类是变化的容器,Context是变化的调度中心,而选择器是变化的决策大脑。
那些在面试中背得滚瓜烂熟的“开闭原则”“解耦”,在真实的代码战场里,就是凌晨三点修复一个因新增策略导致的NPE时,你庆幸自己当初没把所有逻辑塞进一个方法里;就是运营同学在后台改了个配置,新优惠规则秒级生效,而你正喝着咖啡看监控曲线平稳上升;就是新来的同事第一天就能看懂优惠券怎么计算,因为他只需要关注 FullReductionStrategy 这一个类,而不是在400行的if-else迷宫里找出口。
所以别再把它当成一个“设计模式考点”,而要当成一种 工程思维习惯 。下次当你看到一段if-else,或者一个越来越胖的Service类,或者一个被所有人敬畏地称为“上帝类”的代码文件时,停下来,问问自己:这里面,有没有可以被策略模式拯救的部分?
我个人在实际操作中的体会是:策略模式用得最好的团队,往往不是技术最强的,而是对业务理解最深的。因为他们知道,真正的设计,永远始于对业务变化的敬畏,而非对技术概念的炫技。
更多推荐



所有评论(0)