1. 什么是策略模式?它真不是“写一堆if-else再包个接口”那么简单

策略模式(Strategy Design Pattern)在Java开发中被反复提及,尤其在面试场景里几乎成了设计模式的“入门必答题”。但很多人一开口就是“定义:定义一系列算法,把它们一个个封装起来,并且使它们可以互相替换”,说完就卡壳——这就像告诉你“汽车是四个轮子加发动机”,却没讲清楚为什么底盘要调校、变速箱为什么要匹配齿比、ABS介入时机怎么标定。策略模式真正的价值,从来不在“能换算法”这个表象,而在于 把变化点从主流程中剥离出来,让核心逻辑稳定、可测、可演进

我带过不少刚转Java的同事,他们第一次写支付模块时,常会写出这样的代码:

public class PaymentService {
    public void processPayment(String type, BigDecimal amount) {
        if ("alipay".equals(type)) {
            // 支付宝签名、验签、调用SDK...
        } else if ("wechat".equals(type)) {
            // 微信统一下单、回调验签、结果处理...
        } else if ("bank_transfer".equals(type)) {
            // 银行接口对接、对账文件生成...
        }
    }
}

这段代码的问题,远不止“违反开闭原则”这么抽象。实操中它会立刻暴露出三个硬伤:第一,每次新增一种支付方式(比如突然要上数字人民币),就得改 processPayment 方法,动一次就要全量回归测试;第二,单元测试极其痛苦——你得mock所有分支路径,一个 if 条件漏测,线上就可能跳转到错误渠道;第三,不同支付渠道的异常处理逻辑混在一起,支付宝超时重试和微信证书过期的兜底策略根本没法复用,最后变成满屏 try-catch 嵌套。

策略模式的解法,本质是 用多态替代条件分支,用组合替代继承,用接口契约约束行为边界 。它不追求“炫技”,而是解决一个非常具体的问题:当系统中存在 同一类行为的多种实现,且这些实现之间互斥、可替换、未来大概率会增删 时,如何让主干代码像混凝土一样坚固,而算法细节像乐高积木一样可插拔。这不是教科书里的理想模型,而是我在电商大促压测、金融风控规则迭代、IoT设备协议适配等真实项目里,反复验证过的“防抖方案”。

你可能会问:那和简单工厂模式有什么区别?关键就在 谁持有策略实例、何时决定策略、策略间是否需要共享上下文 。工厂模式解决的是“创建谁”,策略模式解决的是“用谁来执行”。比如订单创建用工厂,订单支付用策略——前者关注对象诞生,后者关注行为执行。这个分界线,我在三次跨团队重构中踩过坑:有次把支付策略的初始化硬编码在Spring Bean里,结果灰度发布时无法动态切换渠道,最后连夜改成 @ConditionalOnProperty 配合策略注册表才救回来。所以别只背UML图,先想清楚你的业务里,哪些“变”是高频的、哪些“不变”是核心的,这才是策略模式落地的第一步。

2. 策略模式的核心结构拆解:为什么必须有Context、Strategy、ConcreteStrategy三层?

策略模式的标准结构由三部分组成: 策略接口(Strategy)、具体策略实现(ConcreteStrategy)、策略上下文(Context) 。很多教程把Context简单说成“持有一个策略引用”,这严重低估了它的工程价值。在我参与的7个中大型Java项目里,Context从来不是个透明胶水层,而是 策略调度中枢、上下文透传管道、执行生命周期管理者 。下面逐层拆解每个角色的真实职责和常见误用。

2.1 Strategy接口:契约即法律,不是摆设

策略接口定义的是 行为契约 ,而非数据契约。以支付为例,正确的接口设计是:

public interface PaymentStrategy {
    /**
     * 执行支付主流程
     * @param order 订单信息(含金额、用户ID、商品明细)
     * @return 支付结果(含流水号、状态、跳转URL)
     */
    PaymentResult execute(Order order);

    /**
     * 验证当前策略是否适用此订单
     * @param order 订单信息
     * @return true表示可执行,false需降级或报错
     */
    boolean supports(Order order);

