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

推荐订阅源

爱范儿
爱范儿
P
Palo Alto Networks Blog
月光博客
月光博客
H
Hackread – Cybersecurity News, Data Breaches, AI and More
I
InfoQ
aimingoo的专栏
aimingoo的专栏
腾讯CDC
T
Threatpost
D
DataBreaches.Net
Vercel News
Vercel News
F
Fortinet All Blogs
Engineering at Meta
Engineering at Meta
C
Cybersecurity and Infrastructure Security Agency CISA
Forbes - Security
Forbes - Security
U
Unit 42
C
Check Point Blog
Blog — PlanetScale
Blog — PlanetScale
O
OpenAI News
量子位
TaoSecurity Blog
TaoSecurity Blog
Microsoft Azure Blog
Microsoft Azure Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
V
Visual Studio Blog
Recorded Future
Recorded Future
云风的 BLOG
云风的 BLOG
Security Archives - TechRepublic
Security Archives - TechRepublic
The Last Watchdog
The Last Watchdog
S
Security Affairs
Attack and Defense Labs
Attack and Defense Labs
罗磊的独立博客
Stack Overflow Blog
Stack Overflow Blog
Microsoft Security Blog
Microsoft Security Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
V
V2EX
小众软件
小众软件
S
SegmentFault 最新的问题
www.infosecurity-magazine.com
www.infosecurity-magazine.com
W
WeLiveSecurity
AI
AI
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
博客园 - 聂微东
I
Intezer
Know Your Adversary
Know Your Adversary
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
P
Proofpoint News Feed
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
The Cloudflare Blog
博客园_首页
NISL@THU
NISL@THU
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO

博客园_首页

