策略模式:算法族封装、消除if-else、实战:促销活动系统

说实话,我见过太多代码里充斥着这样的场景:一个促销方法,里面密密麻麻全是if-else,从“满100减20”到“双倍积分”,再到“新用户立减”。每次加一个新活动,就得往这个方法里塞一段逻辑。改着改着,代码就成了一锅粥。

今天聊的策略模式,就是专门解决这类问题的。它的核心思想很简单:把算法(或者说行为)封装成独立的类,让它们可以互相替换。说白了,就是把“做什么”和“怎么做”拆开。

为什么需要策略模式?

先看一个反面教材。假设我们有个促销系统,最初只支持“满减”和“折扣”两种活动。代码可能是这样的:

public BigDecimal calculatePrice(String promotionType, BigDecimal originalPrice) {
    if ("FULL_REDUCTION".equals(promotionType)) {
        // 满100减20
        if (originalPrice.compareTo(new BigDecimal("100")) >= 0) {
            return originalPrice.subtract(new BigDecimal("20"));
        }
        return originalPrice;
    } else if ("DISCOUNT".equals(promotionType)) {
        // 打8折
        return originalPrice.multiply(new BigDecimal("0.8"));
    }
    // 更多else if...
    return originalPrice;
}

这种写法,我称之为“打地鼠式开发”。每次产品经理提一个新活动,你就得在这块代码里加一个else if。时间一长,这个方法可能膨胀到几百行。而且,你改的时候还得小心翼翼,生怕影响到已有的逻辑。

我在项目中遇到过最夸张的一次,一个促销计算类里塞了20多种活动类型,光看代码就花了半天。后来重构时,我果断用了策略模式。

策略模式的核心结构

策略模式由三部分组成:

  • 策略接口(Strategy):定义所有算法共有的行为
  • 具体策略(Concrete Strategy):实现策略接口,封装具体的算法
  • 上下文(Context):持有策略对象的引用,负责调用策略

嗯,这里要注意:上下文本身不实现算法,它只是把请求委托给策略对象。这样,算法就可以独立变化了。

核心原则:开闭原则。对扩展开放,对修改关闭。新增活动时,你只需要添加一个新策略类,不需要修改已有的代码。

用策略模式重构促销系统

我们一步步来。先定义一个策略接口:

public interface PromotionStrategy {
    BigDecimal calculatePrice(BigDecimal originalPrice);
}

然后,把每个活动封装成独立的策略类:

public class FullReductionStrategy implements PromotionStrategy {
    private static final BigDecimal THRESHOLD = new BigDecimal("100");
    private static final BigDecimal REDUCTION = new BigDecimal("20");

    @Override
    public BigDecimal calculatePrice(BigDecimal originalPrice) {
        if (originalPrice.compareTo(THRESHOLD) >= 0) {
            return originalPrice.subtract(REDUCTION);
        }
        return originalPrice;
    }
}

public class DiscountStrategy implements PromotionStrategy {
    private static final BigDecimal DISCOUNT_RATE = new BigDecimal("0.8");

    @Override
    public BigDecimal calculatePrice(BigDecimal originalPrice) {
        return originalPrice.multiply(DISCOUNT_RATE);
    }
}

public class NewUserStrategy implements PromotionStrategy {
    private static final BigDecimal NEW_USER_DISCOUNT = new BigDecimal("30");

    @Override
    public BigDecimal calculatePrice(BigDecimal originalPrice) {
        // 新用户立减30,但不超过原价
        BigDecimal result = originalPrice.subtract(NEW_USER_DISCOUNT);
        return result.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : result;
    }
}

接下来,需要一个上下文类来管理策略:

public class PromotionContext {
    private PromotionStrategy strategy;

    public PromotionContext(PromotionStrategy strategy) {
        this.strategy = strategy;
    }

    public void setStrategy(PromotionStrategy strategy) {
        this.strategy = strategy;
    }

    public BigDecimal executeStrategy(BigDecimal originalPrice) {
        return strategy.calculatePrice(originalPrice);
    }
}

使用的时候,就变得非常清爽了:

// 客户端代码
PromotionContext context = new PromotionContext(new FullReductionStrategy());
BigDecimal price1 = context.executeStrategy(new BigDecimal("150"));
System.out.println("满减后价格:" + price1);