    /**
     * 获取策略唯一标识,用于日志追踪和监控
     */
    String getStrategyCode();
}

注意三个关键点:第一, execute() 方法参数是 Order 对象而非原始字段(如 String orderId, BigDecimal amount ),这是为了 避免策略实现中重复解析订单 ,也方便未来扩展订单属性;第二,强制要求 supports() 方法,这是策略模式的“安全阀”——没有这个方法,系统就无法做运行时策略路由,只能靠外部硬编码判断;第三, getStrategyCode() 是监控命脉,线上出问题时,日志里看到 strategyCode=alipay_v3 比看到 class=AlipayV3Strategy 直观十倍。

反面案例我见过太多:有人把策略接口设计成 void pay(String orderId, BigDecimal amount) ,结果每个实现都要自己查库、自己组装参数,不仅性能差,还导致 AlipayStrategy WechatStrategy 里出现大量重复代码。还有人把 supports() 逻辑写在Context里,结果新增策略时总忘记同步修改Context的判断逻辑,造成策略“幽灵失效”。

2.2 ConcreteStrategy:每个实现都是独立的“微服务”

具体策略实现不是简单的算法函数,而是一个 自包含的业务单元 。以支付宝支付策略为例,它的完整实现要考虑:

  • 依赖隔离 :支付宝SDK版本与微信SDK版本冲突怎么办?我们采用Maven provided scope + ClassLoader隔离,在 AlipayStrategy 里通过 Thread.currentThread().getContextClassLoader() 加载专属SDK,避免污染全局类路径。
  • 配置外化 :AppId、私钥路径、网关地址不能写死在代码里。我们约定所有策略实现必须实现 Configurable 接口,通过 @Value("${payment.alipay.app-id}") 注入,且配置项前缀与策略码强绑定( payment.{strategyCode}.xxx )。
  • 异常分类 :支付宝返回 INVALID_SIGN TRADE_NOT_EXIST 必须走不同告警通道。我们在 execute() 里统一捕获 AlipayApiException ,按 getSubCode() 映射为预定义的 PaymentErrorType (如 SIGN_ERROR , ORDER_NOT_FOUND ),上层Context只处理这几种标准错误。

这种设计让每个策略实现像一个微型服务:它知道自己该做什么、依赖什么、失败时怎么报错。我在某银行项目里曾把12种支付渠道拆成12个独立JAR包,部署时按需加载,运维同学反馈故障定位时间从小时级降到分钟级——因为 alipay-v3.2.1.jar 出问题,绝不会影响 unionpay-2.8.0.jar

2.3 Context:被严重低估的“策略交响乐指挥家”

Context常被简化为:

public class PaymentContext {
    private PaymentStrategy strategy;
    public void setStrategy(PaymentStrategy strategy) { this.strategy = strategy; }
    public PaymentResult execute(Order order) { return strategy.execute(order); }
}

这在Demo里没问题,但在生产环境会出大事。真实的Context必须承担以下职责:

  1. 策略路由 :根据订单属性(如用户等级、支付金额、地域)选择策略。我们用 Map<String, PaymentStrategy> 缓存所有策略,key为 strategyCode ,路由逻辑抽成 StrategyRouter 组件,支持SPI扩展。
  2. 上下文透传 :支付过程中需要传递风控Token、营销优惠券ID、物流单号等,这些不能塞进 Order 对象污染领域模型。Context提供 withContext(Map<String, Object> context) 方法,将临时上下文透传给策略。
  3. 执行增强 :自动添加日志埋点(记录策略码、耗时、输入摘要)、熔断保护(调用超时自动降级)、结果缓存(对查询类策略)。这些通用能力通过装饰器模式注入,而非在每个策略里重复写。

最典型的教训:某次大促,微信支付策略因证书更新失败,但Context没做熔断,导致所有微信支付请求堆积,最终拖垮整个订单服务。后来我们强制要求Context必须集成Hystrix,且 execute() 方法默认500ms超时,超时后自动触发 fallbackStrategy 。这个改动让系统稳定性提升了40%。

