主题
策略与模板方法:消灭 if-else 的两把刀
本文是设计模式系统学习系列的 L2 核心篇。前置:工厂家族:三种工厂模式。 学完可以配合面试题食用:Spring 中的设计模式
从一段烂代码说起
给一个电商后台加促销逻辑,最常见的一版长这样:
java
BigDecimal calculatePrice(Order order, String promoType) {
if ("full_off".equals(promoType)) {
// 满减算法
} else if ("discount".equals(promoType)) {
// 折扣算法
} else if ("group_buy".equals(promoType)) {
// 拼团算法
} else if ("flash_sale".equals(promoType)) {
// 秒杀算法
}
// 每加一种促销,这里就多一个 else if
}这段代码的问题不在行数,在每次新增促销类型都要改同一个方法。改了就要回归测试整个方法,因为所有分支混在一起。策略模式和模板方法从两个角度解决这个问题:策略把整个算法替换出去,模板方法把算法的骨架固定、部分步骤替换。
策略模式:把算法族抽成接口
策略模式的核心很简单:定义一个策略接口,每个实现类封装一种算法,调用方持有接口引用,运行时决定用哪个实现。
java
public interface PriceStrategy {
BigDecimal calculate(Order order);
String supportPromoType();
}满减实现:
java
@Component
public class FullOffStrategy implements PriceStrategy {
@Override
public BigDecimal calculate(Order order) {
if (order.getAmount().compareTo(new BigDecimal("200")) < 0) return order.getAmount();
return order.getAmount().subtract(new BigDecimal("30"));
}
@Override
public String supportPromoType() { return "full_off"; }
}调用方只管依赖策略接口,不关心具体实现:
java
@Service
public class PriceService {
@Autowired
private List<PriceStrategy> strategies; // Spring 自动注入全部实现
public BigDecimal calculate(Order order, String promoType) {
PriceStrategy strategy = strategies.stream()
.filter(s -> s.supportPromoType().equals(promoType))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("unsupported promo: " + promoType));
return strategy.calculate(order);
}
}新增促销时只需要加一个 @Component 类,PriceService 一行不改。这符合开闭原则:对扩展开放,对修改关闭。
策略 + 工厂的经典组合
上面用 Map<String, Strategy> 代替了 if-else 做路由,本质上是工厂模式选策略。实际项目中常把策略工厂抽成单独类,方便做单元测试或缓存:
java
@Component
public class PriceStrategyFactory {
private final Map<String, PriceStrategy> map;
public PriceStrategyFactory(List<PriceStrategy> strategies) {
this.map = strategies.stream()
.collect(Collectors.toMap(PriceStrategy::supportPromoType, s -> s));
}
public PriceStrategy get(String type) { return map.get(type); }
}模板方法:固定骨架,可变步骤下沉
模板方法用于"流程骨架固定,但某些步骤的实现不同"的场景。比如数据导入:
java
public abstract class AbstractDataImporter {
// 模板方法:定义骨架,final 防止子类修改
public final ImportResult execute(String filePath) {
File file = openFile(filePath); // 步骤 1:打开文件
List<String> rawLines = parse(file); // 步骤 2:解析
List<Entity> entities = validate(rawLines); // 步骤 3:校验
int count = save(entities); // 步骤 4:落库
return ImportResult.success(count);
}
private File openFile(String path) { /* 通用文件打开逻辑 */ }
private List<String> parse(File f) { /* 通用解析逻辑 */ }
// 抽象步骤:子类必须实现
protected abstract List<Entity> validate(List<String> rows);
protected abstract int save(List<Entity> entities);
// 钩子:子类可选覆盖
protected boolean needCheckHeader() { return false; }
}子类只需要实现校验和落库两个方法:
java
@Component
public class UserDataImporter extends AbstractDataImporter {
@Override
protected List<Entity> validate(List<String> rows) {
// 校验手机号格式、去重
}
@Override
protected int save(List<Entity> entities) {
return userMapper.batchInsert(entities);
}
}Spring 里的 JdbcTemplate、RestTemplate、JmsTemplate 全是模板方法:连接获取、异常处理、资源释放这些重复工作由模板包办,子类只提供 SQL 或回调。
策略 vs 模板方法:组合 vs 继承
策略:整个算法可替换,调用方持有接口,行为通过组合注入。侧重"算法族的运行时切换"。
模板方法:步骤级可变,子类通过继承重写部分步骤。侧重"固定流程中的扩展点"。
选型标准:
- 如果算法之间没有相同的骨架,用策略("换整个算法")
- 如果算法流程相同、只有步奏实现不同,用模板方法("换部分步骤")
- 策略天然可以用 Spring DI 管理,模板方法用继承,测试时子类需要 mock 父类步骤,比策略难
工程中常看到两者混用:模板方法定义一个批量处理流程,流程中某个步骤(比如"如何投递")用策略模式来替换。
动手实操:促销计价 + 模板方法数据导入
促销计价(策略 + Spring 自动注入)
java
// 策略接口
public interface PriceStrategy {
BigDecimal calculate(Order order);
String supportPromoType();
}
// 满减
@Component
public class FullOffStrategy implements PriceStrategy {
@Override
public BigDecimal calculate(Order order) {
BigDecimal amount = order.getAmount();
if (amount.compareTo(new BigDecimal("200")) < 0) return amount;
return amount.subtract(new BigDecimal("30"));
}
@Override
public String supportPromoType() { return "full_off"; }
}
// 折扣
@Component
public class DiscountStrategy implements PriceStrategy {
@Override
public BigDecimal calculate(Order order) {
return order.getAmount().multiply(new BigDecimal("0.85"));
}
@Override
public String supportPromoType() { return "discount"; }
}
// 调用方
@Service
public class OrderPricingService {
private final Map<String, PriceStrategy> strategyMap;
public OrderPricingService(List<PriceStrategy> strategies) {
strategyMap = strategies.stream()
.collect(Collectors.toMap(PriceStrategy::supportPromoType, s -> s));
}
public BigDecimal price(Order order, String promoType) {
PriceStrategy s = strategyMap.get(promoType);
if (s == null) throw new IllegalArgumentException("Unsupported promo: " + promoType);
return s.calculate(order);
}
}模板方法版数据导入
java
public abstract class AbstractDataImporter<T> {
public final int execute(String filePath) {
List<String> lines = readLines(filePath);
List<T> records = parse(lines);
List<T> valid = validate(records);
return save(valid);
}
private List<String> readLines(String path) {
// 通用文件读取
return Files.readAllLines(Paths.get(path));
}
protected abstract List<T> parse(List<String> lines);
protected abstract List<T> validate(List<T> records);
protected abstract int save(List<T> records);
}
@Component
public class UserImporter extends AbstractDataImporter<User> {
@Override
protected List<User> parse(List<String> lines) {
return lines.stream().map(line -> {
String[] parts = line.split(",");
return new User(parts[0], parts[1]);
}).collect(Collectors.toList());
}
@Override
protected List<User> validate(List<User> users) {
return users.stream().filter(u -> u.getPhone() != null).collect(Collectors.toList());
}
@Override
protected int save(List<User> users) {
return userMapper.batchInsert(users);
}
}常见误区与小结
常见误区:
- 把三行 if-else 也拆成策略+工厂——过度设计。策略模式解决的是"新增类型需要改现有代码"的问题,不是"我不想写 if"的问题。if-else 分支稳定在 3 个以内且变化频率低,用 if 就够了。
- 策略类爆炸。20 个渠道 20 个策略类,每新增一个就要加一个类。这时不如用表驱动(一个配置表存表达式,一个策略类读配置计算)。
- 模板方法用继承重写了非钩子方法。模板方法是"骨架固定"的,子类如果把骨架改了就失去了模板的意义。应该把模板方法声明为
final。 - 策略和模板方法混用却不做区分,一个类里同时用了接口注入和继承,导致层次混乱。
小结: 策略和模板方法是消灭 if-else 蔓延的两种基础手段,前者靠组合换算法,后者靠继承变步骤。它们出现在几乎所有框架的源码里——JdbcTemplate、RestTemplate、Spring 事务管理、Comparator 比较器。下一节看观察者模式,从监听器到事件驱动,到消息总线。
参考
参考:GoF《设计模式:可复用面向对象软件的基础》第 5 章(策略模式)、第 24 章(模板方法) 源码:Spring JdbcTemplate 源码
org.springframework.jdbc.core.JdbcTemplate