策略模式(Strategy):让算法自由切换的艺术

策略模式,说白了就是「把算法封装起来,让它们可以互相替换」。我刚开始接触设计模式时,总觉得这玩意儿不就是多态吗?后来在项目中踩过坑才明白——它解决的是「行为的选择权」问题。

你想想看,如果你的代码里有一堆 if-else,每个分支都是一种算法。今天加一种,明天改一种,改着改着就成了一团乱麻。策略模式就是把这些算法抽出来,变成一个个独立的「策略类」。调用方只需要说「我要这个策略」,具体怎么执行,策略自己知道。

策略模式的结构

策略模式的结构其实很简单,就三个角色:

  • 上下文(Context):持有策略对象的引用,负责调用策略。它不关心策略怎么实现,只关心策略的接口。
  • 策略接口(Strategy):定义所有策略的公共方法。比如一个「计算价格」的方法。
  • 具体策略(ConcreteStrategy):实现策略接口,提供具体的算法。

我画了一张图,帮你理清它们的关系:

Context - strategy: Strategy «interface» Strategy 持有引用 ConcreteStrategyA ConcreteStrategyB ConcreteStrategyC 实现 核心思想:将算法的定义与使用分离 Context 只依赖 Strategy 接口,不依赖具体实现类

你看,Context 只跟 Strategy 接口打交道。具体用哪个策略,由调用方决定。这就是「开闭原则」的体现——对扩展开放,对修改关闭。

关键点:策略模式不是用来消除 if-else 的,而是把 if-else 的选择逻辑从业务代码中剥离出来,交给调用方去决策。

策略模式 vs 状态模式

这两个模式长得有点像,很多初学者容易搞混。我当年也犯过这个错——在一个工作流引擎里,用策略模式去处理状态流转,结果代码越写越别扭。

它们的核心区别在于:

维度 策略模式 状态模式
关注点 算法的选择与替换 对象状态的变化与行为切换
状态管理 不维护状态,策略是独立的 状态对象内部维护状态流转
行为切换 由外部调用方决定切换 状态对象内部自动切换
典型场景 排序算法、支付方式、压缩算法 订单状态、工作流、游戏角色状态
类的关系 策略之间平级,互不依赖 状态之间有关联,知道下一个状态

举个例子你就明白了:

  • 策略模式:你打车,可以选择「快车」「专车」「拼车」。这些策略是平级的,你随时可以换。策略之间不知道彼此的存在。
  • 状态模式:订单有「待支付」「已支付」「已发货」「已完成」。每个状态都知道下一个状态是什么——「已支付」之后必然是「已发货」,你不能从「待支付」直接跳到「已完成」。

我曾经踩过的坑:在一个审批流系统中,我用策略模式实现了「审批通过」「审批驳回」「转交」等操作。结果发现,这些操作之间是有顺序依赖的——必须先「通过」才能「归档」。策略模式根本管不了这种顺序约束,最后不得不重构为状态模式。

策略模式在 Spring Resource 接口中的应用

Spring 框架里到处都是策略模式的影子。最典型的就是 Resource 接口。

你想想看,Spring 需要加载各种资源:classpath 下的文件、文件系统的文件、URL 指向的远程资源、甚至是 ServletContext 里的资源。如果不用策略模式,代码会写成什么样?

// 伪代码——没有策略模式
if (location.startsWith("classpath:")) {
    // 从 classpath 加载
} else if (location.startsWith("file:")) {
    // 从文件系统加载
} else if (location.startsWith("http:")) {
    // 从网络加载
} else {
    // 默认处理
}

这种代码,每加一种资源类型就要改一次。而且 if-else 越堆越多,测试也麻烦。

Spring 的做法是:定义一个 Resource 接口,然后每种资源类型对应一个实现类。

public interface Resource extends InputStreamSource {
    boolean exists();
    boolean isReadable();
    URL getURL() throws IOException;
    File getFile() throws IOException;
    // ... 其他方法
}

具体的实现类有:

  • ClassPathResource — 从 classpath 加载
  • FileSystemResource — 从文件系统加载
  • UrlResource — 从 URL 加载
  • ServletContextResource — 从 ServletContext 加载

那谁来决定用哪个策略呢?答案是 ResourceLoader。它根据资源路径的前缀,选择合适的 Resource 实现。

public interface ResourceLoader {
    Resource getResource(String location);
}

// 默认实现
public class DefaultResourceLoader implements ResourceLoader {
    @Override
    public Resource getResource(String location) {
        if (location.startsWith("classpath:")) {
            return new ClassPathResource(location.substring("classpath:".length()));
        } else if (location.startsWith("file:")) {
            return new FileSystemResource(location);
        } else if (location.startsWith("http:") || location.startsWith("https:")) {
            return new UrlResource(location);
        } else {
            // 默认当作 classpath 处理
            return new ClassPathResource(location);
        }
    }
}

个人习惯:我在设计类似框架时,会把策略的选择逻辑也抽象出来。比如用 Map<String, ResourceFactory> 来注册策略,这样新增一种资源类型时,只需要添加一个工厂类,不需要改选择逻辑。Spring 后来的版本也确实这么做了。

你看,这就是策略模式的威力。调用方(比如 ResourceLoader)只需要知道 Resource 接口,不需要关心底层是文件还是网络。每个策略(ClassPathResource 等)独立开发和测试,互不干扰。

什么时候用策略模式?

我总结了几条经验:

  • 有多个相似的算法:比如排序、压缩、加密、校验。它们做的是同一件事,只是方式不同。
  • 算法需要动态切换:比如根据用户选择、配置参数、运行时条件来决定用哪个算法。
  • 想避免 if-else 或 switch 的膨胀:当条件分支超过 3 个,而且每个分支的逻辑都比较复杂时,就该考虑策略模式了。
  • 算法需要独立测试:每个策略可以单独写单元测试,不用模拟复杂的上下文。

注意:策略模式不是银弹。如果你的算法只有两三种,而且变化频率很低,硬写 if-else 反而更简单。过度设计也是罪。

好了,策略模式就聊到这儿。记住它的核心:把算法封装成对象,让它们可以互相替换。下次你看到一堆 if-else 在选算法时,不妨想想——是不是该用策略模式了?


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