GoF设计模式——策略模式

为什么需要策略模式?

电商网站结算时常常要算优惠:新用户满减、不同等级会员打折、节日活动满减。最直觉的写法是把所有规则塞进一个方法里用 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 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 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;Context:products.sort(comparator)

Comparator byPrice = Comparator.comparingInt(p -> p.price); // 按价格升序

Comparator byName = Comparator.comparing(p -> p.name); // 按名称

Comparator bySales = Comparator.comparingInt(p -> p.sales).reversed(); // 按销量降序

Comparator combined = bySales.thenComparing(byPrice); // 组合策略

List 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 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 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 items) {

return (int) (subtotal * rate);

}

}

// Context:封装订单业务逻辑

class PromotionContext {

private PromotionStrategy strategy;

public void setStrategy(PromotionStrategy strategy) {

this.strategy = strategy;

}

// Context 负责:计算小计;策略只负责:根据规则计算优惠

public int calculateFinalPrice(List 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());

相关推荐
折哥的程序人生 · 物流技术专研10 小时前
第4篇:Lambda 简化策略模式(Java 8+)
java·设计模式·策略模式·函数式编程·lambda·代码简化·扩充系列
jsonbro17 小时前
手把手带你实现发布订阅模式
前端·vue.js·设计模式
workflower18 小时前
室内外配送机器人-应用路径
人工智能·机器学习·设计模式·矩阵·自动化
ttod_qzstudio1 天前
【软考设计模式】原型模式:“克隆“对象与深拷贝、浅拷贝精讲
设计模式·原型模式
geovindu1 天前
java: Builder Pattern
java·开发语言·后端·设计模式·建造者模式·生成器模式
researcher-Jiang1 天前
Design Patterns——Template Method入门到情景实战
python·设计模式·模板方法模式
在水一缸2 天前
拒绝被打扰:深度解析 macOS 进程守护与“反自动启动”攻防战
macos·策略模式·用户体验·进程守护·自动启动·系统权限
饼干哥哥2 天前
用Claude Code跑通跨境电商的5大场景,顶10个人的团队
人工智能·设计模式·正则表达式
折哥的程序人生 · 物流技术专研2 天前
第3篇:手写促销策略引擎(满减、打折、立减)
java·设计模式·策略模式·开闭原则·编程实战·扩充系列·促销引擎