1. 为什么“策略模式”在Java项目里不是教科书摆设,而是每天都在救火的工具?

我第一次在生产环境里真正“看见”策略模式,不是在GOF那本砖头厚的设计模式书里,而是在一个凌晨三点的告警群里。订单履约系统突然开始大量超时,日志里反复刷着同一行报错: java.lang.IllegalArgumentException: Unsupported payment method: wechat_applet 。排查下来,问题出在一个硬编码的 if-else 链上——新增微信小程序支付渠道时,开发同学直接在 PaymentService.process() 方法里加了三行 if (type.equals("wechat_applet")) { ... } ,却忘了同步更新另一处风控校验逻辑。结果新渠道能下单,但风控直接拦截,用户付款后卡在“处理中”,客服电话被打爆。

这就是策略模式最真实、最粗粝的出场方式:它不是为了解决“如何写得更优雅”的哲学问题,而是为了终结那种让人头皮发麻的、牵一发而动全身的硬编码泥潭。你翻看任何一份稍具规模的Java后端代码库,几乎都能找到至少三处“条件分支爆炸”的现场——支付方式选择、优惠券计算规则、消息推送渠道、风控规则引擎、甚至日志脱敏策略。这些地方的共同点是: 行为逻辑多变、未来必然扩展、且不同行为之间完全正交,互不影响 。这时候,用一堆 if-else switch-case 去堆砌,无异于在代码里埋下定时炸弹。每次新增一种策略,你都得战战兢兢地打开那个臃肿的方法,逐行检查所有分支是否覆盖、所有边界是否处理、所有异常是否兜底。而策略模式,就是把这种高风险、高重复的手工劳动,变成一次性的、可插拔的、有明确契约的模块化操作。

它和Java面试里常考的“八股文”答案不同——面试官可能只关心你能否画出UML类图、能否背出“定义”和“角色”。但在真实世界里,它的价值体现在三个具体维度:第一, 隔离变化 。支付渠道从支付宝、微信,到银联、PayPal、甚至未来的数字人民币,每新增一种,你只需写一个新类,实现一个接口,注册进Spring容器,其他代码一行不动;第二, 消除条件判断污染 。那个曾经塞满200行 if-else OrderService.calculateDiscount() 方法,可以被精简成一行 discountStrategy.apply(order) ,干净得像刚洗过的玻璃;第三, 支撑运行时动态决策 。比如根据用户等级、地域、设备类型,实时组合出不同的优惠策略,这在 switch 里根本无法优雅实现,但在策略模式+简单工厂或Spring @Qualifier 注入下,就是几行配置的事。所以,别再把它当成设计模式考试里的一个名词解释。把它看作Java工程师手边一把趁手的瑞士军刀——当你面对“这个逻辑以后肯定要变,而且会变很多次”的需求时,它就是你第一时间该摸向的工具。

2. 策略模式的本质:不是“多态”,而是“契约驱动的算法解耦”

很多人初学策略模式,容易陷入一个认知陷阱:把它等同于“用多态代替 if-else ”。这没错,但太浅了。多态是Java语言的底层能力,而策略模式是一种 基于明确契约(Contract)的、面向行为(Behavior)的解耦范式 。它的核心不在于“怎么调用”,而在于“谁来保证行为的一致性”。

我们来看一个反面教材。假设你写了一个 Sorter 类,里面有两个方法: bubbleSort(int[] arr) quickSort(int[] arr) 。然后你在业务代码里这样用:

if (useQuickSort) {
    sorter.quickSort(data);
} else {
    sorter.bubbleSort(data);
}

这看起来像是策略,但它完全违背了策略模式的精神。问题在哪?第一, Sorter 类本身成了策略的“中心化仓库”,所有排序算法都挤在一个类里,违反了单一职责;第二,调用方(业务代码)依然需要知道具体的算法名称( quickSort bubbleSort ),并手动做条件判断,没有实现真正的解耦;第三,最关键的是, 没有定义统一的契约 bubbleSort quickSort 只是两个名字相似的方法,它们的输入、输出、异常、性能特征、线程安全性,没有任何强制约束。今天你改了 quickSort 的签名,业务代码可能就编译不过,或者运行时抛出 ArrayIndexOutOfBoundsException ——而这一切,契约本该提前拦住。

