设计模式之策略模式
阅读指引:本文较长,可按需跳读。
- 想快速理解 → 读「概念」「举例理解」「工作流程」;
- 想看代码 → 读「代码示例」(含 UML、C++、Java、函数式轻量变体);
- 想区分相似模式 → 读「策略模式与状态模式的对比」;
- 想判断该不该用 → 读「优缺点」「一个反面例子」「应用场景」;
- 准备落地 → 读「注意事项」与「策略模式不负责什么」。
概念
定义
策略模式(Strategy Pattern) 是一种行为型设计模式。它的核心思想是:
把一组可互换的算法分别封装起来,让它们可以互相替换,并且算法的变化独立于使用算法的客户端。
换句话说:当同一件事有多种做法,并且需要在运行时选择其中一种做法时,可以把每种做法封装成一个"策略",由上下文对象持有并调用。
结构
策略模式通常有三个角色:
- Strategy 策略接口
- 定义所有策略必须实现的方法。
- 例如:
calculatePrice()、pay()、sort()。
- ConcreteStrategy 具体策略
- 实现具体算法。
- 例如:微信支付、支付宝支付、银行卡支付。
- Context 上下文
- 持有一个策略接口引用。
- 把客户端请求委托给当前策略执行。
- 客户端可以在运行时切换策略。
举例理解
假设你现在需要去机场,可以选择:打车、坐地铁、自驾、坐机场大巴。
这些方式都能让你从家到机场。但怎么去中间的过程不相同:花费、时间、路线、舒适度等等都不一样。
面对多选择,你可能会下意识想到 if/else 或 switch:
如果选打车:
计算打车路线、费用、时间
否则如果选地铁:
计算地铁换乘、票价、时间
否则如果选自驾:
计算自驾路线、停车费、时间
否则如果选机场大巴:
计算大巴站点、票价、时间
相当于把所有的出行算法都放在了一个方法内。如果新增一种"拼车 + 地铁",就要新增一个判断分支,每种方式的计算逻辑也混在一起,越来越乱。
策略模式的解决办法: 把每种出行方式都封装成一个独立的"策略"。
-
策略接口:出行方式
你可以理解为一个统一规则:任何出行方式都必须回答:从家到机场要多久、花多少钱、怎么走。
出行方式 { 规划路线(起点, 终点) } -
具体策略:具体的出行方式
- 打车策略:规划打车路线,算车费和时间
- 地铁策略:规划换乘路线,算票价和时间
- 自驾策略:规划自驾路线,算油费、停车费和时间
- 机场大巴策略:规划大巴站点,算票价和时间
它们都实现同一个"出行方式"接口。
-
上下文:出行规划器
出行规划器 { 当前出行方式; 设置出行方式(方式) { 当前出行方式 = 方式; } 去机场() { 当前出行方式.规划路线(家, 机场); } }出行规划器不关心打车具体怎么算、地铁怎么换乘,它只负责调用当前策略的"规划路线"。
-
客户端:你
此时你只需要根据你的实际情况做出选择:时间充足想省钱选地铁;快迟到了切换成打车;行李很多选自驾;想便宜又不想挤地铁选机场大巴。
如果早上你本来选择地铁:
规划器.设置出行方式(new 地铁策略()); 规划器.去机场();结果发现时间来不及,于是运行时切换:
规划器.设置出行方式(new 打车策略()); 规划器.去机场();规划器没有变,只是内部换了一种"去机场算法"。
策略模式就像你手机里的导航 App:目的地都是机场,但你可以选"最快路线"“少收费路线”“避开拥堵路线”。导航 App 不关心路线怎么算,它只调用当前选中的路线策略;你随时可以切换策略。
工作流程
策略模式的工作流程很简单,可以理解成:
-
先定一个统一规则:比如"去机场"这件事,不管怎么去,都要能完成"去机场"。
-
把每种做法分别做出来:打车是一种做法,地铁是一种做法,自驾是一种做法。每种做法各自管自己,互不混在一起。
-
找一个执行者,手里拿着"当前做法":这个执行者就像"出行规划器",它不关心具体怎么走,只负责按当前选中的方式去机场。
-
你选一种做法,交给执行者:比如你今天选地铁,就把"地铁做法"交给规划器。
-
执行者调用当前做法:你说"去机场",规划器就按手里的"地铁做法"执行。
-
想换做法时,直接替换:发现来不及了,你把规划器手里的"地铁做法"换成"打车做法"。规划器本身不用改,再执行时就按打车走。
当然,策略模式能换的是"下一次调用用哪个策略",不是把正在执行到一半的算法中途打断换掉。 换策略发生在两次执行之间,不是执行到一半自动切换(你不能坐地铁坐到一半然后就能换策略到打车,这相当于另一种策略,或者需要在业务逻辑中分段处理)。
一句话概括:
定统一规则 → 做多种做法 → 执行者拿着当前做法 → 你选一个交给它 → 它按这个做法办事 → 想换就换。
代码示例
UML
下面用"家到机场"的例子,画出策略模式涉及的所有类,并标注关系。
| 类/接口 | 角色 | 在"家到机场"里的含义 |
|---|---|---|
TravelStrategy | 策略接口 Strategy | 统一规则:任何出行方式都必须能完成"去机场" |
TaxiStrategy | 具体策略 ConcreteStrategy | 打车去机场的算法 |
SubwayStrategy | 具体策略 ConcreteStrategy | 坐地铁去机场的算法 |
DriveStrategy | 具体策略 ConcreteStrategy | 自驾去机场的算法 |
AirportBusStrategy | 具体策略 ConcreteStrategy | 坐机场大巴去机场的算法 |
TravelPlanner | 上下文 Context | 出行规划器,持有当前策略并委托执行 |
Client | 客户端 Client | 你,负责选策略、创建规划器、触发执行、需要时切换策略 |
图中最重要的三条线:
- 具体策略 → 策略接口:实现关系,负责"有多种做法";
- 上下文 → 策略接口:聚合关系,负责"拿着当前做法并委托执行";
- Client → 具体策略:依赖关系。这里 Client 依赖具体策略是合理的,因为"选哪个策略"正是 Client 的职责。如果项目里选择逻辑复杂,可以用工厂、注册表或 DI 容器把这条依赖从 Client 转移到基础设施层,让 Client 只依赖工厂接口。
而上下文与具体策略之间没有直接依赖线,这正是策略模式解耦的核心。
C++ 实现
// ============================================================
// 策略模式(Strategy Pattern)完整示例
// 场景:从家去机场,有多种出行方式,可在运行时切换。
//
// 对应 UML:
// TravelStrategy —— 策略接口(抽象基类)
// TaxiStrategy —— 具体策略
// SubwayStrategy —— 具体策略
// DriveStrategy —— 具体策略
// AirportBusStrategy —— 具体策略
// TravelPlanner —— 上下文(Context),持有策略并委托
// Client(main) —— 客户端,创建策略、注入、切换
// ============================================================
#include <iostream>
#include <memory>
#include <string>
#include <utility>
#include <vector>
// ------------------------------------------------------------
// 1. 策略接口 Strategy
//
// 说明:
// 这里把 goToAirport() 声明为 const,表达"示例中的策略是
// 无状态的,算法不修改策略对象本身"。
// 如果某个策略需要维护自身状态(例如记录调用次数、缓存),
// 应去掉 const,并重新评估其能否被多上下文共享。
// ------------------------------------------------------------
class TravelStrategy {
public:
// 虚析构函数:保证通过基类指针删除派生类对象时,
// 派生类的析构函数能被正确调用,避免资源泄漏。
virtual ~TravelStrategy() = default;
// 纯虚函数:统一契约,每种出行方式都必须实现"去机场"。
virtual void goToAirport() const = 0;
};
// ------------------------------------------------------------
// 2. 具体策略 ConcreteStrategy:打车
// ------------------------------------------------------------
class TaxiStrategy : public TravelStrategy {
public:
void goToAirport() const override {
std::cout << "【打车】叫车、走机场高速,预计 40 分钟,花费约 80 元。"
<< std::endl;
}
};
// ------------------------------------------------------------
// 3. 具体策略 ConcreteStrategy:地铁
// ------------------------------------------------------------
class SubwayStrategy : public TravelStrategy {
public:
void goToAirport() const override {
std::cout << "【地铁】步行到站、换乘 2 次,预计 60 分钟,花费约 8 元。"
<< std::endl;
}
};
// ------------------------------------------------------------
// 4. 具体策略 ConcreteStrategy:自驾
// ------------------------------------------------------------
class DriveStrategy : public TravelStrategy {
public:
void goToAirport() const override {
std::cout << "【自驾】走机场高速、停车 3 天,预计 35 分钟,油费+停车约 120 元。"
<< std::endl;
}
};
// ------------------------------------------------------------
// 5. 具体策略 ConcreteStrategy:机场大巴
// ------------------------------------------------------------
class AirportBusStrategy : public TravelStrategy {
public:
void goToAirport() const override {
std::cout << "【机场大巴】步行到站点、等车 15 分钟,预计 70 分钟,花费约 25 元。"
<< std::endl;
}
};
// ------------------------------------------------------------
// 6. 上下文 Context:出行规划器
//
// 设计取舍:
// - 这里用 shared_ptr 是为了演示场景 4 的"策略共享"。
// 若上下文独占策略,unique_ptr 更轻、语义更清晰,是更常见的选择;
// - 用 shared_ptr<const TravelStrategy> 也可以,能在类型层面表达
// "上下文不修改策略",但本示例保留非 const 版本以便与
// "策略可能有状态"的扩展场景兼容;
// - 本示例保留了"未选择策略"的容错分支,属于"容忍式"设计;
// Java 版则选择 fail-fast(构造时拒绝 null),两种都是合理选择,
// 详见 Java 版注释。
// ------------------------------------------------------------
class TravelPlanner {
public:
explicit TravelPlanner(std::shared_ptr<TravelStrategy> strategy)
: strategy_(std::move(strategy)) {}
// 运行时切换策略,只影响下一次调用。
void setStrategy(std::shared_ptr<TravelStrategy> strategy) {
strategy_ = std::move(strategy);
}
// 对外统一入口:委托给当前策略。
void goToAirport() const {
if (!strategy_) {
std::cout << "【提示】还没有选择出行方式,无法去机场。" << std::endl;
return;
}
// 动态分派:实际执行哪个 goToAirport(),取决于 strategy_ 指向的真实对象。
strategy_->goToAirport();
}
private:
// 上下文只依赖策略接口,不依赖任何具体策略。
std::shared_ptr<TravelStrategy> strategy_;
};
// ------------------------------------------------------------
// 7. 客户端 Client
// ------------------------------------------------------------
int main() {
std::cout << "========== 场景 1:默认选地铁 ==========" << std::endl;
TravelPlanner planner(std::make_shared<SubwayStrategy>());
planner.goToAirport();
std::cout << "\n========== 场景 2:发现地铁来不及,运行时切换打车 ==========" << std::endl;
// 注意:上一次策略已执行完,不是"执行到一半中断再切换"。
planner.setStrategy(std::make_shared<TaxiStrategy>());
planner.goToAirport();
std::cout << "\n========== 场景 3:用有序容器按名称选择策略 ==========" << std::endl;
// 选择说明:
// 这里用 std::vector 保持"注册顺序",是为了演示输出稳定。
// 实际注册表用 std::unordered_map 也没问题,只是遍历顺序不稳定,
// 会对示例输出造成干扰。如果确实需要按 key 有序,
// 用 std::map;如果需要 O(1) 查找且不关心顺序,用 std::unordered_map。
std::vector<std::pair<std::string, std::shared_ptr<TravelStrategy>>> registry{
{"taxi", std::make_shared<TaxiStrategy>()},
{"subway", std::make_shared<SubwayStrategy>()},
{"drive", std::make_shared<DriveStrategy>()},
{"airportBus", std::make_shared<AirportBusStrategy>()},
};
for (const auto& [name, strategy] : registry) {
std::cout << "选择出行方式 [" << name << "]:" << std::endl;
planner.setStrategy(strategy);
planner.goToAirport();
}
std::cout << "\n========== 场景 4:策略共享 ==========" << std::endl;
// 注意:"无状态"指策略对象自身不保存可变数据;
// 如果策略方法内部依赖了共享的可变外部对象(如静态缓存、共享配置),
// 共享仍需单独评估线程安全,不能仅凭"类里没有成员变量"就判定安全。
auto sharedTaxi = std::make_shared<TaxiStrategy>();
TravelPlanner plannerA(sharedTaxi);
TravelPlanner plannerB(sharedTaxi);
std::cout << "规划器 A:";
plannerA.goToAirport();
std::cout << "规划器 B:";
plannerB.goToAirport();
return 0;
}
Java 实现
// ============================================================
// 策略模式(Strategy Pattern)完整示例(Java 版)
// ============================================================
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
// ------------------------------------------------------------
// 1. 策略接口 Strategy
// ------------------------------------------------------------
interface TravelStrategy {
// 统一契约:每种出行方式都必须实现"去机场"。
void goToAirport();
}
// ------------------------------------------------------------
// 2. 具体策略:打车
// ------------------------------------------------------------
class TaxiStrategy implements TravelStrategy {
@Override
public void goToAirport() {
System.out.println("【打车】叫车、走机场高速,预计 40 分钟,花费约 80 元。");
}
}
// ------------------------------------------------------------
// 3. 具体策略:地铁
// ------------------------------------------------------------
class SubwayStrategy implements TravelStrategy {
@Override
public void goToAirport() {
System.out.println("【地铁】步行到站、换乘 2 次,预计 60 分钟,花费约 8 元。");
}
}
// ------------------------------------------------------------
// 4. 具体策略:自驾
// ------------------------------------------------------------
class DriveStrategy implements TravelStrategy {
@Override
public void goToAirport() {
System.out.println("【自驾】走机场高速、停车 3 天,预计 35 分钟,油费+停车约 120 元。");
}
}
// ------------------------------------------------------------
// 5. 具体策略:机场大巴
// ------------------------------------------------------------
class AirportBusStrategy implements TravelStrategy {
@Override
public void goToAirport() {
System.out.println("【机场大巴】步行到站点、等车 15 分钟,预计 70 分钟,花费约 25 元。");
}
}
// ------------------------------------------------------------
// 6. 上下文 Context:出行规划器
//
// 设计取舍(与 C++ 版对照):
// - Java 版选择 fail-fast:构造时和 setStrategy 时都拒绝 null,
// 因为在本示例中"没有策略的规划器"是无效对象,越早失败越容易定位;
// - C++ 版选择"容忍式":允许持有空策略,goToAirport 时打提示,
// 演示另一种常见设计——上下文可以处于"未选择策略"的中间态。
// 两种都合理,取决于业务是否允许"规划器存在但尚未选策略"。
// ------------------------------------------------------------
class TravelPlanner {
private TravelStrategy strategy;
public TravelPlanner(TravelStrategy strategy) {
this.strategy = Objects.requireNonNull(strategy, "strategy 不能为 null");
}
public void setStrategy(TravelStrategy strategy) {
this.strategy = Objects.requireNonNull(strategy, "strategy 不能为 null");
}
// 对外统一入口:委托给当前策略执行。
public void goToAirport() {
strategy.goToAirport();
}
}
// ------------------------------------------------------------
// 7. 客户端 Client
// ------------------------------------------------------------
public class StrategyDemo {
public static void main(String[] args) {
System.out.println("========== 场景 1:默认选地铁 ==========");
TravelPlanner planner = new TravelPlanner(new SubwayStrategy());
planner.goToAirport();
System.out.println("\n========== 场景 2:运行时切换打车 ==========");
planner.setStrategy(new TaxiStrategy());
planner.goToAirport();
System.out.println("\n========== 场景 3:用 Map 按字符串选择策略 ==========");
// 用 LinkedHashMap 保证遍历顺序与注册顺序一致,方便输出演示。
Map<String, TravelStrategy> registry = new LinkedHashMap<>();
registry.put("taxi", new TaxiStrategy());
registry.put("subway", new SubwayStrategy());
registry.put("drive", new DriveStrategy());
registry.put("airportBus", new AirportBusStrategy());
// 兜底策略:应对"用户输入拼错、配置缺失、版本不一致"的情况。
registry.put("default", new SubwayStrategy());
// 模拟用户输入,其中 "walk" 是未注册的 key,用来演示兜底。
String[] userInputs = {"taxi", "subway", "drive", "airportBus", "walk"};
for (String input : userInputs) {
System.out.println("选择出行方式 [" + input + "]:");
// 关键:不能假设 registry 一定命中,必须有兜底。
TravelStrategy strategy = registry.getOrDefault(input, registry.get("default"));
planner.setStrategy(strategy);
planner.goToAirport();
}
System.out.println("\n========== 场景 4:策略共享 ==========");
// "无状态"指策略对象自身不保存可变数据;
// 若策略内部依赖共享可变对象,仍需评估线程安全。
TravelStrategy sharedTaxi = new TaxiStrategy();
TravelPlanner plannerA = new TravelPlanner(sharedTaxi);
TravelPlanner plannerB = new TravelPlanner(sharedTaxi);
System.out.print("规划器 A:");
plannerA.goToAirport();
System.out.print("规划器 B:");
plannerB.goToAirport();
}
}
函数式轻量变体
当策略接口只有一个无参(或少参)方法、且策略无状态、逻辑简单、不需要被 DI 管理时,不必新建接口和多个类——用 JDK / C++ 自带的函数式接口即可,结构更轻。
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Supplier;
// 上下文:直接持有 Runnable / Supplier 作为"策略"
class FnTravelPlanner {
private Runnable strategy;
FnTravelPlanner(Runnable s) { this.strategy = s; }
void setStrategy(Runnable s) { this.strategy = s; }
void goToAirport() { strategy.run(); }
}
public class FnStrategyDemo {
public static void main(String[] args) {
// 用 Map 注册"函数式策略",key 是名字,value 是一段行为
Map<String, Runnable> registry = new LinkedHashMap<>();
registry.put("taxi", () -> System.out.println("【打车】叫车、走机场高速,预计 40 分钟,约 80 元。"));
registry.put("subway", () -> System.out.println("【地铁】步行到站、换乘 2 次,预计 60 分钟,约 8 元。"));
registry.put("drive", () -> System.out.println("【自驾】走机场高速、停车 3 天,预计 35 分钟,油费+停车约 120 元。"));
FnTravelPlanner planner = new FnTravelPlanner(registry.get("subway"));
for (Map.Entry<String, Runnable> e : registry.entrySet()) {
System.out.println("选择出行方式 [" + e.getKey() + "]:");
planner.setStrategy(e.getValue());
planner.goToAirport();
}
}
}
取舍对照:
| 维度 | 传统策略类 | 函数式策略 |
|---|---|---|
| 适用方法数 | 多方法接口 | 单方法接口 |
| 是否需要 DI 管理 | 常需要(Spring Bean) | 通常不需要 |
| 是否需要状态 | 可有状态 | 无状态为主 |
| 独立测试 | 类可独立测试 | lambda 内联,测试需包装 |
| 可读性 | 类名即文档,一目了然 | 短逻辑清晰,长逻辑易失控 |
| 类爆炸风险 | 高 | 低 |
经验小谈:逻辑超过 5~10 行、需要复用、需要被 Spring 管理、需要独立测试,就退回类实现;否则函数式接口更轻。
策略模式与状态模式的对比
策略模式和状态模式在代码结构上几乎一模一样:都是"上下文持有抽象接口,具体实现类实现接口,上下文委托调用"。所以很多人会混淆。但它们的意图、切换控制方、对象间关系完全不同。
一、主要区别
- 策略模式:同一件事有多种算法,由外部(客户端)选择用哪一种。
- 状态模式:对象的行为随内部状态改变,状态之间可以自动转移。
用"去机场"类比:
- 策略模式:你主动选打车还是地铁,出行方式之间互不知道彼此。
- 状态模式:你走到"地铁站内"“检票口”"站台"不同位置,行为自动变化,且位置之间会自然流转。
二、核心差异
| 对比维度 | 策略模式 | 状态模式 |
|---|---|---|
| 核心意图 | 封装可互换的算法 | 让行为随内部状态改变 |
| 切换由谁控制 | 通常由客户端主动选择 | 通常由上下文或状态自身触发转移 |
| 具体实现之间是否知道彼此 | 通常互不知道,彼此独立 | 通常知道彼此,能触发转移到另一个状态 |
| 是否存在自动转移 | 通常没有 | 通常有 |
| 上下文是否感知状态变化 | 不感知,只持有当前策略 | 感知,状态转移是它的核心逻辑 |
| 关注点 | "怎么做"可替换 | "现在是什么状态"决定行为 |
| 典型变化 | 新增一种算法 | 新增一种状态及转移规则 |
| 客户端职责 | 负责选策略、注入、切换 | 通常只发起动作,不关心内部状态转移 |
最关键的一条:
策略模式是"我选一种做法";状态模式是"对象处于某种状态,行为随之改变,状态还会自己流转"。
三、用"去机场"分别实现,对比更直观
策略模式版本(客户端主动换):
TravelPlanner planner = new TravelPlanner(new SubwayStrategy());
planner.goToAirport(); // 坐地铁
planner.setStrategy(new TaxiStrategy()); // 客户端主动换
planner.goToAirport(); // 打车
SubwayStrategy不知道TaxiStrategy存在;- 没有"自动从地铁转到打车"的逻辑;
- 换的是下一次调用的算法。
状态模式版本(状态自身转移):
TravelContext context = new TravelContext(new AtHomeState());
context.move(); // 在家 → 出门
context.move(); // 出门 → 地铁站
context.move(); // 地铁站 → 站台
context.move(); // 站台 → 机场
class AtHomeState implements TravelState {
public void move(TravelContext ctx) {
System.out.println("出门,前往地铁站");
ctx.setState(new ToSubwayState()); // 状态自己触发转移
}
}
- 状态转移由状态自身或上下文触发;
- 每个状态知道下一个状态是谁;
- 客户端只调用
move(),不关心内部怎么转。
四、判断用哪个
| 问题 | 选策略 | 选状态 |
|---|---|---|
| 切换动作由谁发起? | 客户端主动选 | 对象内部状态变化驱动 |
| 实现类之间要知道彼此吗? | 互不知道 | 需要知道下一个状态 |
| 变化的核心是什么? | 算法会增、会换,"做同一件事"不变 | 对象有明确状态阶段,行为随阶段变 |
五、常见误区
- 误区 1:状态模式就是"策略模式的自动版"。 不准确。状态模式的重点不是"自动",而是行为由内部状态决定,状态之间构成转移关系。有些状态转移也由外部事件触发,但转移逻辑仍在状态/上下文内部。
- 误区 2:用了
setStrategy()就是状态模式。 不是。只要有"客户端主动替换实现"的动作,本质仍是策略模式。 - 误区 3:两者不能共存。 可以。订单有状态流转(状态模式),每个状态下又有多种支付方式可选(策略模式)。
六、典型使用场景对照
| 策略模式 | 状态模式 |
|---|---|
| 支付方式:微信、支付宝、银行卡 | 订单状态:待支付、已支付、已发货、已完成 |
| 折扣计算:无折扣、VIP、SVIP | 电梯状态:停止、上行、下行、开门 |
| 排序算法:快排、归并、堆排 | 交通灯:红、绿、黄 |
| 压缩算法:zip、rar、7z | 播放器:播放、暂停、停止 |
| 出行方式:打车、地铁、自驾 | 行程阶段:在家、路上、安检、登机 |
一句话区分:策略模式是"我挑一个工具用";状态模式是"我到哪一步,就做哪一步的事,而且会自己走到下一步"。
策略模式的优缺点
优点
1. 符合开闭原则,扩展性好。 新增一种算法,通常只需新增一个实现策略接口的类,再在注册表或工厂里登记一下,已有的策略类和上下文都不需要修改。
2. 消除大量条件分支,代码更清晰。 把原本堆在一个方法里的 if-else 或 switch 拆成独立的策略类,每个类只负责一种算法,职责单一。选择逻辑可集中到 Map、工厂或配置里。
3. 算法可以独立复用和测试。 每个策略类都是独立单元,可单独编写单元测试,也可被多个上下文复用。
4. 支持运行时动态切换。 客户端可以在两次调用之间替换上下文持有的策略引用,从而改变下一次执行所用的算法。
5. 上下文与具体算法解耦。 上下文只依赖策略接口,不依赖任何具体策略类,符合依赖倒置原则。
6. 算法实现和选择逻辑分离。 "用哪种算法"和"算法怎么实现"是两件事,互不纠缠,便于分别演进。
缺点
1. 类数量会增加。 每种算法一个策略类,算法一多,类的数量就明显上升。算法很简单时,引入策略模式反而让结构变重。
2. 客户端需要了解策略差异。 客户端要负责选择策略,就必须知道有哪些策略、各自适合什么场景。选择逻辑复杂时,需额外引入工厂、注册表、配置中心分担。
3. 策略之间难以自然共享状态或逻辑。 策略类通常彼此独立,公共逻辑要么重复,要么抽到基类或工具类里。算法之间有复杂共享状态或相互依赖时,策略模式表达起来较别扭。
4. 可能造成过度设计。 算法种类少、几乎不变化、分支也简单时,硬套策略模式会引入不必要的接口和类。模式的价值在于隔离变化,没有变化时引入模式就是负担。
5. 上下文与策略的通信需要设计。 数据怎么传、传多少,需要提前设计。传少了策略不够用,传多了策略与上下文耦合变紧。
6. 无法中断正在执行的算法。 策略模式切换的是"下一次调用用哪个策略",不能把正在执行的算法中途替换掉(详见「注意事项」第 11 条)。
7. 策略对象生命周期需要管理。 策略是有状态还是无状态、能否共享、由谁创建和销毁,都需要考虑。多线程环境下要额外注意。
一个反面例子
“不要为了模式而模式”,这里给一个具体的对照:
// 场景:只有 2 种支付方式,且未来一年大概率不会新增
// 写法 A:直接 if-else —— 1 个方法,10 行
String pay(String type, long amount) {
if ("wechat".equals(type)) return wechatPay(amount);
if ("alipay".equals(type)) return alipayPay(amount);
throw new IllegalArgumentException("unknown pay type: " + type);
}
// 写法 B:引入策略模式 —— 1 接口 + 2 实现 + 1 上下文 + 1 注册表
// 收益:新增渠道时不改入口;
// 成本:文件数从 1 变 5,新人需先理解 5 个类才能改一行逻辑。
判断标准:
- 算法数量 ≥ 3 且预计会增长 → 策略模式值得;
- 算法几乎不变、数量少、分支简单 →
if-else更直接; - 算法简单但数量多、需要运行时选择 → 策略模式 + 注册表;
- 算法简单、无状态、方法单一 → 函数式策略 是折中方案。
应用场景
以下场景是策略模式常用的地方,但未必只能用策略模式。判断时看两条:
① 同一行为是否存在多种可互换的实现;
② 是否需要运行时选择其中一种。
如果流程骨架固定、只有某一步可变,优先考虑模板方法;如果策略创建逻辑复杂,需配合工厂。
主流框架使用策略模式的场景
Java 标准库的 Comparator。 Comparator 接口本身就是策略抽象,compare() 是策略方法;调用 Collections.sort(list, comparator) 或 list.sort(comparator) 时,排序算法保持不变,但"怎么比较"被替换成了你传入的策略。按姓名排、按年龄排、按价格排,只需换一个 Comparator 实现,排序主流程一行都不用改。
Spring 的 PlatformTransactionManager。 Spring 事务管理的核心接口就是策略接口,DataSourceTransactionManager、JpaTransactionManager、JtaTransactionManager 是不同的事务策略实现。上层代码只依赖 PlatformTransactionManager,具体用哪种事务管理器由配置或运行环境决定。
Spring MVC 的 HandlerMapping 与 ViewResolver。 DispatcherServlet 作为上下文,根据配置选择不同的 HandlerMapping 来定位请求处理器;ViewResolver 同样通过策略模式支持 JSP、Thymeleaf、Freemarker 等多种视图技术。
Spring 的 InstantiationStrategy。 这是 Spring 内部用于创建 Bean 实例的策略接口,SimpleInstantiationStrategy 和 CglibSubclassingInstantiationStrategy 分别代表"直接反射实例化"和"CGLIB 子类化实例化"两种算法。
前端与 JavaScript 生态。 表单校验是前端最典型的策略模式应用:把"非空"“最小长度”“手机号格式”"邮箱格式"等规则各自封装成独立校验策略,调用方按顺序执行规则集。动画缓动函数(linear、easeIn、easeOut)本质上也是可互换的缓动策略,React 中不同状态对应不同 reducer 的设计同样带有策略模式的影子。
其他场景
支付渠道路由。 微信支付、支付宝、银联、云闪付等渠道在"支付"这个统一动作下,下单、签名、回调验签的算法各不相同。把每个渠道封装成独立策略,支付主流程只依赖统一的支付接口。
优惠与折扣计算。 电商结算中经常同时存在无优惠、满减、折扣码、会员价、阶梯价、秒杀等多种计价规则。每一种规则是一个价格策略,结算引擎根据活动配置或用户身份选择对应策略执行。
风控与审核策略。 同一笔交易或同一个用户,可以根据风险等级、用户类型、金额区间选择不同的审核策略:白名单跳过、基础校验、人工复核,或者规则引擎与机器学习模型之间的切换。
文件解析与导出。 同一个"读取文件"的动作,面对 Excel、CSV、PDF、Word 时解析算法完全不同;导出时也有多种格式选择。
注意:如果"打开→读取→解析→关闭"的流程骨架固定、只有"解析"这一步可变,这更接近模板方法;只有连流程都可变时才是策略模式。
登录与认证方式。 密码登录、短信验证码、扫码、OAuth、Passkey 等认证方式共享"认证"这个统一语义,但校验流程各异。
缓存淘汰与压缩算法选择。 缓存容器需要支持 LRU、LFU、FIFO、TTL 等不同的淘汰策略,容器本身负责存储和读取,淘汰逻辑委托给当前策略。文件上传场景中按类型选择 gzip、brotli 或不压缩,也是同样的结构。
消息处理与数据汇聚。 系统监听多种类型的消息,商品消息、价格消息、库存消息需要补充不同的数据后写入同一个存储。
注意:这类场景往往是策略 + 观察者 + 责任链的组合,策略只负责"补充数据"这一步,消息分发由观察者/责任链承担。
排序、推荐与搜索匹配。 列表需要支持按最新、热度、距离、价格、个性化推荐等多种排序方式,搜索需要支持精确匹配、模糊匹配、拼音匹配、语义匹配等。
游戏 AI 行为切换。 游戏角色可以在巡逻、追击、防御、逃跑等行为之间切换,每个行为是一种决策策略。
注意事项
必读:接口与契约设计
1. 策略接口要稳定,参数要一次设计到位。 策略接口是所有具体策略共同遵守的契约,一旦上线后被大量实现类依赖,再改方法签名成本很高。设计时要考虑清楚:方法需要哪些输入、是否需要返回值、是否需要抛出异常、是否要支持上下文数据。
2. 策略接口的粒度要合适,不要做成"万能接口"。 一个策略接口只应表达一类可互换的算法。如果把支付、退款、对账都塞进同一个接口,就会出现大量空实现或抛 UnsupportedOperationException 的实现类。
3. 策略是否需要返回值、是否需要上下文,要提前想清楚。 如果一开始设计成无返回值,后续需要结果时就得改接口。反过来,如果把整个上下文对象直接传给策略,策略可能反过来修改上下文状态,造成耦合和副作用。
4. 策略接口要有清晰的异常约定。 每个策略可能抛出不同异常,调用方需要统一处理。接口层应约定好异常类型:是抛受检异常、运行时异常,还是返回结果对象。
必读:选择逻辑的位置
5. "选哪个策略"的逻辑不要塞进上下文。 上下文的职责是持有并委托策略,不是判断该用哪个策略。选择逻辑应放在客户端、工厂、注册表或配置中心里。
6. 选择逻辑本身也要防止膨胀。 把 if-else 从业务方法搬到客户端,不等于消灭了分支。选择条件复杂时,应进一步用 Map 注册表、工厂方法、配置映射或规则引擎来承载。
7. 要处理"找不到策略"的情况。 按字符串或枚举从注册表取策略时,key 拼错、配置缺失、版本不一致都可能导致取到 null。必须有明确的兜底策略:是抛异常、用默认策略,还是返回错误结果(见 Java 版场景 3 的 getOrDefault 示例)。
8. 默认策略要显式设计,不要靠运气。 默认策略应明确声明并测试,而不是在取不到策略时临时 new 一个,导致行为不可预期。
选择逻辑的演进路径(按规模从轻到重):
| 阶段 | 选择方式 | 适用规模 | 新增策略要改哪里 |
|---|---|---|---|
| 1 | 客户端 if-else | 2~3 个策略 | 加分支 |
| 2 | Map 注册表 | 中等数量(4~10) | 加一行注册 |
| 3 | 工厂 / 枚举 + 注册表 | 需要统一创建和兜底 | 加注册 + 枚举项 |
| 4 | Spring DI(Map<String, Strategy> 注入) | 项目级 | 加 Bean 即可 |
| 5 | 配置中心 / 规则引擎 | 策略由配置或规则驱动 | 改配置,不改代码 |
不必一步到位,按规模演进即可。跨级升级往往是过度设计。
必读:状态、共享与并发
9. 尽量让策略无状态,无状态策略才能安全共享。 策略对象如果内部保存可变状态,多个上下文共享同一个实例时就会互相干扰。无状态策略可以做成单例或 Spring Bean;有状态策略则要考虑每次创建新实例,或把状态放到方法参数、上下文或外部存储里。
10. 多线程环境下要确认策略是否线程安全。 即使策略本身无成员变量,如果它依赖的成员对象不是线程安全的,或策略方法里用了共享的可变静态变量,仍然会有并发问题。"无状态"只描述策略对象自身,不保证其依赖链安全。
11. 切换策略只影响下一次调用,不要误解为中断当前执行。 setStrategy() 替换的是上下文持有的引用,已经进入旧策略方法的那次调用会继续执行完。如果业务需要"执行到一半改变做法",必须在业务层显式分段。
12. 策略对象的生命周期要明确。 策略由谁创建、由谁销毁、能否缓存、是否每次调用新建,需要统一约定。频繁 new 会增加 GC 压力;长期缓存有状态策略又可能带来状态残留。
必读:上下文与策略的通信
13. 传给策略的数据要最小够用。 策略需要的数据应通过参数显式传入,而不是让策略去反查上下文。传整个上下文虽然方便,但会让策略依赖上下文的具体结构。
14. 策略不应反向修改上下文的核心状态。 需要回传结果时,应通过返回值或结果对象表达,而不是让策略直接改上下文。确需回调时,应定义清晰的回调接口。
15. 策略之间的公共逻辑要合理抽取。 多个策略有重复代码时,可以抽到抽象基类、工具类或默认方法里,但不要为了复用把不相关的策略强行塞进继承体系。
进阶:工程落地(Spring / DI / 序列化)
16. 与工厂、注册表或依赖注入配合使用。 策略模式本身只解决"持有并委托",不解决"如何创建和查找策略"。实际项目中通常用工厂、Map 注册表或 Spring 容器来管理策略实例和名称映射。
17. Spring 中要注意 Bean 名称冲突和注入方式。 多个策略实现同一个接口时,按类型注入会因候选 Bean 过多而失败。通常需要给每个策略指定 Bean 名称,用 Map<String, Strategy> 注入,或配合自定义注解和工厂按名称查找。注入方式要在项目内统一。
18. 策略的标识要稳定且可配置。 用字符串作为策略 key 时,要统一命名规范。key 最好来自枚举或常量,而不是散落的魔法字符串。对外暴露的 key 还要考虑兼容性,重命名可能影响配置中心、数据库存量数据和外部调用方。
19. 序列化与远程调用场景要额外考虑。 如果策略对象需要跨进程传输或持久化,通常不建议直接序列化策略对象,而是序列化策略标识,由接收方根据标识重新选择策略,避免实现类变更导致反序列化失败。
进阶:可维护性与测试
20. 不要为了模式而模式。 如果只有两三种算法、几乎不变化、分支也很简单,直接写 if-else 或 switch 更直观(见「一个反面例子」)。
21. 警惕策略类爆炸。 可以通过合并真正相近的策略、用函数式接口替代简单策略、用配置驱动参数化策略来缓解。不是所有差异都值得单独建类。
22. 策略命名要表达"算法差异",而不是"实现细节"。 比如 WechatPayStrategy、FullReductionDiscountStrategy,而不是 PaymentStrategyImpl1、DiscountHandlerA。
23. 文档中要写清楚每个策略的适用条件和约束。 客户端选择策略时需要知道各策略的差异:适用场景、性能特征、副作用、是否线程安全、失败时行为如何。
24. 每个策略都要独立测试,上下文也要测试委托行为。 策略类应单独覆盖正常路径、边界条件和异常路径。上下文则要测试"是否正确委托给当前策略"以及"切换后是否使用新策略"。
25. 异常处理策略要统一。 不同策略抛出的异常如果类型各异,调用方难以统一处理。可以在策略接口层约定异常转换规则,或者在上下文委托处统一包装。要避免某个策略静默吞掉异常。
26. 策略的失败回退要有明确设计。 某些策略失败后可以降级到默认策略,某些失败必须直接报错。回退逻辑应显式设计并测试,不能靠调用方临时 try-catch 再换一个策略。
策略模式不负责什么
把职责误加给上下文,是策略模式被用坏的最常见原因。明确边界,比记住定义更重要:
| 职责 | 谁负责 | 不该由谁负责 |
|---|---|---|
| 选择哪个策略 | 客户端、工厂、注册表、配置中心 | 上下文(上下文只持有并委托) |
| 策略实例的创建与销毁 | 工厂、DI 容器 | 上下文(除非它是工厂本身) |
| 中断正在执行的算法 | 业务层显式分段 | 策略模式(只换下一次调用) |
| 策略之间的通信与状态共享 | 额外设计(参数、结果对象、共享状态容器) | 策略接口(应保持最小契约) |
| 策略的兜底与降级 | 选择逻辑所在的层(客户端/注册表/配置) | 策略实现类(不应互相兜底) |
一句话:策略模式只做一件事——把"用什么算法"从"怎么调用算法"里分离出来。其余的事情,交给工厂、注册表、DI 容器、配置中心或业务层。
总结
- 统一入口不变,具体算法可换;换的是下一次怎么做,不能把正在做的事中途变掉。
- 接口定契约、具体类实现算法、上下文持有接口并委托执行、客户端负责选择与切换。
- 是否值得用,看两条:算法是否多样且会变;是否需要运行时选择。二者缺一,先写分支。
- 落地时记住三件事:选择逻辑外置、策略尽量无状态、明确策略模式不负责什么。



被折叠的 条评论
为什么被折叠?



