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。

解决方案分三级:

  1. 初级:构造器注入 + @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 ,等第一次调用时再代理创建。这是最轻量的解法。

  2. 中级:策略内通过ApplicationContext获取Bean(慎用)

    @Component
    public class BuyGiftStrategy extends AbstractCouponStrategy<...> {
        @Autowired
        private ApplicationContext context;
        
        private OrderService getOrderService() {
            return context.getBean(OrderService.class);
        }
    }
    

    缺点:破坏了依赖注入的透明性,单元测试难mock。

  3. 高级:领域事件解耦(终极方案)
    买赠策略不该直接调用 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类,或者一个被所有人敬畏地称为“上帝类”的代码文件时,停下来,问问自己:这里面,有没有可以被策略模式拯救的部分?

我个人在实际操作中的体会是:策略模式用得最好的团队,往往不是技术最强的,而是对业务理解最深的。因为他们知道,真正的设计,永远始于对业务变化的敬畏,而非对技术概念的炫技。

更多推荐