策略模式的正确打开方式,是从定义一个清晰、稳定、最小化的接口开始。这个接口就是“契约”。以支付策略为例:

public interface PaymentStrategy {
    /**
     * 执行支付操作
     * @param order 订单信息,必须包含金额、用户ID、支付渠道标识
     * @return 支付结果,包含交易流水号、状态、时间戳
     * @throws PaymentException 当支付失败且不可重试时抛出(如余额不足)
     * @throws TechnicalException 当系统级错误发生时抛出(如网络超时、签名失败)
     */
    PaymentResult process(PaymentOrder order) throws PaymentException, TechnicalException;
    
    /**
     * 获取此策略支持的支付渠道标识
     * 用于运行时策略选择器(Selector)进行匹配
     */
    String getSupportedChannel();
}

看到这个接口,你就立刻明白了策略模式的“契约”力量:

  • 输入明确 PaymentOrder 是一个POJO,它的字段( amount , userId , channel )就是所有策略必须消费的“原材料”,不允许策略自己去数据库查用户余额;
  • 输出统一 :无论支付宝还是微信,返回的都是 PaymentResult ,调用方无需关心内部是调了哪个API、用了哪种加密;
  • 异常分类 PaymentException 代表业务失败(用户操作问题), TechnicalException 代表系统失败(需要告警重试),这直接决定了上层事务的回滚策略;
  • 能力自述 getSupportedChannel() 让策略自己“声明”它能做什么,而不是让调用方去猜、去硬编码匹配逻辑。

这个接口,就是所有策略实现类的“宪法”。 AlipayStrategy WechatPayStrategy UnionPayStrategy ,它们可以内部用不同的HTTP客户端、不同的加签算法、不同的重试机制,但只要它们实现了 PaymentStrategy ,它们就承诺了上述所有行为。这才是策略模式的精髓: 它不关心你内部怎么实现(算法细节),只强制你对外提供什么能力(契约接口) 。这种解耦,比单纯的多态深刻得多。它让系统具备了“可预测性”——我知道只要拿到一个 PaymentStrategy ,我就一定能调用 process() ,一定能得到 PaymentResult ,一定能按约定处理两种异常。这种确定性,是大型系统稳定运行的基石。

3. 从零手写一个可落地的策略模式:以电商优惠券计算为例

光讲理论不够,我们来亲手实现一个真实场景——电商优惠券计算。这个场景完美契合策略模式:优惠规则五花八门(满减、折扣、直降、买赠、阶梯价),每种规则的计算逻辑天差地别,且未来必然不断新增。我们一步步构建,重点看那些教科书里不会写的实操细节。

3.1 定义核心契约:CouponCalculationStrategy

首先,定义一个足够健壮的策略接口。这里的关键是思考“计算优惠”这个行为,到底需要什么输入、产出什么、以及边界在哪里:

/**
 * 优惠券计算策略接口
 * 契约核心:给定订单快照和优惠券,返回最终应扣减金额及明细
 */
public interface CouponCalculationStrategy {

    /**
     * 计算优惠金额
     * @param orderSnapshot 订单快照(含商品列表、总金额、收货地址等)
     * @param coupon 优惠券实体(含面额、门槛、有效期、适用范围等)
     * @return 计算结果,包含实际抵扣金额、明细描述、是否满足门槛
     * @throws CouponInvalidException 优惠券已失效、已被使用、不在有效期内等
     * @throws RuleViolationException 优惠券不适用于当前订单(如品类不符、地区限制)
     */
    CouponCalculationResult calculate(OrderSnapshot orderSnapshot, Coupon coupon)
            throws CouponInvalidException, RuleViolationException;

    /**
     * 获取此策略唯一标识符,用于配置中心或数据库存储
     * 格式:coupon_type:sub_type,如 "FULL_REDUCTION:GENERAL", "DISCOUNT:VIP"
     */
    String getStrategyCode();

