【软考设计模式】策略模式:算法族的封装与动态切换精讲

【软考设计模式】策略模式:算法族的封装与动态切换精讲 系列定位软考软件设计师 / 系统架构设计师 — 行为型模式专题第 1 讲考察分值上午题 1-2 分下午题常作为代码填空或类图识别出现难度等级⭐⭐⭐☆☆结构清晰Context 与 Strategy 的组合关系是填空重点一、考纲定位与模式定义1.1 考纲要求策略模式在软考中属于行为型模式的基础内容。考察形式包括上午选择题判断场景描述所属模式识别策略模式与状态模式、模板方法模式的区别判断 Context 与 Strategy 的关系组合 vs 继承下午设计题补全 Context 类中setStrategy()和execute()方法的代码补全 ConcreteStrategy 中算法实现的代码识别类图中 Context、Strategy、ConcreteStrategy 三个角色1.2 模式定义策略模式定义一系列算法把它们一个个封装起来并且使它们可以相互替换。策略模式让算法的变化独立于使用算法的客户。核心意图当一个系统需要多种算法/策略来完成同一件事如不同的促销方式、不同的支付方式、不同的排序算法且这些算法需要动态切换时如果将所有算法写在一个类里用if-else或switch判断会导致类臃肿、难以扩展。策略模式将每种算法封装为独立的策略类Context 通过组合持有策略对象运行时动态替换让算法的变化独立于使用算法的客户。通俗理解你去旅游从 A 地到 B 地有多种策略坐飞机快但贵、坐高铁中等、自驾自由但累。去旅游 这件事是 Context交通方式 是 Strategy。你不需要在出门前就把所有交通方式的细节都写在行程单里if-else而是到了车站根据情况动态选择一种策略买票/租车。如果未来新增了 骑共享单车 这种方式只需新增一个策略类不需要修改行程单Context的代码。二、UML 类图与角色划分┌─────────────────────────────────────────────────────────┐ │ Client │ │ main() │ │ ├── Context ctx new Context(); │ │ ├── ctx.setStrategy(new ConcreteStrategyA()); │ │ └── ctx.execute(); // 实际执行的是策略 A 的算法 │ └───────────────────────┬─────────────────────────────────┘ │ uses ▼ ┌─────────────────────────────────────────────────────────┐ │ Context │ │ ── 上下文持有策略引用调用策略的算法 ── │ │ - strategy: Strategy ◄── 组合关系 │ │ setStrategy(Strategy s) { │ │ this.strategy s; │ │ } │ │ execute() { │ │ strategy.algorithm(); // 委托给策略执行 │ │ } │ └───────────────────────┬─────────────────────────────────┘ │ delegates to ▼ ┌─────────────────────────────────────────────────────────┐ │ interface │ │ Strategy │ │ algorithm() │ └───────────────────────┬─────────────────────────────────┘ ┌────────────┴────────────┐ ▼ ▼ ┌─────────────────────┐ ┌─────────────────────┐ │ ConcreteStrategyA │ │ ConcreteStrategyB │ │ algorithm() { │ │ algorithm() { │ │ // 算法 A 实现 │ │ // 算法 B 实现 │ │ } │ │ } │ └─────────────────────┘ └─────────────────────┘角色职责软考填空关键词Context上下文持有一个 Strategy 的引用定义一个接口让客户端可以设置/更换策略将操作委托给策略对象执行classprivate Strategy strategysetStrategy()execute()Strategy抽象策略定义所有支持的算法的公共接口让 Context 可以统一调用interface/abstract classalgorithm()ConcreteStrategy具体策略实现 Strategy 接口封装具体的算法或行为implements Strategy 具体算法实现Client客户端创建 Context 和 ConcreteStrategy通过setStrategy()将策略注入 Context调用execute()new Context()context.setStrategy(new XxxStrategy())类图识别要点软考类图中策略模式的核心特征是Context 类通过组合聚合持有一个 Strategy 引用带空心菱形的实线且 Context 中有setStrategy()方法用于动态更换策略。如果类图里有一个类持有另一个接口的引用且有 setter 方法可以替换 → 策略模式。三、场景一电商促销策略最经典的策略模式业务背景电商系统需要支持多种促销策略满减满 100 减 20、折扣打 8 折、直降立减 15 元。结算时根据用户选择的优惠券类型动态应用不同的计价策略。要求新增促销方式时不需要修改结算核心代码。说明这是策略模式最经典的例子也是软考下午题最爱考的 多种算法动态切换 场景。3.1 代码实现// Strategy促销策略接口 public interface PromotionStrategy { double calculatePrice(double originalPrice); } // ConcreteStrategy满减策略 public class FullReductionStrategy implements PromotionStrategy { private double threshold; private double reduction; public FullReductionStrategy(double threshold, double reduction) { this.threshold threshold; this.reduction reduction; } Override public double calculatePrice(double originalPrice) { if (originalPrice threshold) { return originalPrice - reduction; } return originalPrice; } } // ConcreteStrategy折扣策略 public class DiscountStrategy implements PromotionStrategy { private double discountRate; public DiscountStrategy(double discountRate) { this.discountRate discountRate; } Override public double calculatePrice(double originalPrice) { return originalPrice * discountRate; } } // ConcreteStrategy直降策略 public class DirectReductionStrategy implements PromotionStrategy { private double reduction; public DirectReductionStrategy(double reduction) { this.reduction reduction; } Override public double calculatePrice(double originalPrice) { double finalPrice originalPrice - reduction; return finalPrice 0 ? finalPrice : 0; } } // Context结算上下文 public class CheckoutContext { private PromotionStrategy strategy; public void setStrategy(PromotionStrategy strategy) { this.strategy strategy; } public double executeCheckout(double originalPrice) { if (strategy null) { throw new IllegalStateException(未设置促销策略); } double finalPrice strategy.calculatePrice(originalPrice); System.out.println(原价: ¥ originalPrice - 实付: ¥ finalPrice); return finalPrice; } } // Client客户端 public class Client { public static void main(String[] args) { double originalPrice 200.0; CheckoutContext checkout new CheckoutContext(); checkout.setStrategy(new FullReductionStrategy(100, 20)); checkout.executeCheckout(originalPrice); checkout.setStrategy(new DiscountStrategy(0.8)); checkout.executeCheckout(originalPrice); checkout.setStrategy(new DirectReductionStrategy(50)); checkout.executeCheckout(originalPrice); } }关键体会CheckoutContext不知道具体是哪种促销策略它只面向PromotionStrategy接口编程。新增一种促销方式如 第二件半价只需新增一个implements PromotionStrategy的类不需要修改CheckoutContext的代码。这符合开闭原则。如果没有策略模式结算代码里会充满if (type.equals(fullReduction)) { ... } else if (type.equals(discount)) { ... }的臃肿代码。四、场景二支付渠道策略实际开发场景业务背景系统需要支持多种支付方式支付宝、微信支付、银行卡支付。每种支付方式的流程不同支付宝需要调用支付宝 SDK微信需要调用微信 SDK银行卡需要调用银联接口但对外提供的接口一致pay(amount)和refund(orderId)。客户端根据用户选择的支付方式动态切换策略。说明这是实际开发中策略模式的典型应用也是软考下午题中 多算法封装 场景的代表。4.1 代码实现// Strategy支付策略接口 public interface PaymentStrategy { boolean pay(String orderId, double amount); boolean refund(String orderId, double amount); } // ConcreteStrategy支付宝支付 public class AlipayStrategy implements PaymentStrategy { Override public boolean pay(String orderId, double amount) { System.out.println([支付宝] 订单 orderId 支付 ¥ amount); return true; } Override public boolean refund(String orderId, double amount) { System.out.println([支付宝] 订单 orderId 退款 ¥ amount); return true; } } // ConcreteStrategy微信支付 public class WechatPayStrategy implements PaymentStrategy { Override public boolean pay(String orderId, double amount) { System.out.println([微信支付] 订单 orderId 支付 ¥ amount); return true; } Override public boolean refund(String orderId, double amount) { System.out.println([微信支付] 订单 orderId 退款 ¥ amount); return true; } } // ConcreteStrategy银行卡支付 public class BankCardStrategy implements PaymentStrategy { Override public boolean pay(String orderId, double amount) { System.out.println([银行卡] 订单 orderId 支付 ¥ amount 调用银联接口); return true; } Override public boolean refund(String orderId, double amount) { System.out.println([银行卡] 订单 orderId 退款 ¥ amount 原路退回); return true; } } // Context支付上下文 public class PaymentContext { private PaymentStrategy strategy; public void setStrategy(PaymentStrategy strategy) { this.strategy strategy; } public boolean executePay(String orderId, double amount) { if (strategy null) { throw new IllegalStateException(未选择支付方式); } System.out.println(开始支付...); return strategy.pay(orderId, amount); } public boolean executeRefund(String orderId, double amount) { if (strategy null) { throw new IllegalStateException(未选择支付方式); } System.out.println(开始退款...); return strategy.refund(orderId, amount); } } // Client public class Client { public static void main(String[] args) { PaymentContext payment new PaymentContext(); String orderId ORD20240711001; double amount 299.0; payment.setStrategy(new AlipayStrategy()); payment.executePay(orderId, amount); payment.setStrategy(new WechatPayStrategy()); payment.executePay(orderId, amount); payment.setStrategy(new BankCardStrategy()); payment.executePay(orderId, amount); } }五、场景三排序算法策略算法族封装业务背景系统需要对一组数据进行排序但排序算法需要根据数据规模和特性动态选择数据量小用冒泡排序简单数据量大用快速排序高效数据基本有序用插入排序最优。使用策略模式封装不同的排序算法Context 根据条件自动选择或允许客户端手动选择。说明这是策略模式在算法选择场景中的典型应用也是软考上午题中 算法替换 场景的代表。5.1 代码实现import java.util.Arrays; // Strategy排序策略接口 public interface SortStrategy { void sort(int[] data); } // ConcreteStrategy冒泡排序 public class BubbleSortStrategy implements SortStrategy { Override public void sort(int[] data) { System.out.println([冒泡排序] 开始排序数据量: data.length); for (int i 0; i data.length - 1; i) { for (int j 0; j data.length - 1 - i; j) { if (data[j] data[j 1]) { int temp data[j]; data[j] data[j 1]; data[j 1] temp; } } } System.out.println([冒泡排序] 完成: Arrays.toString(data)); } } // ConcreteStrategy快速排序 public class QuickSortStrategy implements SortStrategy { Override public void sort(int[] data) { System.out.println([快速排序] 开始排序数据量: data.length); quickSort(data, 0, data.length - 1); System.out.println([快速排序] 完成: Arrays.toString(data)); } private void quickSort(int[] data, int low, int high) { if (low high) { int pivot partition(data, low, high); quickSort(data, low, pivot - 1); quickSort(data, pivot 1, high); } } private int partition(int[] data, int low, int high) { int pivot data[high]; int i low - 1; for (int j low; j high; j) { if (data[j] pivot) { i; int temp data[i]; data[i] data[j]; data[j] temp; } } int temp data[i 1]; data[i 1] data[high]; data[high] temp; return i 1; } } // Context排序上下文带自动选择逻辑 public class SortContext { private SortStrategy strategy; public void setStrategy(SortStrategy strategy) { this.strategy strategy; } public void autoSelectStrategy(int[] data) { if (data.length 50) { System.out.println([上下文] 数据量较小自动选择冒泡排序); this.strategy new BubbleSortStrategy(); } else { System.out.println([上下文] 数据量较大自动选择快速排序); this.strategy new QuickSortStrategy(); } } public void executeSort(int[] data) { if (strategy null) { autoSelectStrategy(data); } strategy.sort(data); } } // Client public class Client { public static void main(String[] args) { SortContext sorter new SortContext(); int[] smallData {5, 3, 8, 1, 2}; sorter.executeSort(smallData); System.out.println(); int[] largeData new int[1000]; for (int i 0; i largeData.length; i) { largeData[i] (int) (Math.random() * 10000); } sorter.executeSort(largeData); System.out.println(); int[] data {9, 7, 5, 3, 1}; sorter.setStrategy(new BubbleSortStrategy()); sorter.executeSort(data); } }六、三种场景对比与演进思路维度场景一电商促销场景二支付渠道场景三排序算法ContextCheckoutContext结算上下文PaymentContext支付上下文SortContext排序上下文StrategyPromotionStrategy促销策略PaymentStrategy支付策略SortStrategy排序策略ConcreteStrategyFullReduction、Discount、DirectReductionAlipay、WechatPay、BankCardBubbleSort、QuickSort策略切换方式客户端手动setStrategy()客户端手动setStrategy()支持自动选择 手动设置算法参数策略构造器传入threshold、rate无参内部调用 SDK无参操作传入的数据返回值double最终价格boolean成功/失败void直接修改数组新增策略成本新增类不改 Context新增类不改 Context新增类不改 Context软考考法下午大题补全 calculatePrice下午大题补全 pay/refund上午选择题识别算法替换七、策略模式 vs 状态模式 vs 模板方法模式核心对比对比项策略模式状态模式模板方法模式模式分类行为型行为型行为型核心思想封装算法族让它们可以相互替换封装状态转换状态改变时行为改变定义算法骨架子类实现某些步骤切换触发者客户端主动调用setStrategy()切换状态内部根据条件自动转换到下一个状态父类控制流程子类被动参与类关系Context组合Strategy聚合关系Context组合State聚合关系抽象类继承子类 extends 父类客户端感知客户端知道有哪些策略主动选择客户端不知道状态转换细节客户端不知道模板方法的存在代码结构一个 Context N Strategy 实现类一个 Context N State 实现类 可能的状态机一个抽象类 N 子类新增扩展新增策略类不改 Context新增状态类可能需修改状态转换逻辑新增子类实现抽象方法典型应用促销策略、支付方式、排序算法订单状态机待支付→已支付→已发货、游戏角色状态数据导入模板、报表生成模板软考判断看到 多种算法可替换、客户端选择 → 策略看到 状态转换、自动改变行为 → 状态看到 算法骨架、子类实现特定步骤 → 模板方法记忆口诀策略是 客户端选算法主动切换状态是 状态自己变自动转换模板方法是 父类定骨架子类填坑继承实现。八、软考高频考点与易混淆辨析8.1 高频考点考点内容模式分类行为型模式GoF 23 正式成员核心三角色Context上下文、Strategy抽象策略、ConcreteStrategy具体策略核心思想封装算法族让它们可以相互替换算法的变化独立于使用算法的客户Context 与 Strategy 关系组合/聚合Context 持有 Strategy 引用不是继承动态切换通过setStrategy()方法在运行时更换策略设计原则开闭原则新增策略只需增加 ConcreteStrategy无需修改 Context消除 if-else策略模式的核心价值之一避免 Context 中出现大量条件判断语句与状态模式区别策略客户端主动选择状态状态内部自动转换与模板方法区别策略组合 算法整体替换模板方法继承 算法骨架固定、部分步骤替换适用场景① 多种算法/策略需要动态切换 ② 需要避免多重条件判断 ③ 算法需要独立于客户端变化8.2 易混淆辨析策略 vs 状态 vs 模板方法对比项策略模式状态模式模板方法模式UML 结构Context 组合 StrategyContext 组合 State抽象类定义模板子类继承切换控制权外部客户端/setter内部状态对象内部转换无切换子类固定客户端角色客户端选择策略并注入客户端触发事件状态自动流转客户端调用模板方法类数量1 Context N Strategy1 Context N State 可能的状态机1 抽象类 N 子类关系类型组合/聚合组合/聚合继承软考考法代码填空补全 setStrategy execute代码填空补全状态转换逻辑代码填空补全子类实现特定步骤九、真题风格模拟与代码填空模拟题 1上午选择题以下关于策略模式的叙述中正确的是。A. 策略模式属于结构型模式主要用于封装对象的创建逻辑B. 在策略模式中Context 通过继承 Strategy 来获得算法实现C. 策略模式定义了一系列算法把它们封装起来并且使它们可以相互替换让算法的变化独立于使用算法的客户D. 策略模式与状态模式的核心区别在于策略模式由状态内部自动转换状态模式由客户端主动选择答案C解析A 错误策略模式属于行为型模式不是结构型。它封装的是算法/行为不是对象的创建。B 错误Context 通过组合/聚合持有 Strategy 引用来使用算法不是继承。如果用继承就无法在运行时动态切换策略。C 正确这是策略模式的核心定义GoF 原书定义。D 错误说反了。策略模式由客户端主动选择setStrategy()状态模式由状态内部自动转换状态对象根据条件改变 Context 的状态。模拟题 2下午代码填空 — 补全 Context 的 setStrategy 和 execute某系统使用策略模式实现不同的数据验证策略。ValidationStrategy接口提供validate(String data)方法。LengthValidationStrategy和RegexValidationStrategy是具体策略。ValidatorContext是上下文。请补全1~4。// Strategy interface ValidationStrategy { boolean validate(String data); } // ConcreteStrategy长度验证 class LengthValidationStrategy implements ValidationStrategy { private int min; private int max; public LengthValidationStrategy(int min, int max) { this.min min; this.max max; } public boolean validate(String data) { return data ! null data.length() min data.length() max; } } // ConcreteStrategy正则验证 class RegexValidationStrategy implements ValidationStrategy { private String pattern; public RegexValidationStrategy(String pattern) { this.pattern pattern; } public boolean validate(String data) { return data ! null data.matches(pattern); } } // Context class ValidatorContext { private (1)______ strategy; public void (2)______(ValidationStrategy strategy) { this.strategy strategy; } public boolean (3)______(String data) { if (strategy null) { throw new IllegalStateException(未设置验证策略); } return (4)______; } } public class Client { public static void main(String[] args) { ValidatorContext validator new ValidatorContext(); validator.setStrategy(new LengthValidationStrategy(6, 20)); System.out.println(validator.executeValidate(hello123)); } }答案(1)ValidationStrategy(2)setStrategy(3)executeValidate或validate(4)strategy.validate(data)阅卷要点(1) 必须是ValidationStrategy抽象策略接口类型体现面向接口编程。不能写LengthValidationStrategy或RegexValidationStrategy等具体类。(2) 必须是setStrategy这是策略模式 Context 的标准方法名用于动态注入/更换策略。(3) 可以是executeValidate、validate、execute等表示执行操作的方法名。软考中通常用executeXxx()或contextInterface()。(4) 必须是strategy.validate(data)将操作委托给策略对象执行。这是策略模式的核心Context 不直接实现算法而是委托给 Strategy。模拟题 3下午代码填空 — 补全 ConcreteStrategy 和 Context 调用某系统使用策略模式实现不同的运费计算策略。ShippingStrategy接口提供calculateShipping(double weight, double distance)方法。StandardShippingStrategy和ExpressShippingStrategy是具体策略。请补全1~2。// Strategy interface ShippingStrategy { double calculateShipping(double weight, double distance); } // ConcreteStrategy标准快递 class StandardShippingStrategy implements ShippingStrategy { public double calculateShipping(double weight, double distance) { return 10 weight * 2 distance * 0.5; } } // ConcreteStrategy顺丰快递需要补全 class ExpressShippingStrategy (1)______ ShippingStrategy { public double calculateShipping(double weight, double distance) { return (2)______; } } // Context class OrderContext { private ShippingStrategy strategy; public void setStrategy(ShippingStrategy strategy) { this.strategy strategy; } public double calculateTotal(double weight, double distance) { return strategy.calculateShipping(weight, distance); } } public class Client { public static void main(String[] args) { OrderContext order new OrderContext(); order.setStrategy(new ExpressShippingStrategy()); double shipping order.calculateTotal(2.5, 100); System.out.println(运费: ¥ shipping); } }答案(1)implements(2)15 weight * 3 distance * 1或等价表达式阅卷要点(1) 必须是implements具体策略类实现策略接口。如果写extends会零分除非 Strategy 是抽象类但这里是 interface。(2) 必须是根据题干描述的算法表达式。题目说 首重 15 元续重每公斤 3 元每公里 1 元所以表达式为15 weight * 3 distance * 1。如果写10 weight * 2 distance * 0.5标准快递的算法会零分。十、常见陷阱与注意事项陷阱 1误认为策略是结构型模式策略模式属于行为型模式因为它关注的是对象之间的行为/算法交互Context 将行为委托给 Strategy而不是类与类之间的结构关系。软考上午题如果问以下属于结构型模式的是选项里出现策略不能选。陷阱 2Context 继承 Strategy 而不是组合这是软考最高频的失分点。策略模式的核心是组合/聚合❌ 错误class Context extends Strategy继承静态绑定无法动态切换✅ 正确class Context { private Strategy strategy; }组合运行时注入软考代码填空里Context 必须持有private Strategy xxx字段并通过setStrategy()方法注入。如果 Context 继承 ConcreteStrategy那就失去了策略切换的意义。陷阱 3与状态模式混淆这是软考最难的辨析题之一。两者的 UML 结构几乎完全相同都有 Context 类Context 都组合一个接口Strategy / State都有多个具体实现类终极区分方法看切换的触发者。策略模式客户端主动调用setStrategy()选择算法。Context 通常不自己决定切换。状态模式状态对象内部根据业务条件自动调用context.setState()转换到下一个状态。客户端通常只触发事件不直接选择状态。如果题干说 客户端根据条件选择不同的算法 → 策略。如果题干说 对象内部状态改变时自动改变行为 → 状态。陷阱 4与模板方法模式混淆策略模式模板方法模式组合关系Context 持有 Strategy继承关系子类 extends 抽象类算法整体替换换一个 Strategy 对象算法骨架固定只替换部分步骤运行时动态切换编译时静态绑定子类固定新增策略新增类 setStrategy()新增模板新增子类 继承如果类图里是组合关系 可以动态替换 → 策略。如果是继承关系 抽象类定义模板方法 → 模板方法。陷阱 5策略类直接暴露给客户端绕过 Context虽然策略模式允许客户端直接创建ConcreteStrategy对象但规范的做法是客户端通过 Context 来使用策略context.setStrategy(new XxxStrategy()); context.execute();。如果客户端直接调用strategy.algorithm()那就失去了 Context 作为统一入口的意义。软考代码填空里如果题目给出了 Context 类客户端应该通过 Context 调用而不是直接调用 Strategy。陷阱 6Context 内部用 if-else 选择策略这是违背策略模式初衷的写法。策略模式的目的就是消除 Context 中的 if-else❌ 错误public double execute(double price) { if (strategy instanceof FullReductionStrategy) { ... } else if (strategy instanceof DiscountStrategy) { ... } }✅ 正确javapublic double execute(double price) { return strategy.calculatePrice(price); // 直接委托无需判断类型 }软考阅卷时如果 Context 的execute()方法里出现了instanceof或if-else判断策略类型通常会扣分因为这违背了策略模式的设计意图。陷阱 7忘记在 Context 中定义 setStrategy 方法策略模式的核心是动态切换setStrategy()方法是实现动态切换的关键。如果 Context 中只有构造器注入策略没有 setter那就只能在创建时确定策略无法在运行时切换。虽然这种 静态策略 也是策略模式的一种用法但软考通常考察的是动态切换的场景代码填空里必须包含setStrategy()方法。十一、总结要点内容定义定义一系列算法把它们一个个封装起来并且使它们可以相互替换。策略模式让算法的变化独立于使用算法的客户分类行为型模式GoF 23 正式成员核心三角色Context上下文、Strategy抽象策略、ConcreteStrategy具体策略核心思想封装算法族通过组合让 Context 在运行时动态切换策略Context 与 Strategy 关系组合/聚合private Strategy strategysetStrategy()动态切换通过setStrategy()在运行时更换策略设计原则开闭原则新增策略只需增加 ConcreteStrategy无需修改 Context消除 if-else避免 Context 中出现大量条件判断将算法选择推迟到客户端与状态模式区别策略客户端主动选择状态状态内部自动转换与模板方法区别策略组合 算法整体替换模板方法继承 算法骨架固定软考重点代码填空补全 Context 的setStrategy()execute()委托调用与状态模式的辨析最高频与模板方法的辨析答题技巧看到 多种算法可替换、动态切换、避免 if-else、客户端选择策略 → 策略看到 Context 组合 Strategy setStrategy()→ 确认策略系列预告下一篇将讲状态模式—— 当对象的行为取决于它的状态且状态转换需要自动触发时如何将状态封装为独立的类让状态自己管理转换逻辑。策略与状态的辨析是软考高频考点咱们下回重点拆解。咱们下回见。