3. 从零手写一个生产级策略模式:电商优惠券计算实战

光讲理论不够,我们来实操一个真实场景:电商优惠券计算。这个需求完美契合策略模式——优惠类型多(满减、折扣、赠品、N选一)、规则复杂(叠加限制、互斥条件)、变更频繁(运营天天改规则)。下面展示如何从零构建一个可维护、可监控、可扩展的优惠券策略体系。

3.1 需求分析与策略边界划定

先明确什么该放进策略,什么不该:

  • ✅ 应放入策略:满300减50的计算逻辑、85折的折扣公式、赠品SKU的选取规则;
  • ❌ 不应放入策略:优惠券库存扣减(这是事务性操作,应由CouponService统一处理)、用户资格校验(如新用户专享,应前置拦截)、日志记录(Context统一处理)。

我们定义策略接口:

public interface CouponCalculationStrategy {
    /**
     * 计算优惠金额/赠品列表
     * @param cart 购物车(含商品、数量、价格)
     * @param coupon 优惠券(含面额、门槛、类型)
     * @param context 运行时上下文(如用户ID、设备指纹)
     * @return 计算结果(优惠金额、赠品列表、是否可用)
     */
    CalculationResult calculate(ShoppingCart cart, Coupon coupon, Map<String, Object> context);

    /**
     * 判断当前优惠券是否适用于此购物车
     * @param cart 购物车
     * @param coupon 优惠券
     * @param context 上下文
     * @return 是否支持
     */
    boolean supports(ShoppingCart cart, Coupon coupon, Map<String, Object> context);

    /**
     * 获取策略码,对应优惠券type字段
     * @return 策略码
     */
    String getStrategyCode();
}

这里 CalculationResult 是统一返回结构,包含 discountAmount (优惠金额)、 giftSkus (赠品SKU列表)、 usable (是否可用)、 reason (不可用原因)。所有策略必须返回这个结构,上层无需关心具体实现。

3.2 具体策略实现:满减与折扣的差异化处理

满减策略(FullReductionStrategy)
@Component
@ConditionalOnProperty(name = "coupon.strategy.full-reduction.enabled", havingValue = "true")
public class FullReductionStrategy implements CouponCalculationStrategy {

    private final Logger logger = LoggerFactory.getLogger(FullReductionStrategy.class);

    @Value("${coupon.full-reduction.min-amount:300}")
    private BigDecimal minAmount;

    @Value("${coupon.full-reduction.discount-amount:50}")
    private BigDecimal discountAmount;

    @Override
    public CalculationResult calculate(ShoppingCart cart, Coupon coupon, Map<String, Object> context) {
        BigDecimal totalAmount = cart.getTotalAmount();
        if (totalAmount.compareTo(minAmount) >= 0) {
            // 满足门槛,直接减固定金额
            return CalculationResult.builder()
                    .discountAmount(discountAmount)
                    .usable(true)
                    .build();
        } else {
            return CalculationResult.builder()
                    .usable(false)
                    .reason("订单金额" + totalAmount + "不足" + minAmount + "元")
                    .build();
        }
    }

    @Override
    public boolean supports(ShoppingCart cart, Coupon coupon, Map<String, Object> context) {
        // 检查优惠券类型是否为FULL_REDUCTION,且未过期
        return "FULL_REDUCTION".equals(coupon.getType()) 
                && coupon.getExpiredAt().isAfter(Instant.now());
    }

    @Override
    public String getStrategyCode() {
        return "FULL_REDUCTION";
    }
}
折扣策略(DiscountStrategy)
@Component
@ConditionalOnProperty(name = "coupon.strategy.discount.enabled", havingValue = "true")
public class DiscountStrategy implements CouponCalculationStrategy {

    private final Logger logger = LoggerFactory.getLogger(DiscountStrategy.class);

    @Value("${coupon.discount.rate:0.85}")
    private BigDecimal discountRate;