    /**
     * 获取策略的友好名称,用于日志和监控
     */
    String getStrategyName();
}

注意几个关键设计点:

  • 输入是快照(Snapshot),而非实时对象 OrderSnapshot 是不可变的DTO,避免策略在计算过程中意外修改了原始订单状态。这是防止并发问题的常见技巧。
  • 异常分类明确 CouponInvalidException 是用户侧问题(券过期), RuleViolationException 是业务规则问题(券不能用),这直接影响前端提示语(“券已过期” vs “该券不适用于当前商品”)。
  • getStrategyCode() 是灵魂 :它让策略可以脱离Java类名存在。未来你可以在数据库里存一条记录: strategy_code = 'FULL_REDUCTION:REGIONAL' class_name = 'com.xxx.RegionalFullReductionStrategy' 。运行时通过反射加载,实现真正的“配置即代码”。

3.2 实现两个典型策略:满减与折扣

先实现最常用的“满减”策略:

@Component
@Qualifier("fullReductionGeneral")
public class FullReductionGeneralStrategy implements CouponCalculationStrategy {

    private static final Logger log = LoggerFactory.getLogger(FullReductionGeneralStrategy.class);

    @Override
    public CouponCalculationResult calculate(OrderSnapshot orderSnapshot, Coupon coupon)
            throws CouponInvalidException, RuleViolationException {
        
        // 1. 检查基础有效性(复用通用校验器,非策略内逻辑)
        validateCommonRules(orderSnapshot, coupon);

        // 2. 检查满减门槛:订单实付金额 >= 门槛
        BigDecimal orderPayable = orderSnapshot.getPayableAmount();
        BigDecimal threshold = coupon.getThreshold();
        if (orderPayable.compareTo(threshold) < 0) {
            throw new RuleViolationException(
                String.format("订单实付金额 %.2f 元未达到满减门槛 %.2f 元", 
                    orderPayable, threshold));
        }

        // 3. 计算抵扣:取券面额与订单金额的较小值(防负数)
        BigDecimal discountAmount = coupon.getDiscountAmount()
            .min(orderPayable); // 关键!避免抵扣后金额为负

        // 4. 构建结果
        return CouponCalculationResult.builder()
                .discountAmount(discountAmount)
                .detailDescription(String.format("满 %.2f 减 %.2f", threshold, discountAmount))
                .meetsThreshold(true)
                .build();
    }

    private void validateCommonRules(OrderSnapshot orderSnapshot, Coupon coupon) 
            throws CouponInvalidException, RuleViolationException {
        // 这里可以调用一个独立的CouponValidator服务
        // 检查有效期、使用次数、黑名单等,避免每个策略重复写
    }

    @Override
    public String getStrategyCode() {
        return "FULL_REDUCTION:GENERAL";
    }

    @Override
    public String getStrategyName() {
        return "通用满减";
    }
}

再实现一个“会员专属折扣”策略,展示策略间的差异:

@Component
@Qualifier("discountVip")
public class DiscountVipStrategy implements CouponCalculationStrategy {

    private final UserService userService; // 依赖注入,策略可以有自己的服务

    public DiscountVipStrategy(UserService userService) {
        this.userService = userService;
    }

    @Override
    public CouponCalculationResult calculate(OrderSnapshot orderSnapshot, Coupon coupon)
            throws CouponInvalidException, RuleViolationException {
        
        validateCommonRules(orderSnapshot, coupon);

        // 1. 特殊校验:必须是VIP用户
        User user = userService.findById(orderSnapshot.getUserId());
        if (!"VIP".equals(user.getLevel())) {
            throw new RuleViolationException("仅限VIP用户使用");
        }

        // 2. 计算折扣:原价 * 折扣率,但有上限(券面额)
        BigDecimal originalPrice = orderSnapshot.getOriginalAmount();
        BigDecimal discountRate = coupon.getDiscountRate(); // 如0.95表示95折,即打5折
        BigDecimal maxDiscount = coupon.getMaxDiscountAmount(); // 防止折扣过大

        BigDecimal discountAmount = originalPrice.multiply(BigDecimal.ONE.subtract(discountRate))
            .min(maxDiscount); // 取计算值与上限的较小值

        return CouponCalculationResult.builder()
                .discountAmount(discountAmount)
                .detailDescription(String.format("VIP专享 %.0f 折,最高减 %.2f 元", 
                    discountRate.multiply(new BigDecimal("100")), maxDiscount))
                .meetsThreshold(true) // VIP折扣无门槛
                .build();
    }

