一:为什么需要策略模式
策略模式的核心价值是把 "做什么" 和 "怎么做" 分离开 ,解决代码中大量 if-else/switch 堆砌、逻辑耦合 的问题,让不同的实现方案(策略)可以灵活替换 ,且新增方案时不用修改原有代码(符合 "开闭原则")。
开闭原则(Open/Closed Principle,OCP)是面向对象设计的五大原则(SOLID) 中最核心的一条,它的核心定义是:软件实体(类、模块、函数等)应该对扩展开放(Open for extension) ,对修改关闭(Closed for modification)。
用一句大白话解释:当需要给程序新增功能时,尽量通过 "新增代码" 实现,而不是修改已有的、正在运行的代码。
就像你买了一辆汽车
- 对扩展开放:你可以给车加装座椅套、行车记录仪、后备箱收纳箱(新增功能,不用改汽车本身的核心部件);
- 对修改关闭:你不会去拆发动机、改变速箱来实现 "增加储物空间"(核心代码不能动,避免引入 bug)。 可以用一个非常贴近生活的例子:出行上班。
场景还原(无策略模式的糟糕情况)
假设你每天上班需要根据天气、时间选择出行方式,用代码模拟的话,没有策略模式时会写成这样:
java
public class GoToWork {
// 出行方法,塞满了if-else
public void travel(String way, double distance) {
if ("bus".equals(way)) {
System.out.println("坐公交上班," + distance + "公里,耗时约" + (distance * 5) + "分钟,花费2元");
} else if ("subway".equals(way)) {
System.out.println("坐地铁上班," + distance + "公里,耗时约" + (distance * 2) + "分钟,花费4元");
} else if ("bike".equals(way)) {
System.out.println("骑共享单车上班," + distance + "公里,耗时约" + (distance * 8) + "分钟,花费1.5元");
} else if ("car".equals(way)) {
System.out.println("开私家车上班," + distance + "公里,耗时约" + (distance * 3) + "分钟,花费油费" + (distance * 0.8) + "元");
} else {
System.out.println("未知出行方式");
}
}
public static void main(String[] args) {
GoToWork person = new GoToWork();
// 今天起晚了,选地铁
person.travel("subway", 10);
// 明天想省钱,选公交
person.travel("bus", 10);
}
}
问题暴露:
- 新增出行方式(比如 "打车")时,必须修改
travel方法,违反 "开闭原则"; - 所有出行逻辑都堆在一个方法里,代码臃肿、可读性差;
- 若某类出行方式的逻辑变了(比如公交涨价),需要修改核心方法,容易影响其他逻辑。
用策略模式重构
我们把 "出行" 这个核心行为(做什么),和 "公交 / 地铁 / 骑车" 这些具体实现(怎么做)分开:
步骤 1:定义 "出行策略" 接口(统一的行为规范)
java
// 出行策略接口(做什么:出行)
public interface TravelStrategy {
// 计算耗时和花费的方法(统一的行为)
void calculate(double distance);
}
步骤 2:实现不同的出行策略(怎么做:具体方式)
java
// 公交策略
public class BusStrategy implements TravelStrategy {
@Override
public void calculate(double distance) {
double time = distance * 5;
double cost = 2;
System.out.println("坐公交上班," + distance + "公里,耗时约" + time + "分钟,花费" + cost + "元");
}
}
// 地铁策略
public class SubwayStrategy implements TravelStrategy {
@Override
public void calculate(double distance) {
double time = distance * 2;
double cost = 4;
System.out.println("坐地铁上班," + distance + "公里,耗时约" + time + "分钟,花费" + cost + "元");
}
}
// 骑车策略
public class BikeStrategy implements TravelStrategy {
@Override
public void calculate(double distance) {
double time = distance * 8;
double cost = 1.5;
System.out.println("骑共享单车上班," + distance + "公里,耗时约" + time + "分钟,花费" + cost + "元");
}
}
步骤 3:定义 "上下文"(使用策略的主体)
java
// 上班的人(上下文,负责选择和使用策略)
public class Person {
// 持有一个策略对象(可以动态替换)
private TravelStrategy strategy;
// 动态设置策略
public void setStrategy(TravelStrategy strategy) {
this.strategy = strategy;
}
// 执行出行逻辑(只关心"出行",不关心"怎么出行")
public void goToWork(double distance) {
strategy.calculate(distance);
}
public static void main(String[] args) {
Person person = new Person();
// 周一起晚了,选地铁
person.setStrategy(new SubwayStrategy());
person.goToWork(10);
// 周二想省钱,选公交
person.setStrategy(new BusStrategy());
person.goToWork(10);
// 周三天气好,选骑车(新增策略后,原有代码一行不用改)
person.setStrategy(new BikeStrategy());
person.goToWork(5);
}
}
运行结果
java
坐地铁上班,10.0公里,耗时约20.0分钟,花费4.0元
坐公交上班,10.0公里,耗时约50.0分钟,花费2.0元
骑共享单车上班,5.0公里,耗时约40.0分钟,花费1.5元
二:什么是策略模式
1、先给一个通俗定义
策略模式(Strategy Pattern)是一种行为型设计模式 ,核心思想是:把完成某件事的 "多种不同方法 / 规则 / 算法"(即 "策略")封装成独立的类,让它们可以互相替换,且不影响使用这些策略的主逻辑。
简单说,就是把 "选择做什么" 和 "具体怎么做" 彻底分开 ------ 主逻辑只负责 "选策略、用策略",而 "怎么做" 的细节都封装在各自的策略类里。
2、策略模式的核心角色(规范且缺一不可)
策略模式有 3 个固定的核心角色,每个角色都有明确的职责,遵循 "单一职责原则"
| 角色名称 | 英文名称 | 核心职责 | |
|---|---|---|---|
| 1. 抽象策略(Strategy) | Strategy | 定义所有具体策略必须实现的统一接口 / 抽象类,规定策略的行为规范(方法) | |
| 2. 具体策略(Concrete Strategy) | ConcreteStrategy | 实现抽象策略接口,封装具体的算法 / 逻辑 / 规则(不同的 "怎么做") | |
| 3. 上下文(Context) | Context | 持有策略对象的引用,负责调用策略,可以理解为策略的"调度器" |
3.结合我们上文的乘车例子
1. 抽象策略(Strategy)
角色定义 :定义所有策略共同的行为接口,规定 "要做什么"。
在出行例子里:共同行为:出行(计算耗时、花费)
java
// 抽象策略:出行策略
public interface TravelStrategy {
void calculate(double distance);
}
2. 具体策略(ConcreteStrategy)
角色定义 :实现抽象策略接口,每一个类就是一种具体算法 / 方案。
对应:
BusStrategy公交方案SubwayStrategy地铁方案BikeStrategy骑车方案
java
// 具体策略1:公交
public class BusStrategy implements TravelStrategy {
@Override
public void calculate(double distance) {
System.out.println("公交:耗时≈" + distance*5 + "分钟,花费2元");
}
}
// 具体策略2:地铁
public class SubwayStrategy implements TravelStrategy {
@Override
public void calculate(double distance) {
System.out.println("地铁:耗时≈" + distance*2 + "分钟,花费4元");
}
}
// 具体策略3:骑车
public class BikeStrategy implements TravelStrategy {
@Override
public void calculate(double distance) {
System.out.println("骑车:耗时≈" + distance*8 + "分钟,花费1.5元");
}
}
3. 上下文(Context)
角色定义:
- 持有抽象策略的引用
- 提供设置策略的方法
- 提供执行策略的方法(给外部调用)
对应:人(Person) 去上班,人持有 "出行方式",并执行出行。
java
// 上下文:人
public class Person {
// 持有抽象策略(不持有具体类)
private TravelStrategy travelStrategy;
// 设置策略(动态切换)
public void setTravelStrategy(TravelStrategy travelStrategy) {
this.travelStrategy = travelStrategy;
}
// 执行策略(对外统一方法)
public void goToWork(double distance) {
if (travelStrategy == null) {
System.out.println("还没选出行方式");
return;
}
travelStrategy.calculate(distance);
}
}
4、客户端怎么用
java
public class Client {
public static void main(String[] args) {
//创建上下文,即策略调度器,将想要的策略告诉他,他会替我们执行策略
Person person = new Person();
// 选地铁
person.setTravelStrategy(new SubwayStrategy());
//执行上下文提供的执行策略的方法
person.goToWork(10);
// 切换成公交
person.setTravelStrategy(new BusStrategy());
person.goToWork(10);
// 切换成骑车
person.setTravelStrategy(new BikeStrategy());
person.goToWork(5);
}
}
4.再来具体讲解一下上下文
一、策略模式中 "上下文(Context)" 的核心定义
上下文是策略模式的核心调度者 ,它不负责实现具体的策略逻辑 ,而是承担 "连接客户端与策略" 的桥梁作用。简单说:
上下文 = 策略的 "持有者 + 调度者 + 封装者"(结合出行例子:
Person类就是上下文,人不关心公交 / 地铁怎么算耗时,只负责选策略、用策略)
二、上下文的核心职责(结合出行例子)
1. 持有抽象策略的引用(核心)
-
上下文只能持有抽象策略接口 / 抽象类 的引用(如
TravelStrategy),而非具体策略(如BusStrategy),这是依赖倒置原则的体现,避免耦合具体实现。 -
代码示例(出行例子):
javapublic class Person { // 只持有抽象策略,不耦合具体策略 private TravelStrategy travelStrategy; // ... }
2. 提供策略的 "设置 / 切换" 方法
-
允许客户端在运行时动态更换策略(比如上班途中临时换出行方式),这是策略模式 "灵活替换" 的关键。
-
代码示例:
javapublic class Person { // 动态设置/切换策略 public void setTravelStrategy(TravelStrategy travelStrategy) { this.travelStrategy = travelStrategy; } // ... }
3. 封装策略的执行逻辑(对外提供统一接口)
-
上下文会封装策略的调用过程 ,还可在执行策略前后添加公共逻辑(如参数校验、日志、异常处理),让具体策略只关注核心逻辑。
-
代码示例(优化后的
Person类,增加公共逻辑):javapublic class Person { private TravelStrategy travelStrategy; public void setTravelStrategy(TravelStrategy travelStrategy) { this.travelStrategy = travelStrategy; } // 封装执行逻辑,添加公共处理 public void goToWork(double distance) { // 公共逻辑1:参数校验 if (distance <= 0) { System.out.println("错误:距离必须大于0!"); return; } // 公共逻辑2:前置日志 System.out.println("开始规划上班路线,距离:" + distance + "公里"); // 核心:调用策略逻辑 if (travelStrategy != null) { travelStrategy.calculate(distance); } else { System.out.println("未选择出行方式!"); } // 公共逻辑3:后置处理 System.out.println("路线规划完成\n"); } }
4.封装策略的 "选择逻辑"(引入策略工厂)
java
// 1. 抽象策略
public interface TravelStrategy {
void calculate(double distance);
}
// 2. 具体策略(公交)
public class BusStrategy implements TravelStrategy {
@Override
public void calculate(double distance) {
System.out.println("公交:耗时≈" + distance*5 + "分钟,花费2元");
}
}
// 3. 策略工厂
public class TravelStrategyFactory {
public static TravelStrategy getStrategy(String scene) {
return switch (scene) {
case "赶时间" -> new SubwayStrategy();
case "省钱" -> new BusStrategy();
case "健身" -> new BikeStrategy();
default -> throw new IllegalArgumentException("无此场景策略");
};
}
}
// 4. 上下文(核心)
public class Person {
private TravelStrategy travelStrategy;
// 结合工厂选择策略
public void chooseStrategyByScene(String scene) {
this.travelStrategy = TravelStrategyFactory.getStrategy(scene);
}
// 封装执行逻辑(含公共处理)
public void goToWork(double distance) {
if (distance <= 0) {
System.out.println("错误:距离必须大于0!");
return;
}
System.out.println("开始规划上班路线,距离:" + distance + "公里");
if (travelStrategy != null) {
travelStrategy.calculate(distance);
} else {
System.out.println("未选择出行方式!");
}
System.out.println("路线规划完成\n");
}
}
// 5. 客户端
public class Client {
public static void main(String[] args) {
Person person = new Person();
// 选"赶时间"场景,上下文自动选地铁策略
person.chooseStrategyByScene("赶时间");
person.goToWork(10);
// 切换为"省钱"场景,自动选公交策略
person.chooseStrategyByScene("省钱");
person.goToWork(10);
}
}
这里我们是上下文结合策略工厂的实现类型,我们的客户端不直接new一个策略对象,交给上下文,而是客户端将想调用的策略告诉上下文,上下文內部调用策略工厂生产策略对象,客户端通过上下文提供的goToWork方法执行策略
为什么要引入策略工厂
策略工厂不是策略模式的 "必须项",但却是真实项目中优化策略模式的 "刚需项" ------ 它解决了策略模式直接使用时的两个核心痛点,结合出行例子讲最直观:
痛点 1:客户端直接 new 具体策略,耦合度高
没有工厂时,客户端代码是这样的:
java
// 客户端
public class Client {
public static void main(String[] args) {
Person person = new Person();
// 客户端必须知道所有具体策略类的名字,耦合严重
person.setTravelStrategy(new SubwayStrategy());
person.setTravelStrategy(new BusStrategy());
person.setTravelStrategy(new BikeStrategy());
}
}
问题:
- 客户端要硬编码所有具体策略类(如
SubwayStrategy),一旦策略类名修改、或新增 / 删除策略,所有客户端代码都要改; - 若策略创建逻辑复杂(比如初始化时需要传参数、读配置),这些逻辑会散落在所有客户端里,难以维护。
痛点 2:策略选择逻辑散落在客户端,代码冗余
如果需要根据 "场景" 选策略(比如 "赶时间" 选地铁、"省钱" 选公交),没有工厂时,选择逻辑会写在客户端:
java
// 客户端堆满策略选择的if-else,冗余且难维护
public class Client {
public static void main(String[] args) {
Person person = new Person();
String scene = "赶时间";
if ("赶时间".equals(scene)) {
person.setTravelStrategy(new SubwayStrategy());
} else if ("省钱".equals(scene)) {
person.setTravelStrategy(new BusStrategy());
} else if ("健身".equals(scene)) {
person.setTravelStrategy(new BikeStrategy());
}
}
}
问题:
- 若有 10 个客户端(比如手机端、PC 端、小程序端),这堆
if-else要复制 10 遍,改一处要改 10 处; - 策略选择逻辑和客户端业务逻辑混在一起,违反 "单一职责原则"。
策略工厂的本质是把 "策略的创建 + 选择逻辑" 集中封装,让客户端只关注 "用什么策略",不用关注 "怎么创建 / 怎么选"。
价值 1:解耦客户端与具体策略类
客户端只需传 "标识 / 场景"(如 "赶时间"),不用知道具体策略类名,即使策略类改名 / 新增,客户端代码完全不用改:
java
// 有工厂后的客户端(极简)
public class Client {
public static void main(String[] args) {
Person person = new Person();
// 客户端只传场景,不用知道SubwayStrategy的存在
TravelStrategy strategy = TravelStrategyFactory.getStrategy("赶时间");
person.setTravelStrategy(strategy);
person.goToWork(10);
}
}
价值 2:集中管理策略选择逻辑,避免冗余
所有 if-else/switch 都集中在工厂里,改策略选择规则只需改工厂,所有客户端自动生效:
java
// 策略工厂(集中管理创建+选择)
public class TravelStrategyFactory {
// 所有选择逻辑都在这,改一处全生效
public static TravelStrategy getStrategy(String scene) {
return switch (scene) {
case "赶时间" -> new SubwayStrategy();
case "省钱" -> new BusStrategy();
case "健身" -> new BikeStrategy();
// 新增策略只需加一行,客户端无感知
case "下雨" -> new TaxiStrategy();
default -> throw new IllegalArgumentException("无此场景策略:" + scene);
};
}
}
价值 3:封装复杂的策略创建逻辑
如果策略创建需要初始化参数、读配置、甚至依赖其他对象,工厂可以封装这些细节,客户端无需关心:
java
// 工厂封装复杂创建逻辑(比如公交策略需要读票价配置)
public class TravelStrategyFactory {
public static TravelStrategy getStrategy(String scene) {
if ("省钱".equals(scene)) {
// 从配置读取公交票价,客户端完全不用知道
double busFare = ConfigReader.getBusFare();
return new BusStrategy(busFare); // 带参数的策略构造
}
// 其他策略...
}
}
价值 4:方便扩展与维护
- 新增策略:只需加一个具体策略类 + 工厂里加一行 case,无其他改动;
- 删除策略:只需在工厂里注释掉对应 case,客户端传该场景会抛异常,便于排查;
- 改策略逻辑:只需改具体策略类,工厂和客户端都不用动。
三:策略模式的其他用法
一、策略模式的进阶优化
1. 策略工厂:解决客户端频繁 new 具体策略的问题
客户端直接 new SubwayStrategy() 会耦合具体策略类,可通过简单工厂封装策略选择逻辑:
java
// 出行策略工厂(统一创建策略对象)
public class TravelStrategyFactory {
// 根据出行方式名称获取对应策略
public static TravelStrategy getStrategy(String way) {
return switch (way) {
case "bus" -> new BusStrategy();
case "subway" -> new SubwayStrategy();
case "bike" -> new BikeStrategy();
default -> throw new IllegalArgumentException("无此出行方式:" + way);
};
}
}
// 客户端优化后用法
public class Client {
public static void main(String[] args) {
Person person = new Person();
// 只需传名称,无需直接new具体策略
person.setTravelStrategy(TravelStrategyFactory.getStrategy("subway"));
person.goToWork(10);
person.setTravelStrategy(TravelStrategyFactory.getStrategy("bus"));
person.goToWork(10);
}
}
优势:策略创建逻辑集中管理,客户端只需关注 "选什么",不用关注 "怎么创建"。
这一点类似于我们的封装策略的 "选择逻辑"(引入策略工厂) 章节的讲解,但是这里我们是客户端手动创建策略工厂,将生产的策略对象交给上下文
2. 策略模式 + 枚举:简化策略标识与创建
用枚举替代字符串标识出行方式,避免拼写错误:
java
// 出行方式枚举类,提前定义所有的可用策略
public enum TravelWay {
BUS, SUBWAY, BIKE;
// 枚举关联对应策略
public TravelStrategy getStrategy() {
return switch (this) {
case BUS -> new BusStrategy();
case SUBWAY -> new SubwayStrategy();
case BIKE -> new BikeStrategy();
};
}
}
// 客户端用法
public class Client {
public static void main(String[] args) {
Person person = new Person();
// 用枚举选策略,无拼写错误风险
person.setTravelStrategy(TravelWay.SUBWAY.getStrategy());
person.goToWork(10);
}
}
这里引入枚举类,好处是我们提前将所有策略都定义在一个枚举类,用户可以直接从枚举类中选择策略,而不需要手动传入,避免用户输入出现语法错误,比如策略"Train",用户输入成"Tain",导致策略工厂无法根据策略生成对应的策略类,同时枚举类还能起到统一管理各种变量的作用,具体有以下原因
- 编译期校验,杜绝非法值(最核心)
枚举的取值是固定的(比如 HURRY_UP/SAVE_MONEY/KEEP_FIT),客户端只能选预定义的值,拼错 / 传错会直接编译报错,根本到不了运行时:
java
// 枚举定义(限定所有合法场景)
public enum TravelScene {
HURRY_UP, // 赶时间
SAVE_MONEY, // 省钱
KEEP_FIT // 健身
}
// 工厂改造(参数为枚举,而非字符串)
public class TravelStrategyFactory {
public static TravelStrategy getStrategy(TravelScene scene) {
return switch (scene) {
case HURRY_UP -> new SubwayStrategy();
case SAVE_MONEY -> new BusStrategy();
case KEEP_FIT -> new BikeStrategy();
};
}
}
// 客户端使用:
// ✅ 正确写法:编译通过
TravelStrategy s1 = TravelStrategyFactory.getStrategy(TravelScene.HURRY_UP);
// ❌ 错误写法:编译直接报错(找不到HURRYUP这个枚举值)
TravelStrategy s2 = TravelStrategyFactory.getStrategy(TravelScene.HURRYUP);
效果:把 "运行时才发现的错误" 提前到 "编译期拦截",大幅降低线上 bug。
- 代码可读性提升,语义更清晰
枚举值是 "见名知意" 的常量(比如 HURRY_UP 比字符串 "赶时间" 更符合编程规范),且 IDE 会自动提示所有可选值,新人不用翻文档 / 源码:
java
// 用枚举:一眼看懂是"赶时间"场景
person.chooseStrategy(TravelScene.HURRY_UP);
// 用字符串:要确认"赶时间"是不是工厂支持的写法
person.chooseStrategy("赶时间");
- 集中管理策略标识,避免取值混乱
所有策略标识都集中在枚举类里,新增 / 修改场景只需改枚举,所有客户端自动同步:
java
// 新增"下雨"场景:只需在枚举里加一行
public enum TravelScene {
HURRY_UP, // 赶时间
SAVE_MONEY, // 省钱
KEEP_FIT, // 健身
RAINY_DAY // 下雨(新增)
}
四、策略模式的适用场景
一、核心使用场景
特征 1:同一行为有多种不同的实现方案,且需动态切换
-
核心判断:"做什么" 固定,但 "怎么做" 有多种选择,且运行时可能根据条件换方案。
-
典型例子:
- 出行场景(核心案例):上班的 "出行行为" 固定,但公交 / 地铁 / 骑车 / 打车是不同实现,可根据天气、时间动态切换;
- 支付场景:电商下单的 "支付行为" 固定,微信 / 支付宝 / 银行卡 / 花呗是不同实现,用户可自选;
- 排序场景:"排序行为" 固定,冒泡 / 快排 / 归并 / 希尔排序是不同实现,可根据数据量大小动态选(小数据用冒泡,大数据用快排);
- 日志输出场景:"日志记录行为" 固定,控制台 / 文件 / 数据库 / ELK 是不同实现,可根据环境(开发 / 生产)切换。
特征 2:代码中出现大量 if-else/switch 选择不同逻辑
-
核心判断:一个方法里堆满
if (type == A) { ... } else if (type == B) { ... },且每个分支逻辑独立、可拆分。 -
反例(需要重构)
java// 支付逻辑堆满if-else,符合策略模式使用场景 public void pay(String type, double amount) { if ("wechat".equals(type)) { // 微信支付逻辑 } else if ("alipay".equals(type)) { // 支付宝支付逻辑 } else if ("card".equals(type)) { // 银行卡支付逻辑 } } -
重构后:把每种支付逻辑拆成
WechatPayStrategy/AlipayPayStrategy,用策略模式消除 if-else。
特征 3:新增 / 修改实现方案时,不想改动原有核心代码(符合开闭原则)
- 核心判断:新增功能(比如新增 "打车" 出行方式、"花呗" 支付方式)时,希望只加新类,不修改原有逻辑。
- 典型例子:电商促销系统:"计算优惠" 行为固定,满减 / 折扣 / 满赠 / 无优惠是不同实现,双十一新增 "跨店满减" 策略时,只需加
CrossShopDiscountStrategy,原有代码一行不改。
特征 4:不同实现方案的核心逻辑独立,且有统一的行为规范
- 核心判断:所有实现方案都能抽象出一个统一的接口(比如
TravelStrategy的calculate()、PaymentStrategy的pay())。 - 反例(不适合):如果不同实现的方法签名、返回值都不一样(比如 A 方案返回布尔值,B 方案返回字符串),无法抽象统一接口,就不适合用策略模式。