GoF设计模式——策略模式
本文是【GoF设计模式】系列第14篇,更多内容欢迎关注公众号:咖啡八杯

前言
为什么需要策略模式?
电商网站结算时常常要算优惠:新用户满减、不同等级会员打折、节日活动满减。最直觉的写法是把所有规则塞进一个方法里用 if-else 区分:
class Cashier {
public int calc(int total, String type) {
if ("打九折".equals(type)) {
return (int) (total * 0.9);
} else if ("满300减40".equals(type)) {
return total >= 300 ? total - 40 : total;
} else {
return total;
}
}
}
这种写法很快就会失控:每加一种优惠就要往 if-else 里塞分支,Cashier 被迫认识所有规则;规则一改,核心代码跟着动。分支越堆越多,最后谁也不敢碰这块代码。
策略模式解决的就是这个"同一件事有多种做法,做法还会不断增加"的问题。把每种做法封装成独立的策略类,Cashier 只管持有策略、在结算时调用,需要哪种优惠就注入哪种,互不干扰。
概念
策略模式(Strategy Pattern)是一种行为型设计模式,核心思想是定义一系列算法,将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户端。
这些算法完成的是相同的工作,只是实现不同。客户端在运行时选择不同的具体策略,而不必修改自身代码——新增算法只需添加新策略类,符合开闭原则。
策略模式包含三个角色:
- Strategy(策略接口):定义所有支持的算法的公共接口
- ConcreteStrategy(具体策略):实现策略接口,提供具体的算法实现
- Context(上下文):持有策略引用,负责业务数据管理和流程编排,在适当时机调用策略方法
![image]()
图中各类之间的关系:ConcreteStrategyA、ConcreteStrategyB 实现 Strategy 接口,Context 持有一个 Strategy 引用,客户端面向 Context 编程——客户端和具体策略之间没有直接依赖,新增策略时 Context 无需改动。
可以把策略模式想象成餐厅点菜:顾客(客户端)跟服务员(Context)说"来一份辣的菜",服务员不自己下厨,而是把需求转交给后厨对应的厨师(具体策略)。厨师换一道菜的做法,服务员和顾客完全不用变;后厨新招一个川菜师傅(新增策略),服务员只要会喊单就行。
实现
标准实现
GoF 的标准实现中,Context 不是简单的转发层,它承担着业务数据管理和流程编排的职责。策略接口只定义纯算法契约,Context 负责将业务数据"翻译"成算法需要的参数。
// 策略接口:定义算法的公共契约
interface Strategy {
public void algorithm();
}
// 具体策略 A
class ConcreteStrategyA implements Strategy {
public void algorithm() {
System.out.println("策略 A 的算法实现");
}
}
// 具体策略 B
class ConcreteStrategyB implements Strategy {
public void algorithm() {
System.out.println("策略 B 的算法实现");
}
}
// 上下文:持有策略引用,适当时机调用策略方法
class Context {
private Strategy strategy;
public Context(Strategy strategy) {
this.strategy = strategy;
}
public void setStrategy(Strategy strategy) {
this.strategy = strategy;
}
public void executeStrategy() {
// Context 可以在调用策略前后做预处理/后处理
strategy.algorithm();
}
}
// 客户端代码
Context context = new Context(new ConcreteStrategyA());
context.executeStrategy(); // 使用策略 A
context.setStrategy(new ConcreteStrategyB());
context.executeStrategy(); // 切换为策略 B
实现思想:Context 持有策略引用,业务流程由 Context 编排,具体算法委托给策略执行,运行时通过 setStrategy 动态切换。
引入一个具体场景:商场收银支持无优惠、打九折、满减三种结算方式。用策略模式实现,Context 负责管理购物车和计算总价,策略只负责根据总价算优惠。
// 策略接口:根据总价计算应付金额
interface Strategy {
public int algorithm(int price);
}
// 具体策略:无优惠
class NormalStrategy implements Strategy {
public int algorithm(int price) {
return price;
}
}
// 具体策略:打九折
class DiscountStrategy implements Strategy {
public int algorithm(int price) {
return (int) (price * 0.9);
}
}
// 具体策略:满减(满100减10,满200减25,满300减40,取最大满足档位)
class FullReductionStrategy implements Strategy {
public int algorithm(int price) {
if (price >= 300) return price - 40;
if (price >= 200) return price - 25;
if (price >= 100) return price - 10;
return price;
}
}
// Context:管理购物车(数据总线)+ 计算总价(流程编排)+ 应用策略
class CashierContext {
private Strategy strategy;
private List<Integer> prices = new ArrayList<>();
public void setStrategy(Strategy strategy) {
this.strategy = strategy;
}
public void addItem(int price) {
prices.add(price);
}
public void checkout() {
// 1. Context 负责计算总金额(数据准备)
int sum = 0;
for (int price : prices) {
sum += price;
}
// 2. 委托策略做优惠计算(算法)
int result = strategy.algorithm(sum);
System.out.println(result);
// 3. 清空购物车,为下次收银做准备
prices.clear();
}
}
// 客户端代码
CashierContext cashier = new CashierContext();
// 使用九折策略
cashier.setStrategy(new DiscountStrategy());
cashier.addItem(100);
cashier.addItem(50);
cashier.addItem(50);
cashier.checkout(); // 200 × 0.9 = 180
// 切换为满减策略
cashier.setStrategy(new FullReductionStrategy());
cashier.addItem(100);
cashier.addItem(80);
cashier.addItem(70);
cashier.addItem(50);
cashier.checkout(); // 300 - 40 = 260
关键点:CashierContext 承担了购物车管理和总价计算的职责,策略只负责根据总价计算优惠。Context 复用同一个实例,通过 setStrategy 切换策略——这正是策略模式"运行时动态切换算法"的体现。
Context 的核心职责
Context 在 GoF 中绝不是无意义的包装层,它在策略模式中承担三个核心职责:
1. 数据总线:Context 持有业务数据,策略不直接访问这些数据。策略接口只接收 Context 准备好的参数,不需要知道数据从哪来、怎么组织的。
2. 流程编排:Context 定义了"先做什么、再做什么、最后调用策略"的完整流程。策略只负责算法步骤本身。
3. 状态复用:同一个 Context 可以在不同时刻应用不同的策略,Context 中积累的业务状态不需要重新构建。
对比直接调用策略和通过 Context 调用的区别:直接调用时,客户端要自己准备算法需要的参数;通过 Context 调用时,客户端只管触发动作,Context 把数据准备好再传给策略。
// ❌ 直接调用策略:客户端承担了所有准备工作
int total = 0;
for (int price : items) {
total += price;
}
int result = strategy.algorithm(total); // 客户端要自己算总金额
// ✅ 通过 Context 调用:Context 封装了业务流程
cashier.addItem(100);
cashier.addItem(200);
cashier.checkout(); // Context 内部完成总价计算 + 策略调用
理解了 Context 的职责,就能避免把策略模式写成"策略 + 空壳 Context"的反模式——那种 Context 只是 strategy.algorithm() 的一层转发,没有承担任何业务逻辑,违背了 GoF 的设计意图。
用 Lambda 简化策略定义
前面具体策略类各自只有一两行逻辑,却要单独定义一个类,略显啰嗦。由于策略接口只有一个抽象方法,本质上是函数式接口(Functional Interface),Java 8+ 可以直接用 Lambda 代替具体策略类,省去独立的类定义。
// 策略接口是函数式接口,可直接用 Lambda 表达策略,无需再写 NormalStrategy 等类
Strategy normal = price -> price;
Strategy discount = price -> (int) (price * 0.9);
Strategy fullReduction = price -> {
if (price >= 300) return price - 40;
if (price >= 200) return price - 25;
if (price >= 100) return price - 10;
return price;
};
CashierContext c = new CashierContext();
c.setStrategy(discount);
c.addItem(200);
c.checkout(); // 180
实现思想:策略接口只要保持单方法契约,具体策略就能用 Lambda 内联表达,Context 的编排逻辑完全不变。更进一步,甚至可以用内建的 IntUnaryOperator、Function<Integer,Integer> 代替自定义接口,但保留一个有领域含义的 Strategy 接口名,可读性通常更好。
⚠️ Lambda 不是万能替换:当策略逻辑复杂、需要持有自身状态(如满减档位参数)、或要在多处复用、需要被工厂统一管理时,仍应使用独立策略类——Lambda 适合"一行就能说清、用完即弃"的简单算法。
总结
策略模式的本质是把可变的算法封装成独立对象,通过组合而非继承实现行为切换——客户端只面向策略接口,运行时动态替换算法。
什么时候用:
- 系统需要在运行时根据条件动态选择算法
- 代码中存在大量
if-else或switch,且每个分支只是行为不同 - 算法会频繁新增或修改,需要独立于客户端扩展
- 想用组合替代继承来实现行为变化,避免类爆炸
什么时候不用:
- 算法只有两三种且永远不会变化,直接
if-else更简单 - 策略之间需要共享中间状态,策略模式的独立类做不到
- 客户端完全不需要了解策略差异(此时策略选择逻辑反而成了负担)
简单记忆:
策略解决"同一件事多种做法"的问题,把 if-else 拆成可互换的算法类,运行时随便换。
⚠️ 用策略模式要注意:每个策略一个类,算法多时类文件会膨胀;客户端必须知道有哪些策略可用才能选择,策略选择逻辑仍需在某处用 if-else 或工厂集中处理。
相似模式区分
策略模式容易和模板方法、状态、工厂方法混淆,它们都涉及"封装可变行为",但实现方式和意图不同。
总览对比:
| 模式 | 接口关系 | 核心意图 | 典型场景 |
|---|---|---|---|
| 策略 | Context 持有策略接口引用 | 封装可互换的算法,运行时切换 | 折扣算法、支付方式、排序 |
| 模板方法 | 子类继承父类,重写步骤 | 定义算法骨架,子类填充步骤 | 框架的流程骨架、生命周期钩子 |
| 状态 | Context 持有状态接口引用 | 根据内部状态改变行为 | 订单状态流转、审批流程 |
| 工厂方法 | 工厂返回产品对象 | 封装对象创建过程 | 根据配置创建数据源、解析器 |
口诀:策略换算法,模板填步骤,状态自身转,工厂管造谁。
策略 vs 模板方法
两者都用于封装可变的行为,但实现方式完全不同。策略模式通过组合实现——Context 持有策略接口的引用,算法在运行时通过注入切换;模板方法通过继承实现——父类定义算法骨架,子类重写具体步骤,行为在编译时就确定了。
| 维度 | 策略模式 | 模板方法 |
|---|---|---|
| 核心意图 | 封装可互换的算法族 | 定义算法骨架,子类填充步骤 |
| 结构差异 | Context 持有接口引用,组合关系 | 父类定义骨架,子类继承重写 |
| 关注点 | 整个算法的替换 | 算法中个别步骤的定制 |
| 灵活性 | 运行时动态切换 | 编译时确定 |
| 典型场景 | 支付方式、折扣算法、排序策略 | 框架生命周期、流程骨架 |
逐步区分法:
- 算法整体需要在运行时切换 → 选策略模式
- 算法骨架固定、只是个别步骤需要子类定制 → 选模板方法
策略 vs 状态
两者结构几乎一样(都有 Context + 接口 + 多个实现类),但意图完全不同。策略模式中,策略由外部注入,策略之间不知道彼此的存在;状态模式中,状态由对象自身管理,状态之间可以触发转换。
| 维度 | 策略模式 | 状态模式 |
|---|---|---|
| 核心意图 | 封装可互换的算法 | 根据内部状态改变行为 |
| 结构差异 | 策略由 Context 持有,外部注入 | 状态由对象自身持有,状态间可转换 |
| 关注点 | 算法的选择与替换 | 状态流转驱动行为变化 |
| 典型场景 | 折扣算法、支付方式 | 订单状态流转、审批流程 |
逐步区分法:
- 行为由调用方决定,主动选哪个算法 → 选策略模式
- 行为由对象自身状态决定,状态会自动流转 → 选状态模式
策略 vs 工厂方法
两者都会出现 if-else 或 switch 来选择不同的实现类,但目的完全不同。工厂方法解决的是 "创建什么对象" ——根据条件决定实例化哪个产品类;策略模式解决的是 "用什么算法" ——将算法封装成独立对象,运行时可替换。
| 维度 | 策略模式 | 工厂方法 |
|---|---|---|
| 核心意图 | 封装可互换的算法 | 封装对象的创建过程 |
| 结构差异 | Context 持有策略引用并调用算法 | 工厂返回产品对象,由客户端使用 |
| 关注点 | 行为(怎么做) | 创建(造哪个) |
| 运行时切换 | 同一个 Context 可随时切换策略 | 工厂通常只创建一次对象 |
| 典型场景 | 折扣算法、排序策略、支付方式 | 根据配置创建数据源、根据类型创建解析器 |
逐步区分法:
if-else的结果是用来执行不同行为(算法、规则、处理方式) → 选策略模式if-else的结果是用来创建不同对象(产品、组件、服务) → 选工厂方法
简单记忆:工厂管"造谁",策略管"怎么做"。如果只是创建对象时需要分支,用工厂方法就够了;如果创建出来的对象还需要在运行时动态切换行为,用策略模式。
练习题目
商场收银系统
题目描述:某商场的收银系统支持多种优惠策略。收银时,收银员先选择优惠策略,再逐个扫描商品价格,系统自动计算总价并应用优惠。优惠策略如下:九折优惠策略(总价的 90%,向下取整)、满减优惠策略(满 100 减 10,满 200 减 25,满 300 减 40,取最大满足的档位)、无优惠策略(原价)。请使用策略模式实现收银系统,Context 类负责管理购物车商品和计算总价,优惠策略只负责根据总价计算优惠。
输入描述:第一行输入整数 N(1 ≤ N ≤ 20),表示收银次数。每次收银:第一行输入两个整数 k 和 t,k(1 ≤ k ≤ 20)表示商品数量,t(0/1/2)表示优惠策略编号,0 为无优惠,1 为九折,2 为满减。接下来一行输入 k 个整数表示商品价格(0 < 价格 < 400)。
输出描述:每次收银输出一行,表示优惠后的应付金额。
输入示例:
3
3 1
100 50 50
4 2
100 80 70 50
2 0
60 40
输出示例:
180
260
100
解题思路:本题中 CashierContext(收银台)承担了购物车管理和总价计算的职责,策略只负责根据总价计算优惠——去掉策略模式,三种优惠就只能堆在 checkout 里用 if-else,新增优惠就要改核心代码。Context 复用同一个实例,通过 setStrategy 切换策略,每次收银完成后清空购物车。策略选择用 if-else 集中处理(生产中可换成工厂或注册表),但真正的算法实现被封装在各自的策略类中,互不影响。
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
CashierContext c = new CashierContext();
while (n-- > 0) {
int k = sc.nextInt();
int t = sc.nextInt();
Strategy s = null;
if (t == 1) s = new DiscountStrategy(); // 九折
else if (t == 2) s = new FullReductionStrategy(); // 满减
else s = new NormalStrategy(); // 无优惠
c.setStrategy(s);
while (k-- > 0) {
int price = sc.nextInt();
c.addItem(price);
}
c.checkout(); // 计算总价 + 应用优惠 + 输出 + 清空购物车
}
}
}
interface Strategy {
public int algorithm(int price);
}
// 无优惠策略
class NormalStrategy implements Strategy {
public int algorithm(int price) {
return price;
}
}
// 九折策略
class DiscountStrategy implements Strategy {
public int algorithm(int price) {
return (int) (price * 0.9);
}
}
// 满减策略:取最大满足的档位
class FullReductionStrategy implements Strategy {
public int algorithm(int price) {
if (price >= 300) return price - 40;
if (price >= 200) return price - 25;
if (price >= 100) return price - 10;
return price;
}
}
// Context:数据总线 + 流程编排
class CashierContext {
private Strategy strategy;
private List<Integer> prices = new ArrayList<>();
public void setStrategy(Strategy strategy) {
this.strategy = strategy;
}
public void addItem(int price) {
prices.add(price);
}
public void checkout() {
// Context 负责计算总金额
int sum = 0;
for (int price : prices) {
sum += price;
}
// 策略只负责优惠计算
int res = strategy.algorithm(sum);
System.out.println(res);
// 清空购物车,为下次收银做准备
prices.clear();
}
}
验证示例:
- 第一次:商品 100+50+50=200,策略 1(九折)→ 200×0.9=180
- 第二次:商品 100+80+70+50=300,策略 2(满减)→ 300−40=260
- 第三次:商品 60+40=100,策略 0(无优惠)→ 100
三行结果与输出示例一致。
扩展:实际项目中的策略模式
JDK 中的 Comparator 排序
java.util.Comparator 是 Java 标准库中最经典的策略模式应用。Collections.sort() 或 Arrays.sort() 是 Context,Comparator 是策略接口,不同的比较逻辑是具体策略。
// 策略接口:Comparator<T>;Context:products.sort(comparator)
Comparator<Product> byPrice = Comparator.comparingInt(p -> p.price); // 按价格升序
Comparator<Product> byName = Comparator.comparing(p -> p.name); // 按名称
Comparator<Product> bySales = Comparator.comparingInt(p -> p.sales).reversed(); // 按销量降序
Comparator<Product> combined = bySales.thenComparing(byPrice); // 组合策略
List<Product> products = Arrays.asList(
new Product("手机", 2999, 500),
new Product("耳机", 199, 1200),
new Product("平板", 3999, 300)
);
products.sort(byPrice); // 运行时切换排序策略
products.sort(combined); // 再换一种
关键点:sort() 作为 Context,封装了排序的完整流程(边界检查、数组分割、递归调用),策略只需要实现"两个元素谁大谁小"。thenComparing 还能把多个策略组合起来,体现了策略的可组合性。
Spring 的 Resource 接口
Spring 框架用策略模式统一了不同来源的资源访问。Resource 是策略接口,ResourceLoader 是 Context。
// 策略接口:Resource;不同来源就是不同具体策略
Resource classpathRes = new ClassPathResource("config.yml");
Resource fileRes = new FileSystemResource("/etc/app/config.yml");
Resource urlRes = new UrlResource("https://example.com/config.yml");
// 统一读取方式,不关心资源来自哪里
InputStream is = classpathRes.getInputStream();
// ResourceLoader 是 Context,根据路径前缀自动选择策略
ResourceLoader loader = new DefaultResourceLoader();
Resource res = loader.getResource("classpath:config.yml"); // → ClassPathResource
Resource res2 = loader.getResource("file:/etc/config.yml"); // → FileSystemResource
关键点:DefaultResourceLoader.getResource() 作为 Context,内部根据路径前缀(classpath:、file:、https:)自动选择合适的 Resource 策略,业务代码完全不需要关心资源来源。
电商促销系统
促销系统是策略模式的典型应用:满减、折扣、买赠等规则各自独立,通过策略模式组合使用。
interface PromotionStrategy {
// subtotal 商品原价总计,items 商品列表(部分策略按商品维度计算)
int apply(int subtotal, List<PromotionItem> items);
}
// 满减策略
class FullReductionPromotion implements PromotionStrategy {
private int threshold;
private int reduction;
public FullReductionPromotion(int threshold, int reduction) {
this.threshold = threshold;
this.reduction = reduction;
}
public int apply(int subtotal, List<PromotionItem> items) {
return subtotal >= threshold ? subtotal - reduction : subtotal;
}
}
// 折扣策略
class DiscountPromotion implements PromotionStrategy {
private double rate;
public DiscountPromotion(double rate) { this.rate = rate; }
public int apply(int subtotal, List<PromotionItem> items) {
return (int) (subtotal * rate);
}
}
// Context:封装订单业务逻辑
class PromotionContext {
private PromotionStrategy strategy;
public void setStrategy(PromotionStrategy strategy) {
this.strategy = strategy;
}
// Context 负责:计算小计;策略只负责:根据规则计算优惠
public int calculateFinalPrice(List<PromotionItem> items) {
int subtotal = 0;
for (PromotionItem item : items) {
subtotal += item.price * item.quantity;
}
return strategy.apply(subtotal, items);
}
}
关键点:PromotionContext 承担了计算小计、遍历商品等业务逻辑,策略接口只关心"给定总金额怎么优惠"。新增促销规则只需添加新的策略类,符合开闭原则。
支付方式选择
支付系统中,不同支付渠道(微信、支付宝、银行卡)的扣款逻辑不同,但支付流程(创建订单 → 调用支付 → 处理结果)是固定的。
interface PaymentStrategy {
boolean pay(int amountCents);
String getChannelName();
}
class WechatPay implements PaymentStrategy {
public boolean pay(int amountCents) {
System.out.println("微信支付: " + amountCents + " 分");
return true;
}
public String getChannelName() { return "WECHAT"; }
}
class Alipay implements PaymentStrategy {
public boolean pay(int amountCents) {
System.out.println("支付宝支付: " + amountCents + " 分");
return true;
}
public String getChannelName() { return "ALIPAY"; }
}
// Context:封装完整的支付流程
class PaymentContext {
private PaymentStrategy strategy;
public PaymentContext(PaymentStrategy strategy) {
this.strategy = strategy;
}
// Context 负责创建支付单、日志、状态更新;策略只负责扣款
public boolean processPayment(String orderId, int amountCents) {
System.out.println("创建支付单: " + orderId + ", 渠道: " + strategy.getChannelName());
boolean success = strategy.pay(amountCents);
if (success) {
System.out.println("支付成功,更新订单状态");
} else {
System.out.println("支付失败,记录异常");
}
return success;
}
}
PaymentContext ctx = new PaymentContext(new WechatPay());
ctx.processPayment("ORD_001", 299900);
关键点:PaymentContext 封装了支付流程中的通用逻辑(创建支付单、日志、状态更新),策略只需要实现与支付渠道的对接。如果不用策略模式,这些流程逻辑会在每个 if-else 分支中重复。
文件导出功能
导出功能常需要支持多种格式(CSV、JSON、Excel),每种格式的写入逻辑完全不同,但数据准备和格式化是通用的。
interface ExportStrategy {
void export(List<Map<String, Object>> data, OutputStream out) throws IOException;
}
class CsvExportStrategy implements ExportStrategy {
public void export(List<Map<String, Object>> data, OutputStream out) throws IOException {
if (data.isEmpty()) return;
PrintWriter writer = new PrintWriter(out);
writer.println(String.join(",", data.get(0).keySet())); // 表头
for (Map<String, Object> row : data) { // 数据行
writer.println(row.values().stream()
.map(String::valueOf)
.collect(Collectors.joining(",")));
}
writer.flush();
}
}
class JsonExportStrategy implements ExportStrategy {
public void export(List<Map<String, Object>> data, OutputStream out) throws IOException {
new ObjectMapper().writeValue(out, data);
}
}
// Context:封装数据查询和脱敏,策略只负责写文件
class ExportContext {
private ExportStrategy strategy;
public ExportContext(ExportStrategy strategy) {
this.strategy = strategy;
}
public void exportData(String query, OutputStream out) throws IOException {
List<Map<String, Object>> data = queryData(query); // Context 查询数据
data = maskSensitiveData(data); // Context 脱敏
strategy.export(data, out); // 策略写文件
}
// queryData / maskSensitiveData 省略...
}
ExportContext ctx = new ExportContext(new CsvExportStrategy());
ctx.exportData("SELECT * FROM orders", response.getOutputStream());
关键点:ExportContext 承担了数据查询、脱敏等业务逻辑,策略只关心"怎么把数据写到文件"。切换导出格式不需要改动任何业务逻辑,换个策略即可。
技术交流 & 更多原创内容,关注公众号:咖啡八杯


浙公网安备 33010602011771号