    // ... getStrategyCode(), getStrategyName() ...
}

提示:策略类内部可以自由注入其他服务(如 UserService ),这体现了策略的“自治性”。它不是一个孤立的算法,而是一个可以访问领域上下文的完整业务组件。

3.3 构建策略上下文(Context)与运行时选择器

策略模式的“大脑”是 Context 。它不持有具体策略,而是持有一个策略引用,并提供统一的调用入口:

@Service
public class CouponCalculationContext {

    // Spring会自动注入所有实现了CouponCalculationStrategy的Bean
    private final Map<String, CouponCalculationStrategy> strategyMap;

    public CouponCalculationContext(List<CouponCalculationStrategy> strategies) {
        // 将所有策略按getStrategyCode()建立索引
        this.strategyMap = strategies.stream()
                .collect(Collectors.toMap(
                    CouponCalculationStrategy::getStrategyCode,
                    Function.identity(),
                    (existing, replacement) -> existing // 冲突时保留第一个
                ));
    }

    /**
     * 核心方法:根据优惠券类型,选择并执行对应策略
     * @param orderSnapshot 订单快照
     * @param coupon 优惠券
     * @return 计算结果
     */
    public CouponCalculationResult execute(OrderSnapshot orderSnapshot, Coupon coupon) {
        String strategyCode = coupon.getStrategyCode(); // 从优惠券实体中读取
        CouponCalculationStrategy strategy = strategyMap.get(strategyCode);

        if (strategy == null) {
            throw new IllegalStateException(
                String.format("未找到策略实现类,strategyCode=%s", strategyCode));
        }

        try {
            return strategy.calculate(orderSnapshot, coupon);
        } catch (CouponInvalidException | RuleViolationException e) {
            // 统一包装业务异常,便于上层处理
            throw new CouponCalculationException(e.getMessage(), e);
        }
    }
}

这个 Context 的设计非常务实:

  • 构造器注入所有策略 :利用Spring的 List<T> 注入特性,自动收集所有 @Component 化的策略,无需手动 new
  • Map 索引加速查找 getStrategyCode() 作为Key,O(1)时间复杂度完成策略定位,比遍历列表高效得多。
  • 异常统一兜底 :将策略内部抛出的各类业务异常,统一包装为 CouponCalculationException ,上层服务(如 OrderService )只需捕获这一种异常即可。

最后,业务代码调用变得极其简洁:

@Service
public class OrderService {

    private final CouponCalculationContext calculationContext;

    public OrderService(CouponCalculationContext calculationContext) {
        this.calculationContext = calculationContext;
    }

    public Order createOrder(CreateOrderRequest request) {
        OrderSnapshot snapshot = buildOrderSnapshot(request);
        Coupon coupon = couponRepository.findById(request.getCouponId());

        // 一行代码,完成所有策略选择与计算
        CouponCalculationResult result = calculationContext.execute(snapshot, coupon);

        // 后续逻辑:更新订单金额、生成优惠明细等...
        return orderRepository.save(...);
    }
}

4. 策略模式的实战陷阱与避坑指南:那些文档里绝不会写的血泪教训

策略模式看似简单,但真正在复杂项目里落地时,会遇到一堆“文档里绝不会写,但踩了就疼”的坑。这些都是我在三个不同电商平台重构优惠系统时,用加班和线上事故换来的经验。

4.1 陷阱一:策略的“生命周期”管理混乱,导致内存泄漏

