惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Schneier on Security
Schneier on Security
C
Cisco Blogs
Help Net Security
Help Net Security
I
Intezer
Simon Willison's Weblog
Simon Willison's Weblog
Know Your Adversary
Know Your Adversary
Hacker News: Ask HN
Hacker News: Ask HN
Cisco Talos Blog
Cisco Talos Blog
A
Arctic Wolf
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
WordPress大学
WordPress大学
小众软件
小众软件
Jina AI
Jina AI
量子位
T
Threatpost
Forbes - Security
Forbes - Security
L
LINUX DO - 最新话题
S
Securelist
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
C
Cyber Attacks, Cyber Crime and Cyber Security
有赞技术团队
有赞技术团队
博客园_首页
T
Threat Research - Cisco Blogs
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
The Exploit Database - CXSecurity.com
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Scott Helme
Scott Helme
博客园 - 【当耐特】
IT之家
IT之家
Hugging Face - Blog
Hugging Face - Blog
月光博客
月光博客
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
大猫的无限游戏
大猫的无限游戏
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
The Last Watchdog
The Last Watchdog
Attack and Defense Labs
Attack and Defense Labs
Google DeepMind News
Google DeepMind News
阮一峰的网络日志
阮一峰的网络日志
SecWiki News
SecWiki News
博客园 - 叶小钗
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
罗磊的独立博客
The Cloudflare Blog
S
Schneier on Security
爱范儿
爱范儿
N
News and Events Feed by Topic
NISL@THU
NISL@THU
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More

博客园_首页

