深入浅出设计模式【二十一、策略模式】
一、策略模式介绍
在软件开发中,我们经常需要根据不同的上下文或条件执行不同的算法或业务规则。例如,一个电商系统可能需要支持多种支付方式(支付宝、微信支付、信用卡)、多种折扣策略(满减、百分比折扣、无折扣)或多种排序算法(按价格、按销量、按评分)。
一种直观但笨拙的实现方式是使用条件语句(if-else 或 switch-case)在主业务逻辑中判断并调用不同的算法。这种方法的缺点非常明显:
- 违反开闭原则 (OCP): 增加新算法或修改现有算法需要修改包含条件判断的主类。
- 代码臃肿且难以维护: 主类会充斥着各种算法的实现细节,变得庞大而复杂。
- 难以复用算法: 算法与使用它的上下文紧密耦合,无法独立复用。
策略模式通过将算法抽象和算法实现分离来解决这些问题。它将每个算法封装到一个独立的策略类中,并使它们可以互相替换。客户端代码依赖于算法的抽象接口,而不是具体的实现,从而可以在运行时灵活地选择和切换算法。
二、核心概念与意图
-
核心概念:
- 策略接口 (Strategy): 定义所有支持的算法或策略的公共接口。上下文使用这个接口来调用具体策略定义的算法。
- 具体策略 (Concrete Strategy): 实现策略接口,提供具体的算法实现。
- 上下文 (Context): 持有一个策略对象的引用。上下文通常提供一个接口,允许客户端设置或切换策略。最后,上下文将客户端的请求委托给当前的策略对象执行。
-
意图:
- 定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。
- 使得算法可以独立于使用它的客户端而变化。
- 消除条件语句,提供一种在运行时选择算法行为的灵活方式。
三、适用场景剖析
策略模式在以下场景中非常有效:
- 一个系统需要在多种算法或策略中选择一种时: 例如,支付方式、折扣计算、数据验证规则、排序算法、压缩算法等。
- 需要避免暴露复杂的、与算法相关的数据结构时: 策略模式将算法的实现细节完全隐藏起来。
- 有多个仅在行为上稍有差别的类时: 使用策略模式可以避免创建大量庞大的条件语句。
- 希望算法能够独立于使用它的客户端,便于单元测试和复用: 每个策略都是一个独立的类,可以单独测试和复用。
四、UML 类图解析(Mermaid)
以下UML类图清晰地展示了策略模式的结构和角色间的关系:
Strategy(策略接口): 声明了算法的方法(通常是execute或apply),是所有具体策略的通用接口。ConcreteStrategyA,ConcreteStrategyB,ConcreteStrategyC(具体策略): 实现了Strategy接口,提供了算法的具体实现。Context(上下文):- 持有一个对策略对象的引用 (
-strategy: Strategy)。 - 通常提供一个设置器方法 (
setStrategy) 允许客户端在运行时替换当前使用的策略对象。 - 提供一个方法 (
executeStrategy) 来委托策略执行算法。上下文本身不实现算法,而是将工作委派给连接的策略对象。
- 持有一个对策略对象的引用 (
- 客户端: 负责创建和配置上下文。客户端通常会创建一个具体策略对象,然后将其传递给上下文。之后,客户端通过上下文接口触发算法的执行。
五、各种实现方式及其优缺点
策略模式的实现非常灵活,主要取决于策略接口的设计和上下文的配置方式。
1. 标准实现(接口 + 类)
即上述UML所描述的方式,为每种算法定义一个实现策略接口的具体类。
// 1. Strategy Interface
public interface DiscountStrategy {
double applyDiscount(double originalPrice);
}
// 2. Concrete Strategies
public class NoDiscountStrategy implements DiscountStrategy {
@Override
public double applyDiscount(double originalPrice) {
return originalPrice;
}
}
public class PercentageDiscountStrategy implements DiscountStrategy {
private double percentage;
public PercentageDiscountStrategy(double percentage) {
this.percentage = percentage;
}
@Override
public double applyDiscount(double originalPrice) {
return originalPrice * (1 - percentage / 100);
}
}
public class FixedAmountDiscountStrategy implements DiscountStrategy {
private double discountAmount;
public FixedAmountDiscountStrategy(double discountAmount) {
this.discountAmount = discountAmount;
}
@Override
public double applyDiscount(double originalPrice) {
double result = originalPrice - discountAmount;
return result > 0 ? result : 0; // Ensure price is not negative
}
}
// 3. Context
public class ShoppingCart {
private DiscountStrategy discountStrategy;
private double totalAmount;
public ShoppingCart(double totalAmount) {
this.totalAmount = totalAmount;
this.discountStrategy = new NoDiscountStrategy(); // Default strategy
}
public void setDiscountStrategy(DiscountStrategy discountStrategy) {
this.discountStrategy = discountStrategy;
}
public double checkout() {
return discountStrategy.applyDiscount(totalAmount);
}
}
// 4. Client
public class Client {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart(100.0);
// Client selects and sets the strategy at runtime
cart.setDiscountStrategy(new PercentageDiscountStrategy(10.0)); // 10% off
System.out.println("Price after 10% discount: " + cart.checkout());
cart.setDiscountStrategy(new FixedAmountDiscountStrategy(20.0)); // $20 off
System.out.println("Price after $20 discount: " + cart.checkout());
}
}
- 优点:
- 符合开闭原则: 可以轻松添加新的策略,而无需修改上下文或客户端代码。
- 避免条件语句: 消除了复杂的条件判断。
- 算法复用和独立测试: 每个策略类可以独立测试和复用。
- 缺点:
- 客户端必须了解所有策略: 客户端需要知道不同策略的区别,并负责选择和使用正确的策略。
- 类数量增加: 如果策略很多,会产生大量的小类。(可与工厂模式结合缓解)
2. 函数式实现(Java 8+ Lambda / Method Reference)
如果策略接口是一个函数式接口(只包含一个抽象方法),可以利用Java 8的Lambda表达式或方法引用来极大地简化实现,避免创建大量具体的策略类。
// Strategy Interface remains the same (it's a functional interface)
@FunctionalInterface
public interface DiscountStrategy {
double applyDiscount(double originalPrice);
}
// Context remains the same
public class ShoppingCart {
private DiscountStrategy discountStrategy;
// ... other fields and methods
public void setDiscountStrategy(DiscountStrategy discountStrategy) {
this.discountStrategy = discountStrategy;
}
}
// Client uses Lambdas or Method References
public class Client {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart(100.0);
// Using a lambda expression
cart.setDiscountStrategy(price -> price * 0.9); // 10% off
System.out.println("Price with lambda discount: " + cart.checkout());
// Using a method reference (if a method exists elsewhere)
// cart.setDiscountStrategy(this::calculateDiscount);
// Using a variable for reuse
DiscountStrategy fixedDiscount = price -> {
double result = price - 20;
return result > 0 ? result : 0;
};
cart.setDiscountStrategy(fixedDiscount);
System.out.println("Price with fixed discount: " + cart.checkout());
}
}
- 优点:
- 极其简洁: 无需为每个策略创建单独的类,代码更紧凑。
- 灵活直观: 策略逻辑可以在使用处就地定义。
- 缺点:
- 局限性: 适用于简单的、无状态的策略。如果策略需要复杂的配置或多个方法,标准的类实现仍然更合适。
- 可读性: 复杂的逻辑写在Lambda中可能降低可读性。
六、最佳实践
- 优先使用组合而非继承: 策略模式是“组合优于继承”原则的经典案例。上下文拥有(has-a)一个策略,而不是是(is-a)某种策略。
- 与工厂模式结合: 当策略的选择逻辑比较复杂,或者不希望客户端直接依赖具体策略时,可以引入一个策略工厂。客户端只需要告诉工厂所需策略的“类型”或“key”,由工厂负责创建并返回正确的策略对象。这进一步解耦了客户端和具体策略。
public class DiscountStrategyFactory { private Map<String, DiscountStrategy> strategies = new HashMap<>(); public DiscountStrategyFactory() { strategies.put("NO_DISCOUNT", new NoDiscountStrategy()); strategies.put("PERCENTAGE_10", new PercentageDiscountStrategy(10)); // ... register more strategies } public DiscountStrategy getStrategy(String discountCode) { DiscountStrategy strategy = strategies.get(discountCode); if (strategy == null) { throw new IllegalArgumentException("Unknown discount code"); } return strategy; } } - 考虑使用枚举策略 (Enum Strategy): 如果策略是固定的、有限的,并且不需要在运行时动态添加,可以使用枚举来实现策略模式。每个枚举实例代表一个策略,并实现策略接口。
public enum DiscountStrategyEnum implements DiscountStrategy { NO_DISCOUNT { @Override public double applyDiscount(double price) { return price; } }, PERCENTAGE_10 { @Override public double applyDiscount(double price) { return price * 0.9; } }; // ... other enum strategies } - 明确与状态模式的区别:
- 策略模式: 客户端主动选择策略。策略之间通常是独立的,不关心状态转换。目的是灵活替换算法。
- 状态模式: 状态转换通常由内部状态自动触发,客户端不感知。状态对象知道下一个可能的状态。目的是管理状态相关的行为。
七、在开发中的演变和应用
策略模式的思想是现代软件架构中可插拔架构和依赖注入的核心:
- Spring Framework 的依赖注入 (DI): Spring的IoC容器本质上是一个巨大的策略模式应用。你定义一个接口(策略),并可能有多个实现(具体策略)。通过
@Autowired注入接口时,Spring根据配置(如@Primary,@Qualifier) 或Profile来决定注入哪个具体的策略实现。这使得业务代码只依赖于接口,实现可以轻松替换。 - Java Collections.sort() 和 Comparator:
Comparator接口就是一个策略接口,用于定义排序算法。Collections.sort(list, comparator)方法接受一个Comparator策略对象,从而允许客户端在运行时指定排序规则,而不是使用元素默认的Comparable实现。这是策略模式的教科书级案例。 - Java Servlet 过滤器 (Filter) 和 Spring 拦截器 (Interceptor): 虽然结构上更偏向责任链,但其可插拔的思想与策略模式相通。你可以提供不同的过滤/拦截策略,并配置它们应用到Web请求上。
- 支付网关集成: 系统定义统一的支付接口 (
PaymentService),而支付宝、微信支付、Stripe等分别提供实现。通过配置决定使用哪个支付策略,新增支付方式只需添加新实现,无需修改核心业务逻辑。
八、真实开发案例(Java语言内部、知名开源框架、工具)
-
java.util.Comparator:- 这是JDK中最经典的策略模式应用。
- 策略接口:
Comparator<T> - 具体策略: 任何实现了
Comparator的类,或者通过Lambda表达式、方法引用创建的 comparator。 - 上下文:
Collections.sort(List<T> list, Comparator<? super T> c),List.sort(Comparator<? super T> c),Arrays.sort(T[] a, Comparator<? super T> c)等方法。 - 客户端: 调用排序方法的代码,负责提供具体的比较策略。
-
Spring Framework的
PlatformTransactionManager:- Spring的事务抽象是策略模式的完美体现。
- 策略接口:
PlatformTransactionManager(定义了getTransaction,commit,rollback等方法)。 - 具体策略:
DataSourceTransactionManager(用于JDBC),JpaTransactionManager(用于JPA),JtaTransactionManager(用于JTA分布式事务),HibernateTransactionManager等。 - 上下文: Spring的事务拦截器和管理器,它们只依赖于
PlatformTransactionManager接口。 - 客户端: 通常是Spring框架本身,根据你的配置(如
@Transactional, XML)来选择具体的事务管理器策略。
-
Spring Security 的
AuthenticationManager:- Spring Security 的认证过程也使用了策略模式。
- 不同的认证方式(表单登录、JWT、OAuth2、LDAP)可能对应不同的
AuthenticationProvider(策略)。AuthenticationManager(通常是一个ProviderManager) 会遍历一个AuthenticationProvider列表,直到找到一个能处理当前认证请求的Provider。
-
Java 的
TimeZone类 (概念上):- 虽然不像前几个例子那样有明确的接口,但
TimeZone.getTimeZone(String ID)方法允许你通过ID(如 “GMT+8”, “America/New_York”)获取不同的时区对象,每个对象计算时间偏移的规则不同,这也体现了策略选择的思想。
- 虽然不像前几个例子那样有明确的接口,但
九、总结
| 方面 | 总结 |
|---|---|
| 模式类型 | 行为型设计模式 |
| 核心意图 | 定义一系列算法,封装它们,并使它们可以互相替换,让算法独立于使用它的客户端。 |
| 关键角色 | 策略接口(Strategy), 具体策略(ConcreteStrategy), 上下文(Context) |
| 核心机制 | 1. 抽象接口: 定义算法族。 2. 封装实现: 每个算法独立成类。 3. 委托调用: 上下文将请求委托给当前策略对象。 |
| 主要优点 | 1. 完美符合开闭原则,易于扩展新算法。 2. 消除条件分支,代码清晰。 3. 算法可复用、可独立测试。 4. 运行时灵活切换算法。 |
| 主要缺点 | 1. 客户端必须了解策略差异(可与工厂模式结合缓解)。 2. 策略类数量可能增多(可用Lambda缓解)。 3. 增加了对象和间接调用的开销(通常可忽略)。 |
| 适用场景 | 系统需要在多种算法中选择;需要避免暴露复杂算法细节;有大量条件判断语句。 |
| 实现选择 | 标准类实现: 功能强大,适合复杂策略。 函数式实现 (Lambda): 简洁灵活,适合简单策略。 |
| 最佳实践 | 结合工厂模式选择策略;优先使用组合;明确与状态模式的区别。 |
| 现代应用 | 依赖注入 (DI) 的理论基础,可插拔架构的核心思想。 |
| 真实案例 | Java Comparator (经典),Spring PlatformTransactionManager (工业级),支付网关集成。 |
策略模式是应对算法变化和实现灵活性的终极武器。它通过将算法抽象化,使得系统架构更加清晰、灵活和健壮。其“组合优于继承”的思想深刻影响了现代框架的设计(如Spring),是实现开闭原则和依赖倒置原则的关键技术。掌握策略模式,意味着你掌握了构建可扩展、可维护系统的核心设计能力。从简单的排序比较器到复杂的事务管理,策略模式无处不在,是架构师和高级开发者工具箱中不可或缺的工具。
本文来自博客园,作者:NeoLshu,转载请注明原文链接:https://www.cnblogs.com/neolshu/p/19120412

浙公网安备 33010602011771号