最常见的错误,是把策略当作一个简单的工具类,随意在策略内部缓存大量数据。比如,为了提升性能,你在 FullReductionGeneralStrategy 里加了一个静态 ConcurrentHashMap ,用来缓存“城市ID -> 满减门槛”的映射:

// ❌ 危险!静态缓存 + 策略单例 = 内存泄漏温床
private static final Map<Long, BigDecimal> CITY_THRESHOLD_CACHE = new ConcurrentHashMap<>();

问题在哪?Spring默认的Bean作用域是 singleton (单例)。这意味着整个JVM生命周期内, FullReductionGeneralStrategy 只有一个实例。而你的静态缓存,会随着业务增长,不断往里塞数据,永不释放。当系统运行几个月后,这个Map可能占掉几百MB内存,成为GC的噩梦。

正确做法是:策略必须是无状态(Stateless)的,所有需要缓存的数据,交给专门的缓存服务(如Redis、Caffeine)管理。

@Component
public class FullReductionGeneralStrategy implements CouponCalculationStrategy {

    private final CityThresholdService cityThresholdService; // 依赖注入缓存服务

    public FullReductionGeneralStrategy(CityThresholdService cityThresholdService) {
        this.cityThresholdService = cityThresholdService;
    }

    @Override
    public CouponCalculationResult calculate(OrderSnapshot orderSnapshot, Coupon coupon) {
        // 从缓存服务获取,服务内部负责LRU、过期等逻辑
        BigDecimal threshold = cityThresholdService.getThresholdByCityId(
            orderSnapshot.getShippingAddress().getCityId());
        // ... 后续计算
    }
}

注意: CityThresholdService 本身可以是单例,它内部管理自己的缓存。策略只负责“使用”缓存,不负责“拥有”缓存。这是职责分离的铁律。

4.2 陷阱二:策略间“悄悄话”,破坏了封装性

另一个高频坑,是策略之间开始互相调用。比如,你发现“满减”策略在计算时,需要知道用户是否是VIP,以便叠加一个额外折扣。于是你写了:

// ❌ 错误示范:策略A直接调用策略B
public class FullReductionGeneralStrategy implements CouponCalculationStrategy {
    @Autowired
    private DiscountVipStrategy vipStrategy; // 直接注入另一个策略

    @Override
    public CouponCalculationResult calculate(...) {
        // ... 计算满减 ...
        // 然后偷偷调用VIP策略
        CouponCalculationResult vipResult = vipStrategy.calculate(...);
        // ... 合并结果 ...
    }
}

这彻底摧毁了策略模式的价值。策略的初衷是“正交”和“可替换”。现在 FullReductionGeneralStrategy 强依赖 DiscountVipStrategy ,如果哪天你要下线VIP折扣, FullReductionGeneralStrategy 就会直接编译失败。这又回到了“牵一发而动全身”的老路。

正确解法是:引入“组合策略”(Composite Strategy)或“策略链”(Strategy Chain)。 创建一个新的策略类,它内部持有多个子策略的引用,并定义它们的执行顺序和组合逻辑:

@Component
@Qualifier("fullReductionWithVipBonus")
public class FullReductionWithVipBonusStrategy implements CouponCalculationStrategy {

    private final FullReductionGeneralStrategy fullReductionStrategy;
    private final DiscountVipStrategy vipStrategy;
    private final UserService userService;

    public FullReductionWithVipBonusStrategy(FullReductionGeneralStrategy fullReductionStrategy,
                                             DiscountVipStrategy vipStrategy,
                                             UserService userService) {
        this.fullReductionStrategy = fullReductionStrategy;
        this.vipStrategy = vipStrategy;
        this.userService = userService;
    }