    @Override
    public CalculationResult calculate(ShoppingCart cart, Coupon coupon, Map<String, Object> context) {
        BigDecimal totalAmount = cart.getTotalAmount();
        // 折扣计算需保留两位小数,避免浮点误差
        BigDecimal discount = totalAmount.multiply(BigDecimal.ONE.subtract(discountRate))
                .setScale(2, RoundingMode.HALF_UP);
        return CalculationResult.builder()
                .discountAmount(discount)
                .usable(true)
                .build();
    }

    @Override
    public boolean supports(ShoppingCart cart, Coupon coupon, Map<String, Object> context) {
        return "DISCOUNT".equals(coupon.getType()) 
                && coupon.getExpiredAt().isAfter(Instant.now())
                && isUserEligible(context); // 额外校验:比如仅限VIP用户
    }

    private boolean isUserEligible(Map<String, Object> context) {
        String userId = (String) context.get("userId");
        // 调用用户服务查询VIP等级
        return userService.isVip(userId);
    }

    @Override
    public String getStrategyCode() {
        return "DISCOUNT";
    }
}

关键细节:

  • 使用 @ConditionalOnProperty 控制策略开关,上线新策略时无需发版,改配置即可;
  • calculate() 方法内不做远程调用(如查用户等级),而是通过 context 传入必要数据,保证策略纯函数特性;
  • 金额计算强制 setScale(2, RoundingMode.HALF_UP) ,这是金融计算铁律,我见过因 double 精度导致优惠多算0.01元被用户投诉的事故。

3.3 Context实现:策略路由与执行增强

@Service
public class CouponCalculationContext {

    // 策略注册表:策略码 -> 策略实例
    private final Map<String, CouponCalculationStrategy> strategyMap;

    // 熔断器配置
    private final HystrixCommand.Setter hystrixSetter;

    public CouponCalculationContext(List<CouponCalculationStrategy> strategies) {
        this.strategyMap = strategies.stream()
                .collect(Collectors.toMap(CouponCalculationStrategy::getStrategyCode, Function.identity()));
        this.hystrixSetter = HystrixCommand.Setter
                .withGroupKey(HystrixCommandGroupKey.Factory.asKey("COUPON_CALCULATION"))
                .andCommandKey(HystrixCommandKey.Factory.asKey("CALCULATE"))
                .andCommandPropertiesDefaults(
                        HystrixCommandProperties.defaultSetter()
                                .withExecutionTimeoutInMilliseconds(300) // 300ms超时
                                .withCircuitBreakerEnabled(true)
                );
    }

    /**
     * 主执行入口
     */
    public CalculationResult execute(ShoppingCart cart, Coupon coupon, Map<String, Object> context) {
        long startTime = System.currentTimeMillis();

        // 1. 路由:找到匹配的策略
        CouponCalculationStrategy strategy = findStrategy(cart, coupon, context);
        if (strategy == null) {
            return CalculationResult.builder()
                    .usable(false)
                    .reason("无匹配的优惠券计算策略")
                    .build();
        }

        // 2. 执行:带熔断和日志
        CalculationResult result = new HystrixCommand<CalculationResult>(hystrixSetter) {
            @Override
            protected CalculationResult run() throws Exception {
                return strategy.calculate(cart, coupon, context);
            }

            @Override
            protected CalculationResult getFallback() {
                // 熔断降级:返回默认结果或调用备用策略
                return CalculationResult.builder()
                        .usable(false)
                        .reason("策略执行超时,已降级")
                        .build();
            }
        }.execute();

        // 3. 日志:记录关键指标
        long costTime = System.currentTimeMillis() - startTime;
        logger.info("CouponCalculation executed | strategy={} | cartId={} | couponId={} | cost={}ms | result={}",
                strategy.getStrategyCode(), cart.getId(), coupon.getId(), costTime, result);

        return result;
    }

    /**
     * 策略路由逻辑
     */
    private CouponCalculationStrategy findStrategy(ShoppingCart cart, Coupon coupon, Map<String, Object> context) {
        return strategyMap.values().stream()
                .filter(s -> s.supports(cart, coupon, context))
                .findFirst()
                .orElse(null);
    }
}

