GoF设计模式——策略模式

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

image

前言

为什么需要策略模式?

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

图中各类之间的关系:ConcreteStrategyAConcreteStrategyB 实现 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 的编排逻辑完全不变。更进一步,甚至可以用内建的 IntUnaryOperatorFunction<Integer,Integer> 代替自定义接口,但保留一个有领域含义的 Strategy 接口名,可读性通常更好。

⚠️ Lambda 不是万能替换:当策略逻辑复杂、需要持有自身状态(如满减档位参数)、或要在多处复用、需要被工厂统一管理时,仍应使用独立策略类——Lambda 适合"一行就能说清、用完即弃"的简单算法。

总结

策略模式的本质是把可变的算法封装成独立对象,通过组合而非继承实现行为切换——客户端只面向策略接口,运行时动态替换算法。

什么时候用

  • 系统需要在运行时根据条件动态选择算法
  • 代码中存在大量 if-elseswitch,且每个分支只是行为不同
  • 算法会频繁新增或修改,需要独立于客户端扩展
  • 想用组合替代继承来实现行为变化,避免类爆炸

什么时候不用

  • 算法只有两三种且永远不会变化,直接 if-else 更简单
  • 策略之间需要共享中间状态,策略模式的独立类做不到
  • 客户端完全不需要了解策略差异(此时策略选择逻辑反而成了负担)

简单记忆

策略解决"同一件事多种做法"的问题,把 if-else 拆成可互换的算法类,运行时随便换。

⚠️ 用策略模式要注意:每个策略一个类,算法多时类文件会膨胀;客户端必须知道有哪些策略可用才能选择,策略选择逻辑仍需在某处用 if-else 或工厂集中处理。

相似模式区分

策略模式容易和模板方法、状态、工厂方法混淆,它们都涉及"封装可变行为",但实现方式和意图不同。

总览对比:

模式 接口关系 核心意图 典型场景
策略 Context 持有策略接口引用 封装可互换的算法,运行时切换 折扣算法、支付方式、排序
模板方法 子类继承父类,重写步骤 定义算法骨架,子类填充步骤 框架的流程骨架、生命周期钩子
状态 Context 持有状态接口引用 根据内部状态改变行为 订单状态流转、审批流程
工厂方法 工厂返回产品对象 封装对象创建过程 根据配置创建数据源、解析器

口诀:策略换算法,模板填步骤,状态自身转,工厂管造谁。

策略 vs 模板方法

两者都用于封装可变的行为,但实现方式完全不同。策略模式通过组合实现——Context 持有策略接口的引用,算法在运行时通过注入切换;模板方法通过继承实现——父类定义算法骨架,子类重写具体步骤,行为在编译时就确定了。

维度 策略模式 模板方法
核心意图 封装可互换的算法族 定义算法骨架,子类填充步骤
结构差异 Context 持有接口引用,组合关系 父类定义骨架,子类继承重写
关注点 整个算法的替换 算法中个别步骤的定制
灵活性 运行时动态切换 编译时确定
典型场景 支付方式、折扣算法、排序策略 框架生命周期、流程骨架

逐步区分法

  • 算法整体需要在运行时切换 → 选策略模式
  • 算法骨架固定、只是个别步骤需要子类定制 → 选模板方法

策略 vs 状态

两者结构几乎一样(都有 Context + 接口 + 多个实现类),但意图完全不同。策略模式中,策略由外部注入,策略之间不知道彼此的存在;状态模式中,状态由对象自身管理,状态之间可以触发转换。

维度 策略模式 状态模式
核心意图 封装可互换的算法 根据内部状态改变行为
结构差异 策略由 Context 持有,外部注入 状态由对象自身持有,状态间可转换
关注点 算法的选择与替换 状态流转驱动行为变化
典型场景 折扣算法、支付方式 订单状态流转、审批流程

逐步区分法

  • 行为由调用方决定,主动选哪个算法 → 选策略模式
  • 行为由对象自身状态决定,状态会自动流转 → 选状态模式

策略 vs 工厂方法

两者都会出现 if-elseswitch 来选择不同的实现类,但目的完全不同。工厂方法解决的是 "创建什么对象" ——根据条件决定实例化哪个产品类;策略模式解决的是 "用什么算法" ——将算法封装成独立对象,运行时可替换。

维度 策略模式 工厂方法
核心意图 封装可互换的算法 封装对象的创建过程
结构差异 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 承担了数据查询、脱敏等业务逻辑,策略只关心"怎么把数据写到文件"。切换导出格式不需要改动任何业务逻辑,换个策略即可。

技术交流 & 更多原创内容,关注公众号:咖啡八杯

posted @ 2026-06-23 22:21  咖啡八杯  阅读(27)  评论(0)    收藏  举报