策略模式:算法族封装、消除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 打交道,上下文把请求转发给具体的策略对象。每个策略类只负责自己的算法,互不干扰。
实战中的一些建议
最后,分享几个我在实战中总结的经验:
- 策略类命名要清晰:比如
FullReductionStrategy、DiscountStrategy,一看就知道是干什么的。别用StrategyA、StrategyB这种名字。 - 策略参数可以灵活:如果策略需要额外的参数(比如满减的阈值、折扣率),可以在构造函数里传入,或者用策略的setter方法设置。
- 结合Spring使用:把策略类注册成Bean,然后用一个Map来管理,key是策略类型,value是策略Bean。这样新增策略时,只需要加一个Bean,连枚举都不用改。
- 不要滥用:如果只有两三种变化,而且变化频率很低,用简单的if-else反而更直观。策略模式是为“变化”而生的,不是为“存在”而生的。
嗯,关于策略模式,今天就聊到这里。记住一句话:把变化封装起来,让稳定部分不受影响。这是设计模式的精髓,也是写出优雅代码的关键。
公众号:蓝海资料掘金营,微信deep3321