    @Override
    public CouponCalculationResult calculate(OrderSnapshot orderSnapshot, Coupon coupon) {
        // 1. 先执行满减
        CouponCalculationResult baseResult = fullReductionStrategy.calculate(orderSnapshot, coupon);
        
        // 2. 判断是否VIP,决定是否叠加
        if (isVipUser(orderSnapshot.getUserId())) {
            // 3. 执行VIP折扣(注意:传入的是满减后的订单快照!)
            OrderSnapshot afterReductionSnapshot = buildSnapshotAfterReduction(
                orderSnapshot, baseResult.getDiscountAmount());
            CouponCalculationResult vipResult = vipStrategy.calculate(
                afterReductionSnapshot, getCouponForVipBonus());
            
            // 4. 合并两个结果
            return mergeResults(baseResult, vipResult);
        }
        return baseResult;
    }
    // ... 其他方法 ...
}

这样, FullReductionWithVipBonusStrategy 是一个全新的、独立的策略,它和 FullReductionGeneralStrategy DiscountVipStrategy 是平行关系,而非父子关系。你可以随时启用或禁用它,而不影响其他策略。

4.3 陷阱三:忽略策略的“可观测性”,线上出问题两眼一抹黑

当策略逻辑复杂时(比如风控策略),一旦线上计算结果不符合预期,你根本不知道是哪个策略、在哪个环节出了问题。日志里只有一句 calculate() returned result: ... ,毫无上下文。

必须为每个策略的执行过程添加结构化日志和指标。 CouponCalculationContext.execute() 方法里,加入统一的监控切面:

@Around("@annotation(org.springframework.web.bind.annotation.PostMapping) && " +
        "execution(* com.xxx.service.CouponCalculationContext.execute(..))")
public Object logAndMonitor(ProceedingJoinPoint joinPoint) throws Throwable {
    long startTime = System.currentTimeMillis();
    String strategyCode = getStrategyCodeFromArgs(joinPoint.getArgs());
    
    try {
        Object result = joinPoint.proceed();
        long duration = System.currentTimeMillis() - startTime;
        
        // 记录关键指标:执行耗时、成功/失败、策略类型
        Metrics.timer("coupon.calculation.duration", "strategy", strategyCode).record(duration, TimeUnit.MILLISECONDS);
        Metrics.counter("coupon.calculation.success", "strategy", strategyCode).increment();
        
        log.info("Coupon calculation success. strategy={}, duration={}ms, result={}", 
            strategyCode, duration, result);
        return result;
    } catch (Exception e) {
        long duration = System.currentTimeMillis() - startTime;
        Metrics.counter("coupon.calculation.failure", "strategy", strategyCode, "error", e.getClass().getSimpleName()).increment();
        log.error("Coupon calculation failed. strategy={}, duration={}ms, error={}", 
            strategyCode, duration, e.getMessage(), e);
        throw e;
    }
}

同时,在每个策略的 calculate() 方法开头,打印详细的输入参数摘要:

@Override
public CouponCalculationResult calculate(OrderSnapshot orderSnapshot, Coupon coupon) {
    log.debug("Executing {} with orderSnapshot=[id={}, payable={}], coupon=[id={}, code={}, threshold={}]",
        getStrategyName(),
        orderSnapshot.getId(), orderSnapshot.getPayableAmount(),
        coupon.getId(), coupon.getStrategyCode(), coupon.getThreshold());
    // ... 实际计算逻辑 ...
}

提示:日志级别用 DEBUG ,避免污染生产 INFO 日志。但务必确保 DEBUG 日志在生产环境是开启的(通过Logback的 <logger> 配置),因为这是你线上排障的唯一线索。

5. 策略模式与Java生态的深度结合:超越 Collections.sort() 的现代实践

提到策略模式,很多Java教程都会拿 Collections.sort(list, comparator) 举例。这没错, Comparator 确实是一个策略接口。但如果你只停留在这个层面,就严重低估了策略模式在现代Java工程中的威力。它早已和Spring、函数式编程、配置中心深度绑定,成为构建弹性系统的标准范式。

5.1 Spring的 @Qualifier 与策略注入:告别 if-else 的终极方案

Collections.sort() 的例子中, Comparator 是临时传入的。但在企业级应用中,策略往往是长期存在的、有状态的、需要依赖注入的Bean。Spring提供了完美的解决方案: @Qualifier 注解。