这个Context实现了三大能力:

  • 动态路由 findStrategy() 遍历所有注册策略,调用 supports() 判断,无需硬编码 if-else
  • 熔断保护 :Hystrix确保单个策略故障不影响全局,降级逻辑清晰;
  • 可观测性 :日志包含策略码、耗时、输入摘要,排查问题时直接grep策略码即可。

3.4 策略注册与Spring集成:告别new关键字

策略实例不能手动 new ,必须由Spring容器管理,才能享受依赖注入、AOP增强等能力。我们采用两种注册方式:

方式一:自动扫描(推荐)
@Configuration
public class StrategyAutoConfiguration {

    @Bean
    @Primary
    public CouponCalculationContext couponCalculationContext(
            List<CouponCalculationStrategy> strategies) {
        return new CouponCalculationContext(strategies);
    }
}

只要策略类加了 @Component ,Spring就会自动收集到 List<CouponCalculationStrategy> 中。这是最简洁的方式,也是我们线上项目的标准做法。

方式二:显式注册(适合多模块)

如果策略分散在不同模块(如 coupon-core coupon-marketing ),可在主模块中显式声明:

@Configuration
public class StrategyManualConfiguration {

    @Bean
    public CouponCalculationStrategy fullReductionStrategy() {
        return new FullReductionStrategy();
    }

    @Bean
    public CouponCalculationStrategy discountStrategy() {
        return new DiscountStrategy();
    }

    @Bean
    @Primary
    public CouponCalculationContext couponCalculationContext(
            FullReductionStrategy fullReductionStrategy,
            DiscountStrategy discountStrategy) {
        List<CouponCalculationStrategy> strategies = Arrays.asList(
                fullReductionStrategy, discountStrategy);
        return new CouponCalculationContext(strategies);
    }
}

无论哪种方式,核心原则是: Context只依赖Strategy接口,不感知具体实现类 。这样未来新增 GiftStrategy (赠品策略)时,只需写实现类+加 @Component ,其他代码零修改。

4. 策略模式的进阶技巧与避坑指南:那些文档里不会写的实战经验

策略模式看似简单,但在线上环境会遇到一堆“文档里没写”的坑。下面分享我在6个Java项目中总结的独家经验,全是血泪教训换来的。

4.1 策略选择的性能陷阱:别让supports()成为性能杀手

supports() 方法看似只是个布尔判断,但如果在里面做数据库查询、远程调用,就会让策略路由变成性能瓶颈。某次我们发现优惠券计算平均耗时突增到800ms,排查发现 DiscountStrategy.supports() 里调用了 userService.isVip(userId) ,而这个接口RT高达400ms。

解决方案

  • 预加载上下文 :在调用 execute() 前,由上层服务(如 OrderService )统一加载必要数据到 context 中,策略里直接取用;
  • 本地缓存 :对高频查询(如用户VIP状态),在 supports() 里用Caffeine缓存, expireAfterWrite(10, TimeUnit.MINUTES)
  • 异步预检 :对严格要求实时性的场景(如风控),改用 CompletableFuture 异步加载, supports() 只做快速判断,真正校验延迟到 calculate() 中。

提示: supports() 方法必须满足“快、轻、无副作用”三原则。我们团队的红线是:单次调用不超过5ms,不能有IO,不能修改任何状态。

4.2 策略间的协同与组合:当一个订单需要多个策略时

真实业务中,一个订单可能同时使用满减+折扣+赠品,这时策略模式怎么用?答案是: 策略模式不解决组合问题,它解决的是“单个行为的多实现”,组合是上层Context的责任

我们设计了 CompositeCouponCalculationStrategy

public class CompositeCouponCalculationStrategy implements CouponCalculationStrategy {

    private final List<CouponCalculationStrategy> strategies;

    public CompositeCouponCalculationStrategy(List<CouponCalculationStrategy> strategies) {
        this.strategies = strategies;
    }

    @Override
    public CalculationResult calculate(ShoppingCart cart, Coupon coupon, Map<String, Object> context) {
        // 按顺序执行多个策略,结果合并
        CalculationResult result = CalculationResult.empty();
        for (CouponCalculationStrategy strategy : strategies) {
            CalculationResult partial = strategy.calculate(cart, coupon, context);
            result = result.merge(partial); // 合并逻辑:金额相加、赠品去重等
        }
        return result;
    }