Plist 二进制格式 Milvus 和 PGVector,哪个更好? OpenClaw 已过时?在 VS Code 中运行 Hermes Agent! 第30篇文章:一个大三计科生的自白 Manim如何在数学公式中完美显示中文? Docker 部署 RocketMQ 5 并发编程核心概念辨析 C#事务处理最佳实践:别再让“主表存了、明细丢了”的破事发生 CLI 是什么?为什么大厂突然集体卷命令行? 【从0到1构建一个ClaudeAgent】协作-自主Agent UIImageView 设置图片不生效的原因排查 最小二乘问题详解20:无先验约束下的增量式SFM自由网平差 痞子衡嵌入式:大话双核i.MXRT1180之XIP应用里借助MU实现可靠Flash IAP的方法 AI Chat 封装, SemanticKerne.AiProvider.Unified 已发布 Windows下右键编辑js文件无法打开记事本——在注册表中使用环境变量 在后台服务中使用 Scoped 服务,为什么总是报错? H200 安装驱动并使用sglang启动模型 wireshark 抓包Trap上报告警内容 我用 AI 辅助开发了一系列小工具(2):图片压缩工具 [A Primer On MC and CC] 2.1 Memory Consistency 1 - 指令重排序和 SC 模型 Oracle数据库SCN推进技术详解与实践指南 玩转控件:封装个带图片的Label控件 Claude Code 4.7 真正该升级的不是模型,而是你的工作流 前端小白一句话,AI 帮我做了个颜值拉满的桌面媒体播放器。当代码不再是门槛,一句话编程就是现实。 5. WorkBuddy: 小龙虾的灵魂三件套,让你的小龙虾不只是工具 SQLite 分片方案实战:三种分片策略的深度对比 告别简陋 UI!一款基于 Fluent Design 和基于 WinUI 的开源免费、现代化的 Avalonia UI 控件库 关于二进制排列组合枚举的总结 AI开发-python-LangGraph框架(3-27-LangGraph从零实现大模型智能决策工作流) ElasticSearch主分片和副本分片概念详解 【002】HTTPS 粗解:证书、TLS 握手与对后端配置的影响 Hermes Agent 一周暴涨五万 Star,但我劝你别急着追 明明连接的是Redis的DB0,为什么能查到DB3的数据? 【从0到1构建一个ClaudeAgent】协作-Agent团队 熟悉电子元器件之后,电子小白下一步该怎么走? MAF快速入门(23)通过C#类定义Skills .NET 高级开发 | 手写一个对象映射框架 FastAPI数据库ORM怎么选?我肝了三个Demo后,终于不再纠结了 mysqldump 参数拾遗:在遗忘与铭记之间 C# .NET 周刊|2026年3月5期 Claude code入门 - 陈彦斌 一文学习入门 ThingsBoard 开源物联网平台 GitHub 热门项目 | 2026年04月16日 如何为GIT设置全局勾子,为每次提交追加信息 Number.isFinite和isFinite与isNaN()和Number.isNaN的区别 PortSwigger SQL注入LAB2 推荐一个测试人必备的Skills,从功能到性能全搞定(附详细实操和安装下载方式) 筑基期:掌握Odoo基础核心知识点02(Odoo XML 开发方式详解) GLM模型这么火,咱们用vllm也咧一个呗! 深入理解 AbortController:从底层原理到跨语言设计哲学 字符串学习笔记 多租户系统框架的基础模块设计和分析设计 Apache SeaTunnel Zeta 为什么能做到“又快又稳”? AI开发-python-LangGraph框架(3-26-LangGraph基本概念及第一个简单样例) Vue 3 组件通信,别只会用 Props 和 Emits 了,这几个狠活儿你得看看 ElasticSearch7.X版本配置密码 用Manim实现动态交点计算--从一个动点问题说起 团结引擎+Addressable+Instant Game打包抖音小游戏 function call 实战:让 LLM 自动判断 pod 异常、调用日志工具并完成故障分析 bubseek —— 让 Agent 的足迹,变成团队的洞察 通过 C# 读取并导出 PDF 书签 如何用 GitHub Actions 实现 Steam 自动化发布 【从0到1构建一个ClaudeAgent】并发-后台任务 .NET 高级开发 | 定制 ASP.NET Core 框架 电子小白:什么是运算放大器(运放) zero2Agent:面向大厂面试的 Agent 工程教程,从概念到生产的完整学习路线 堆上的ORW HC32F460 USB CDC通信异常:非对齐访问异常排查 20260413-Hyperbridge 攻击事件:发生在默克尔山上的验证绕过 那些喊着AI 要淘汰你的人,正在靠你的焦虑赚大钱! 深度学习进阶(八)Swin Transformer 最小二乘问题详解19:带先验约束的增量式SFM优化与实现 SnapTranslate 3.0 正式发布:全局划词翻译 + 完整英语学习闭环,一站式搞定查词、记词、复习 工作的意义、工作的困难认知再思考 .NET + AI 进阶实战:基于类的技能开发 - 打造可治理的 Agent 能力模块 【从0到1构建一个ClaudeAgent】规划与协调-技能 上周热点回顾(4.6-4.12) 电子小白的工具三件套:面包板、杜邦线、万能板 单表五亿数据的查询优化 | Mysql、StarRocks 2. WorkBuddy:从“我是谁”到“帮我干活” C# 如何减少代码运行时间:7 个实战技巧 基于HelixToolkit.SharpDX 渲染3D模型 - 笺上知微 从零开始的双臂具身VLA起源及现阶段发展综述 - SkyXZ 记对 xonsh shell 的使用, 脚本编写, 迁移及调优 - pluvium27 受够了Vibe Coding的失控?换个起点,让AI事半功倍 从开始配置漏洞环境到漏洞复现流程 - 難しい 关于10年工作经验的程序员对OpenClaw的实战经验分享以及看法 - 虚无境 Any metadata 的内存布局 C# .NET 周刊|2026年3月2期 - InCerry 我帮你测过了,测试圈排名第二的 Skill 依然很牛逼 Skill Discovery | 无监督技能发现的经典工作总结 - MoonOut 上下文工程是什么?过时了么?一文讲明白! - 一枫说码 开了 TUN 模式还是直连?90% 的人都踩过这个坑 AScript扩展多种脚本语言 - rockey627 AI 学习笔记:Agent 的记忆机制 你能被装进一个文件里吗?——7 万人把同事"蒸馏"成了 AI - 我没有三颗心脏 Claude Code 通关手册(七):给 AI 装上技能包——Skills 完全指南 - 暮色之狐 在浏览器中快速编辑代码:VSCode Web 集成实践 - Newbe36524 蒸馏自己 skill?基于 Deepseek 的蒸馏器,丐版蒸馏方式,简单便捷 - To_Carpe_Diem Spring AI Aliababa和AgentScope,哪个更好? - 苏三说技术
GoF设计模式——策略模式
咖啡八杯 · 2026-06-23 · via 博客园_首页

本文是【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 承担了数据查询、脱敏等业务逻辑,策略只关心"怎么把数据写到文件"。切换导出格式不需要改动任何业务逻辑,换个策略即可。

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