回顾我们之前的 CouponCalculationContext ,它通过 List<CouponCalculationStrategy> 注入所有策略,再用 Map 索引。这很通用,但有时你需要更精细的控制。比如,某个特定的订单创建流程, 必须 使用 FullReductionGeneralStrategy ,不能由 Context 动态选择。这时,你可以直接注入:

@Service
public class SpecialOrderService {

    // 直接注入指定策略,Spring会根据@Qualifier找到它
    private final CouponCalculationStrategy fullReductionStrategy;

    public SpecialOrderService(@Qualifier("fullReductionGeneral") 
                              CouponCalculationStrategy fullReductionStrategy) {
        this.fullReductionStrategy = fullReductionStrategy;
    }

    public void createSpecialOrder(...) {
        // 强制使用满减策略,跳过Context的动态选择
        CouponCalculationResult result = fullReductionStrategy.calculate(...);
    }
}

@Qualifier("fullReductionGeneral") 这个字符串,就是策略的“身份证”。它和策略类上的 @Qualifier("fullReductionGeneral") 注解严格对应。这种方式,让你可以在代码层面,对策略的使用做出精确的、编译期安全的约束,比运行时字符串匹配( strategyMap.get("xxx") )更可靠。

5.2 Java 8+函数式接口:用Lambda表达轻量策略

对于逻辑极其简单、且不需要依赖注入的策略,Java 8的函数式接口是绝佳选择。它省去了定义接口、实现类的繁琐,代码极度简洁。

比如,一个简单的“日志脱敏策略”,只需要对手机号做掩码:

// 定义一个函数式接口
@FunctionalInterface
public interface LogMaskingStrategy {
    String mask(String rawValue);
}

// 在业务代码中,直接用Lambda创建策略
LogMaskingStrategy phoneMasker = (phone) -> {
    if (phone == null || phone.length() < 11) return phone;
    return phone.substring(0, 3) + "****" + phone.substring(7);
};

LogMaskingStrategy idCardMasker = (idCard) -> {
    if (idCard == null || idCard.length() < 18) return idCard;
    return idCard.substring(0, 6) + "********" + idCard.substring(14);
};

// 使用
String maskedPhone = phoneMasker.mask("13812345678");

这种写法,把策略变成了一个“值”(Value),可以作为参数传递、存储在集合中、甚至序列化。它牺牲了一点“契约”的正式感,但换来了极致的灵活性和可读性。在微服务间传递简单规则时,非常高效。

5.3 与配置中心(如Nacos/Apollo)联动:实现策略的热更新

策略模式的最高境界,是让策略的“开关”和“参数”脱离代码,进入配置中心。想象一下,运营同学想临时关闭某个地区的满减活动,你不再需要发版,只需在Nacos里把 full_reduction_region_xxx.enabled true 改成 false ,策略就会自动生效。

这需要一点额外的基础设施:

  1. 策略元数据管理 :为每个策略定义一个配置项,例如:

    coupon:
      strategies:
        full_reduction_general:
          enabled: true
          threshold: 100.00
          discount_amount: 20.00
        discount_vip:
          enabled: false
          discount_rate: 0.95
    
  2. 策略代理(Proxy) :创建一个代理类,它持有真实策略的引用,并在每次调用前检查配置:

    @Component
    public class ConfigurableCouponStrategyProxy implements CouponCalculationStrategy {
    
        private final CouponCalculationStrategy delegate;
        private final String strategyCode;
        private final NacosConfigService configService;
    
        public ConfigurableCouponStrategyProxy(CouponCalculationStrategy delegate,
                                               String strategyCode,
                                               NacosConfigService configService) {
            this.delegate = delegate;
            this.strategyCode = strategyCode;
            this.configService = configService;
        }
    
        @Override
        public CouponCalculationResult calculate(OrderSnapshot orderSnapshot, Coupon coupon) {
            // 1. 从配置中心读取该策略的启用状态
            boolean isEnabled = configService.getBoolean(
                String.format("coupon.strategies.%s.enabled", strategyCode), true);
            
            if (!isEnabled) {
                throw new RuleViolationException(
                    String.format("策略 %s 当前已禁用", strategyCode));
            }
    
            // 2. 调用真实策略
            return delegate.calculate(orderSnapshot, coupon);
        }
    
        // ... 其他方法 ...
    }
    
  3. 动态刷新 :利用Spring Cloud Alibaba的 @RefreshScope 或Apollo的 @ApolloConfigChangeListener ,监听配置变更,动态更新代理的状态。