    @Override
    public boolean supports(ShoppingCart cart, Coupon coupon, Map<String, Object> context) {
        // 只要有一个策略支持,就认为复合策略支持
        return strategies.stream()
                .anyMatch(s -> s.supports(cart, coupon, context));
    }

    @Override
    public String getStrategyCode() {
        return "COMPOSITE";
    }
}

然后在Context中,当检测到订单含多个优惠券时,动态构建 CompositeCouponCalculationStrategy 。这样既保持了单一策略的纯粹性,又实现了灵活组合。

4.3 策略的版本管理与灰度发布:如何安全上线新算法

运营经常要求“新满减规则先对10%用户灰度”,这时不能停机发布。我们的方案是:

  1. 策略码带版本号 FULL_REDUCTION_V2 DISCOUNT_V3
  2. 路由规则外置 :将灰度规则配置在Apollo/Nacos中,如 coupon.strategy.route.rule=userId % 100 < 10 ? FULL_REDUCTION_V2 : FULL_REDUCTION_V1
  3. Context动态解析 findStrategy() 方法读取配置,按规则选择策略码,再从 strategyMap 中获取实例。

这样新策略上线后,先配置1%灰度,观察监控指标(成功率、耗时、错误率),没问题再逐步放大。我们曾用此方案在双11前一周灰度上线新折扣算法,提前发现并发下锁竞争问题,避免了大促事故。

4.4 单元测试的黄金法则:每个策略必须有独立测试类

策略模式的测试重点不是“能不能跑”,而是“边界条件是否覆盖”。我们强制要求每个 ConcreteStrategy 对应一个 *StrategyTest ,且必须覆盖:

  • ✅ 正常流程(满300减50,计算结果准确);
  • ✅ 边界值(刚好300元、299.99元、0元订单);
  • ✅ 异常场景(优惠券过期、金额为null、负数);
  • ✅ 性能基准(单次计算耗时<5ms)。

FullReductionStrategyTest 为例:

@SpringBootTest
class FullReductionStrategyTest {

    @Autowired
    private FullReductionStrategy strategy;

    @Test
    void should_calculate_discount_when_amount_meets_threshold() {
        // given
        ShoppingCart cart = createCartWithAmount(new BigDecimal("300.00"));
        Coupon coupon = createCoupon("FULL_REDUCTION");

        // when
        CalculationResult result = strategy.calculate(cart, coupon, Collections.emptyMap());

        // then
        assertThat(result.isUsable()).isTrue();
        assertThat(result.getDiscountAmount()).isEqualTo(new BigDecimal("50.00"));
    }

    @Test
    void should_not_apply_discount_when_amount_below_threshold() {
        // given
        ShoppingCart cart = createCartWithAmount(new BigDecimal("299.99"));
        Coupon coupon = createCoupon("FULL_REDUCTION");

        // when
        CalculationResult result = strategy.calculate(cart, coupon, Collections.emptyMap());

        // then
        assertThat(result.isUsable()).isFalse();
        assertThat(result.getReason()).contains("不足300元");
    }
}

注意:测试中 createCartWithAmount() createCoupon() 必须是静态工厂方法,确保测试数据纯净。我们禁止在测试中使用 new 创建领域对象,全部通过工厂构造,避免测试数据污染。

4.5 监控与告警:让策略“看得见、管得住”

没有监控的策略模式就是定时炸弹。我们在Context中埋点,上报到Prometheus:

  • coupon_calculation_total{strategy="FULL_REDUCTION",result="success"} :成功次数;
  • coupon_calculation_duration_seconds{strategy="DISCOUNT"} :耗时直方图;
  • coupon_calculation_fallback_total{strategy="GIFT"} :熔断次数。

告警规则:

  • coupon_calculation_fallback_total 5分钟内>10次,立即告警;
  • coupon_calculation_duration_seconds P95>200ms,触发性能优化工单;
  • 当某策略 result="failure" 占比连续10分钟>5%,自动禁用该策略(通过Apollo配置 coupon.strategy.{code}.enabled=false )。

