Skip to content

策略与模板方法:消灭 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 里的 JdbcTemplateRestTemplateJmsTemplate 全是模板方法:连接获取、异常处理、资源释放这些重复工作由模板包办,子类只提供 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

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。
粤ICP备2026104257号-1