这样,策略的生命周期就从“编译期固定”升级到了“运行时可管可控”。它让技术决策和业务决策解耦,是成熟团队的标配。

6. 策略模式不是万能的:何时该说“不”

再强大的工具,也有其适用边界。盲目套用策略模式,反而会增加系统复杂度。我见过太多项目,为了“设计模式”而设计模式,把一个只有两种情况的 if-else ,硬生生拆成四个类、三个接口、一个上下文,最后连作者自己都看不懂。

6.1 明确的“不适用”场景清单

以下情况,请果断放弃策略模式,用更简单的方式:

  • 分支数量极少且稳定不变 :比如一个支付状态枚举,只有 PENDING , SUCCESS , FAILED 三种,且未来几年都不会变。此时,一个 switch (status) 比建一套策略模式清爽一百倍。记住: 模式是用来应对变化的,不是用来装饰静态的。

  • 策略间逻辑高度耦合,无法正交 :比如“优惠券计算”和“积分抵扣”,它们的计算结果会相互影响(积分抵扣后,订单金额变了,优惠券门槛可能就不满足了)。这种强耦合的场景,强行拆成两个策略,只会让代码更难理解。应该合并为一个“订单总费用计算”策略,内部协调所有规则。

  • 性能是绝对瓶颈,且策略切换开销不可接受 :策略模式的核心是“运行时多态”,这涉及到虚方法调用(Virtual Method Invocation),虽然JVM优化得很好,但在纳秒级敏感的场景(如高频交易引擎的核心路径),一次 interface 方法调用的开销,可能比一个内联的 if-else 慢几个CPU周期。这时,用 enum + switch ,或者CGLIB字节码增强,是更优解。

  • 团队成员普遍缺乏面向对象基础 :策略模式要求开发者理解接口、多态、依赖倒置。如果团队里一半人还在纠结 static final 的区别,强行推广策略模式,只会导致代码质量下降。先夯实基础,再谈设计。

6.2 替代方案对比:什么时候选别的?

场景 推荐方案 理由
极简条件分支(2-3个分支) if-else switch 零学习成本,零抽象开销,代码最短,可读性最高。
状态驱动的行为(如订单状态机) 状态模式(State Pattern) 状态模式关注“对象在不同状态下,行为如何改变”,它隐含了状态转换的逻辑,比策略模式更贴合。策略模式是“选一个算法”,状态模式是“根据当前状态,自动执行对应行为”。
需要组合多个算法(如先过滤再排序) 责任链模式(Chain of Responsibility) 责任链强调“请求沿着链条传递,每个处理器决定是否处理或继续传递”,天然适合处理流程化的、可插拔的步骤。策略模式是“选一个”,责任链是“走一串”。
算法逻辑极其复杂,且需要深度定制 模板方法模式(Template Method Pattern) 模板方法定义了算法骨架,把某些步骤延迟到子类实现。它比策略模式提供了更强的“框架约束”,适合那些“大部分流程固定,只有少数步骤可变”的场景。

选择的本质,是权衡。策略模式的权重是: 变化频率高、变化维度单一、各变体完全独立、需要运行时动态选择 。当你确认这四点都满足时,它就是你最锋利的那把刀。否则,放下它,去拿更适合的工具。

我在实际项目里,通常会先用最简单的 if-else 快速实现MVP。当需求开始频繁变动,当 if-else 链突破10行,当产品经理说“下个月我们要上线XX新渠道”时,我才动手重构。这不是偷懒,而是对技术债的敬畏—— 最好的设计,永远诞生于对真实痛点的精准回应,而不是对教科书概念的虔诚膜拜。

更多推荐