这套监控让我们在某次支付宝SDK升级后,10分钟内发现 AlipayStrategy 错误率飙升,及时回滚配置,避免了资损。

5. 策略模式 vs 其他设计模式:什么时候该用它?什么时候该换思路?

策略模式常被拿来和状态模式、模板方法、工厂模式对比。但实际选型不能只看UML图,要看 业务语义、变化频率、团队认知成本 。下面用真实案例说明。

5.1 策略模式 vs 状态模式:订单状态流转该用谁?

订单有“待支付”、“已支付”、“已发货”、“已完成”等状态,状态改变时行为不同(如“待支付”可取消,“已发货”不可取消)。很多人第一反应是状态模式,但这是误区。

正确选择:状态模式 。因为:

  • 状态是 对象的内在属性 ,订单的状态改变会影响其所有行为;
  • 状态间有 严格流转关系 (不能从“已完成”跳回“待支付”);
  • 状态变更由 订单自身驱动 order.pay() 触发状态机迁移)。

而策略模式适用于:

  • 行为是 外部选择的 (运营配置用满减还是折扣);
  • 实现间 完全独立、无状态依赖
  • 变更由 配置或参数驱动 ,而非对象内部状态。

实战经验:我们曾把订单状态机错误地设计成策略模式,结果状态迁移逻辑散落在12个策略里,新人根本看不懂流转规则。重构为状态模式后,用 StateMachine 框架(如Spring State Machine)统一管理,代码量减少60%,可读性大幅提升。

5.2 策略模式 vs 模板方法:算法骨架固定时怎么选?

模板方法定义算法骨架,子类实现步骤。比如导出报表: export() 方法固定为“查数据→格式化→写文件”,子类只重写 formatData()

选择策略模式当

  • 算法实现 完全独立 ,没有公共骨架;
  • 需要 运行时动态切换 (如根据用户等级选不同导出格式);
  • 实现类 可能来自不同模块或第三方 (如接入飞书、钉钉的推送策略)。

选择模板方法当

  • 算法步骤 高度相似 ,只有少数步骤差异;
  • 步骤间有 强依赖和顺序约束 (必须先查数据再格式化);
  • 团队希望 强制规范实现流程 (所有子类必须实现 validateInput() )。

5.3 策略模式 vs 规则引擎:复杂规则该不该上Drools?

当优惠规则变成“新用户首单满100减20,且非周末,且商品类目为美妆,且用户近30天未下单”时,策略模式还适用吗?

答案:视情况而定 。我们做过对比:

维度 策略模式 Drools规则引擎
开发效率 高(Java代码,IDE友好) 低(需学DRL语法,调试困难)
运维成本 极低(和应用同生命周期) 高(需单独部署、监控、版本管理)
规则复杂度 适合≤5个条件的规则 适合≥10个条件的动态规则
变更频率 中低频(周级) 高频(运营随时改)

我们的决策树

  • 如果规则由 开发写死 ,且半年内不会大改 → 用策略模式,加 @ConditionalOnProperty 控制;
  • 如果规则由 运营后台配置 ,且每天调整 → 上Drools,但用策略模式封装Drools调用( DroolsRuleStrategy ),保持上层接口不变;
  • 如果规则 介于两者之间 (如季度调整)→ 用JSON配置+策略模式解析,如 {"type":"full_reduction","minAmount":100,"discount":20,"conditions":[{"field":"userType","op":"eq","value":"new"}]}

最后分享一个心法: 设计模式是解决问题的工具,不是炫技的勋章 。我在某项目评审会上,看到同事为“发送短信”写了5个策略(阿里云、腾讯云、华为云、自建SMSC、Mock),只为体现“开闭原则”。但实际业务中99%流量走阿里云,其他全是备用,最后我们砍掉4个策略,用 AliyunSmsStrategy + FallbackSmsStrategy 搞定,代码更少,故障面更小,监控更聚焦。记住:简单、可靠、可维护,永远是第一优先级。

更多推荐