context.setStrategy(new DiscountStrategy());
BigDecimal price2 = context.executeStrategy(new BigDecimal("150"));
System.out.println("折扣后价格:" + price2);

你想想看,现在要加一个新活动,比如“买二送一”,我只需要新建一个 BuyTwoGetOneFreeStrategy 类,实现 PromotionStrategy 接口,然后在客户端代码里切换策略就行了。完全不用动已有的代码。

个人习惯:我会把策略类的创建交给工厂模式或Spring的IoC容器管理。这样客户端代码甚至不需要知道具体策略类的名字,只需要传入一个类型标识即可。

消除if-else的进阶玩法

如果策略比较多,客户端代码里还是可能出现一堆if-else来选择策略。这时候,可以结合工厂模式或枚举来进一步优化。

举个例子,用枚举来管理策略:

public enum PromotionType {
    FULL_REDUCTION(new FullReductionStrategy()),
    DISCOUNT(new DiscountStrategy()),
    NEW_USER(new NewUserStrategy());

    private final PromotionStrategy strategy;

    PromotionType(PromotionStrategy strategy) {
        this.strategy = strategy;
    }

    public PromotionStrategy getStrategy() {
        return strategy;
    }
}

// 使用
PromotionType type = PromotionType.valueOf("FULL_REDUCTION");
PromotionContext context = new PromotionContext(type.getStrategy());
BigDecimal price = context.executeStrategy(new BigDecimal("150"));

这样一来,客户端代码里连一个if-else都没有了。所有的策略选择逻辑,都集中到了枚举的定义中。

我曾经踩过一个坑:策略类如果是有状态的(比如需要记录调用次数),就不能用单例模式。每个请求都得new一个新的策略实例。否则,多线程环境下会出现数据错乱。这一点在Spring中尤其要注意,默认的Bean是单例的,如果策略类里有成员变量,记得加上@Scope("prototype")。

策略模式的适用场景

不是所有地方都需要策略模式。我个人总结了几条判断标准:

  • 算法经常变化:比如促销规则、运费计算、税率计算
  • 有多个相似的行为:比如不同的排序算法、不同的支付方式
  • 需要避免大量条件判断:if-else或switch-case超过3个,就该考虑重构了

反过来,如果算法很稳定,或者只有一两种变化,硬套策略模式反而会增加代码复杂度。没必要为了用模式而用模式。

策略模式的优缺点

优点 缺点
符合开闭原则,扩展方便 策略类数量会增多
消除大量条件判断 客户端必须了解不同策略的区别
算法可以独立复用 增加了系统的对象数量
便于单元测试 策略切换可能带来性能开销

说白了,策略模式是用“类的数量”换“代码的清晰度”。如果你的项目里策略类超过10个,建议配合工厂模式一起使用,避免客户端代码变得臃肿。

核心知识体系

下面这张图,展示了策略模式在促销系统中的整体结构:

策略模式 - 促销活动系统结构图 PromotionContext 持有策略引用 <<接口>> PromotionStrategy FullReductionStrategy 满100减20 DiscountStrategy 打8折 NewUserStrategy 新用户立减30 客户端代码 客户端通过上下文调用策略,策略之间可以互相替换

从图中可以看到,客户端只和 PromotionContext 打交道,上下文把请求转发给具体的策略对象。每个策略类只负责自己的算法,互不干扰。

实战中的一些建议

最后,分享几个我在实战中总结的经验:

  • 策略类命名要清晰:比如 FullReductionStrategyDiscountStrategy,一看就知道是干什么的。别用 StrategyAStrategyB 这种名字。
  • 策略参数可以灵活:如果策略需要额外的参数(比如满减的阈值、折扣率),可以在构造函数里传入,或者用策略的setter方法设置。
  • 结合Spring使用:把策略类注册成Bean,然后用一个Map来管理,key是策略类型,value是策略Bean。这样新增策略时,只需要加一个Bean,连枚举都不用改。
  • 不要滥用:如果只有两三种变化,而且变化频率很低,用简单的if-else反而更直观。策略模式是为“变化”而生的,不是为“存在”而生的。

嗯,关于策略模式,今天就聊到这里。记住一句话:把变化封装起来,让稳定部分不受影响。这是设计模式的精髓,也是写出优雅代码的关键。


公众号:蓝海资料掘金营,微信deep3321