GoF设计模式——策略模式

📅 2026/6/30 4:20:27
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上下文持有策略引用负责业务数据管理和流程编排在适当时机调用策略方法图中各类之间的关系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 ListInteger 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 InterfaceJava 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、FunctionInteger,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 类负责管理购物车商品和计算总价优惠策略只负责根据总价计算优惠。输入描述第一行输入整数 N1 ≤ N ≤ 20表示收银次数。每次收银第一行输入两个整数 k 和 tk1 ≤ k ≤ 20表示商品数量t0/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 ListInteger 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(); } }验证示例第一次商品 1005050200策略 1九折→ 200×0.9180第二次商品 100807050300策略 2满减→ 300−40260第三次商品 6040100策略 0无优惠→ 100三行结果与输出示例一致。扩展实际项目中的策略模式JDK 中的 Comparator 排序java.util.Comparator是 Java 标准库中最经典的策略模式应用。Collections.sort()或Arrays.sort()是 ContextComparator是策略接口不同的比较逻辑是具体策略。// 策略接口ComparatorTContextproducts.sort(comparator) ComparatorProduct byPrice Comparator.comparingInt(p - p.price); // 按价格升序 ComparatorProduct byName Comparator.comparing(p - p.name); // 按名称 ComparatorProduct bySales Comparator.comparingInt(p - p.sales).reversed(); // 按销量降序 ComparatorProduct combined bySales.thenComparing(byPrice); // 组合策略 ListProduct 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, ListPromotionItem 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, ListPromotionItem 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, ListPromotionItem items) { return (int) (subtotal * rate); } } // Context封装订单业务逻辑 class PromotionContext { private PromotionStrategy strategy; public void setStrategy(PromotionStrategy strategy) { this.strategy strategy; } // Context 负责计算小计策略只负责根据规则计算优惠 public int calculateFinalPrice(ListPromotionItem 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(ListMapString, Object data, OutputStream out) throws IOException; } class CsvExportStrategy implements ExportStrategy { public void export(ListMapString, Object data, OutputStream out) throws IOException { if (data.isEmpty()) return; PrintWriter writer new PrintWriter(out); writer.println(String.join(,, data.get(0).keySet())); // 表头 for (MapString, Object row : data) { // 数据行 writer.println(row.values().stream() .map(String::valueOf) .collect(Collectors.joining(,))); } writer.flush(); } } class JsonExportStrategy implements ExportStrategy { public void export(ListMapString, 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 { ListMapString, 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());