Linux实操--组管理、权限管理和定时任务 Java + EasyExcel 实现单个接口导出多个Excel Mem0 源码解析系列(二):提示词工程的深度剖析 Openclaw TaskFlow究竟是什么?和普通Skill技能有什么区别 博文阅读密码验证 - 博客园 嘉立创开源:应该是全网MicroPython教程最多的开发板 Hermes Agent 集成实践:从协议到生产 2026年AI编程工具横评:Cursor、Codex、Claude Code、Zed、Windsurf Java程序员必看的RAG入门教程 2026 AI效率神器:Superpowers + Claude Code 保姆级教程 本地大模型部署全攻略:从 0 到 1 玩转 Ollama 【从0到1构建一个ClaudeAgent】内存管理-上下文压缩 .NET 高级开发 | 设计、实现一个事件总线框架 电子小白入门之NE555 3. WorkBuddy:隐藏玩法,一键召唤专家,让 AI 以"专家身份"给你干活 和AI一起搞事情#3:Claude Teammate 游戏开发翻车实录 【OpenClaw】通过 Nanobot 源码学习架构---(7)Memory C# .NET 周刊|2026年3月3期 我在 Debian 11 上把 K8s 单机搭起来了,过程没你想的那么顺(/opt 目录版) 深度学习进阶(七)Data-efficient Image Transformer CLI+Skill搭建浏览器AI自动化框架,告别一切重复枯燥任务 告别Token账单无底洞:OpenClaw本地部署,重塑企业数据主权的唯一解 FastAPI+Vue:文件分片上传+秒传+断点续传,这坑我帮你踩平了! SBTI 爆火后,我做了个程序员版的 CBTI。。已开源 + 附开发过程 多模态检索开始进入工程期:用 Sentence Transformers 搭建可落地的 Multimodal RAG 100多行代码实现一个最简单的Agent(用ReAct) Claude Code 通关手册(八):推荐 5 个 Hooks,代码质量提升 3 倍 老板:“有人截图了!”。安全部门:“收到,马上查暗水印!” - why技术 技术之外,皆是人间 C#/.NET/.NET Core技术前沿周刊 | 第 69 期(2026年4.01-4.12) Snack JSONPath 项目架构分析 Claude Code Buddy 小析:一个非核心功能,如何体现产品的细节完成度 AI新时代下的图床管理方案-Cloudflare图床+MCP+Skills方案指南 化繁为简:顺丰速运App如何通过 HarmonyOS SDK实现专业级空间测量 从零实现富文本编辑器#13-React非编辑节点的内容渲染 AI开发-python-langchain框架(3-23-OpenAI Functions风格Tool Calling智能助手) .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 PbootCMS 网站内容数量多导致访问慢?这些实用优化方案帮你提速! - 家兴网络技术工作室 上下文工程是什么?过时了么?一文讲明白! - 一枫说码 网站漏洞怎么发现并修复?一篇实用指南(附完整流程) - 家兴网络技术工作室 开了 TUN 模式还是直连?90% 的人都踩过这个坑 Github日报|2026年04月12日 - AI一族 AScript扩展多种脚本语言 - rockey627 AI 学习笔记:Agent 的记忆机制 你能被装进一个文件里吗?——7 万人把同事"蒸馏"成了 AI - 我没有三颗心脏 Claude Code 通关手册(七):给 AI 装上技能包——Skills 完全指南 - 暮色之狐 在浏览器中快速编辑代码:VSCode Web 集成实践 - Newbe36524 蒸馏自己 skill?基于 Deepseek 的蒸馏器,丐版蒸馏方式,简单便捷 - To_Carpe_Diem Spring AI Aliababa和AgentScope,哪个更好? - 苏三说技术 Etsy 把 1000 个 MySQL 分片迁进 Vitess:425TB 数据背后的真正问题不是性能,而是运维规模 MicroPython LVGL基础知识和概念:底层渲染与性能优化 - FreakStudio 数据库草图算法 Python 潮流周刊#146:CPython 引入 Rust 的进展 - 豌豆花下猫 最小生成树 - mofei1116 红日靶场七:从外网入口、容器逃逸到 AD 接管的完整利用链复盘 - YouDiscovered1t 分享四款开源且实用的 Kafka 管理工具 - 追逐时光者 vLLM 权重加载机制全解析:从挑战到理想架构 LCT 学习笔记 - ACehomoxue Avalonia UI 12.0.0 正式发布:架构演进和性能飞跃 - 张善友 当 AI Agent 把调用链拉长,延迟开始成为一门生意 conhost.exe 无法显示 U+2717 - 145a 太秀了,我把自己蒸馏成了 Skill!已开源 - 程序员鱼皮 ASP.NET Core 内存缓存实战:一篇搞懂该怎么配、怎么避坑 基于 Ghostty 带有分割标签页和为 Claude 编程设计的通知终端 - BugShare AI 焊死入口:教育的“操作系统级”重塑 - 郝hai 初级Java开发工程师使用sql脚本编写代码的过程是简单而且不糊涂 - CoderOilStation Claude Code通关手册(六):MCP协议完全指南 - 暮色之狐 边框灯光环绕动画特效实现指南 - Newbe36524 开源:子木蒸馏版的 SEO 审计工具 seo-audit-skill v1.0 我所理解的Python元模型 【从0到1构建一个ClaudeAgent】规划与协调-TodoWrite - 程序员Seven Claude 和 Codex 在审计 Skill 上性能差异探究 - ACai_sec AScript如何实现中文脚本引擎 - rockey627 【渗透测试】HTB Season10 Garfield 全过程wp - dynasty_chenzi Android 开发者为什么必须掌握 AI 能力?端侧视角下的技术变革 树状数组正确性证明 - AC-wyr 你的 AI 焦虑,可能比 AI 本身更危险——ATM 机没有消灭银行柜员,但恐慌消灭了你的判断力 - 我没有三颗心脏 一个拉胯的分库分表方案有多绝望?整个部门都在救火! - 冰河团队 动态规划入门必学之走方格问题 - Ofnoname PostgREST 与 PostgreSQL 角色权限配置全解析(生产级实践) - SheepDog1998 使用 UEFI 图形输出协议 GOP 在屏幕上显示图像的方法 - 阿源- Claude Code通关手册(五):组建你的AI专家团队,子代理系统 - 暮色之狐 一个程序员到架构师的催婚路之感悟(整整10年后的催婚相亲感悟) - MisterLip 用 Agent Skill 自动生成工作周报 - 赵康
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 承担了数据查询、脱敏等业务逻辑,策略只关心"怎么把数据写到文件"。切换导出格式不需要改动任何业务逻辑,换个策略即可。

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