设计模式系列:行为型-策略模式(Strategy Pattern)
简介
策略模式是一种行为型设计模式,它定义一系列的算法,并将每一个算法封装起来,使它们可以互相替换。策略模式让算法独立于使用它的客户端而独立变化。
策略模式的适用场景是:多个类只是区别在表现行为的不同,在运行时选择具体要执行的行为对客户隐藏自己的实现细节,彼此相互独立。策略模式最常见的应用场景是利用它来避免冗长的 if-else 或 switch 分支判断。不过,它的作用还不止如此。它也可以像模板模式那样,提供框架的扩展点等等。
当需要定义一系列的算法时,可以将这些算法封装成一个一个的类,然后客户端可以选择使用哪个类的实例,这个选择可以通过配置来完成,无需修改代码。
策略模式的关键在于将算法封装成独立的对象,这些对象可以相互替换,从而使得算法可以独立于使用它的客户端而变化。
结构
策略模式的主要角色如下:
- 抽象策略(Strategy)类:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。
- 具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现或行为。
- 环境(Context)类:持有一个策略类的引用,最终给客户端调用。
案例实现
【例】促销活动
一家百货公司在定年度的促销活动。针对不同的节日(春节、中秋节、圣诞节)推出不同的促销活动,由促销员将促销活动展示给客户。类图如下:
代码如下:
定义百货公司所有促销活动的共同接口
public interface Strategy {
void show();
}
定义具体策略角色(Concrete Strategy):每个节日具体的促销活动
//为春节准备的促销活动A
public class StrategyA implements Strategy {
public void show() {
System.out.println("买一送一");
}
}
//为中秋准备的促销活动B
public class StrategyB implements Strategy {
public void show() {
System.out.println("满200元减50元");
}
}
//为圣诞准备的促销活动C
public class StrategyC implements Strategy {
public void show() {
System.out.println("满1000元加一元换购任意200元以下商品");
}
}
定义环境角色(Context):用于连接上下文,即把促销活动推销给客户,这里可以理解为销售员
public class SalesMan {
//持有抽象策略角色的引用
private Strategy strategy;
public SalesMan(Strategy strategy) {
this.strategy = strategy;
}
//向客户展示促销活动
public void salesManShow(){
strategy.show();
}
}
优缺点
1,优点:
-
策略类之间可以自由切换
由于策略类都实现同一个接口,所以使它们之间可以自由切换。
-
易于扩展
增加一个新的策略只需要添加一个具体的策略类即可,基本不需要改变原有的代码,符合“开闭原则“
-
避免使用多重条件选择语句(if else),充分体现面向对象设计思想。
2,缺点:
- 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。
- 策略模式将造成产生很多策略类,可以通过使用享元模式在一定程度上减少对象的数量。
使用场景
- 一个系统需要动态地在几种算法中选择一种时,可将每个算法封装到策略类中。
- 一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现,可将每个条件分支移入它们各自的策略类中以代替这些条件语句。
- 系统中各算法彼此完全独立,且要求对客户隐藏具体算法的实现细节时。
- 系统要求使用算法的客户不应该知道其操作的数据时,可使用策略模式来隐藏与算法相关的数据结构。
- 多个类只区别在表现行为不同,可以使用策略模式,在运行时动态选择具体要执行的行为。
源码中的应用
JDK
Comparator 接口
Comparator 接口是 JDK 中策略模式的一个典型应用。它允许在不同的排序场景中使用不同的比较算法。例如,在使用 Collections.sort()
进行排序时,你可以传递不同的 Comparator 实现来指定不同的排序规则。
List<String> names = new ArrayList<>();
names.add("John");
names.add("Alice");
names.add("Bob");
// 使用默认的排序规则(字母顺序)
Collections.sort(names);
// 使用自定义的排序规则(根据字符串长度)
Collections.sort(names, (s1, s2) -> s1.length() - s2.length());
java.util.concurrent.ThreadPoolExecutor
ThreadPoolExecutor 是一个用于管理线程池的类,在其构造函数中接受 RejectedExecutionHandler 参数,这就是策略模式的一种应用。RejectedExecutionHandler 定义了线程池无法执行任务时的处理策略,例如将任务加入队列、抛弃任务等。
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 自定义处理策略,例如将任务加入队列
executor.getQueue().offer(r);
}
}
);
java.util.Arrays.sort()
在对数组进行排序时,可以使用 Arrays.sort() 方法,并传入一个 Comparator 来指定排序策略。
Integer[] numbers = {5, 3, 8, 2, 1};
Arrays.sort(numbers, (a, b) -> b - a); // 降序排序
Spring
在Spring框架中,策略模式通常用于解耦业务逻辑,实现可插拔的行为。以下是一些在Spring中应用策略模式的常见场景:
@Qualifier 注解
在Spring中,@Qualifier 注解可与 @Autowired 一起使用,以指定要注入的具体实现类。这种方式可以用于实现策略模式。例如,假设有一个接口 PaymentService
和多个实现类,如 CreditCardPaymentService
、PayPalPaymentService
等,可以根据不同的条件选择不同的支付方式。
public interface PaymentService {
void pay();
}
@Component
@Qualifier("creditCard")
public class CreditCardPaymentService implements PaymentService {
// 实现支付逻辑
}
@Component
@Qualifier("payPal")
public class PayPalPaymentService implements PaymentService {
// 实现支付逻辑
}
@Service
public class PaymentProcessor {
@Autowired
@Qualifier("creditCard")
private PaymentService paymentService;
// 其他代码
}
通过使用 @Qualifier 注解,可以根据具体情况选择不同的 PaymentService 实现类,从而实现了策略模式。
Handler / Processor 模式
在Spring MVC等框架中,Handler / Processor 模式也是策略模式的一种应用。每个请求都由对应的 Handler 处理,而不同的请求类型则由不同的 Handler 来处理。这种方式将请求的处理与具体的实现解耦,使得系统更加灵活和可扩展。
动态 Bean 注册
Spring允许在运行时动态注册 Bean,并根据条件选择不同的实现。这可以用于实现策略模式,根据不同的条件注册不同的 Bean,并在需要时注入相应的 Bean。
@Configuration
public class PaymentConfig {
@Bean
@ConditionalOnProperty(name = "payment.method", havingValue = "creditCard")
public PaymentService creditCardPaymentService() {
return new CreditCardPaymentService();
}
@Bean
@ConditionalOnProperty(name = "payment.method", havingValue = "payPal")
public PaymentService payPalPaymentService() {
return new PayPalPaymentService();
}
}
在上面的示例中,根据配置文件中的 payment.method
属性的值,Spring 将会选择注册不同的 PaymentService 实现类。
Sprng AOP
Spring AOP 使用了策略模式来实现不同类型的通知(advice)。通知是在切点(join point)周围执行的代码,用于在核心业务逻辑执行之前或之后执行附加逻辑。Spring AOP 支持以下几种类型的通知:
- Before Advice: 在切点方法执行之前执行的代码。
- After Returning Advice: 在切点方法执行之后,如果正常返回,则执行的代码。
- After Throwing Advice: 在切点方法执行之后,如果抛出异常,则执行的代码。
- After (Finally) Advice: 无论切点方法是否正常返回或抛出异常,都会执行的代码。
- Around Advice: 包围切点方法的代码,能在切点方法之前和之后执行自定义逻辑。
Spring 使用策略模式来实现这些通知类型的处理,每种通知类型都有对应的策略接口,而具体的通知逻辑则通过实现这些接口来实现。在运行时,Spring 根据配置和需要选择相应的通知策略,并将其织入到目标方法周围。
虽然 Spring AOP 的实现细节可能有所不同,但它的基本原理是通过策略模式来管理和应用不同类型的通知,从而实现面向切面编程的目标。
事务管理
Spring 框架支持多种事务管理策略,包括基于注解的声明式事务管理和编程式事务管理,以及不同的事务传播行为和隔离级别。
在 Spring 的事务管理中,通过 PlatformTransactionManager
接口和其实现类来实现事务管理策略。PlatformTransactionManager
是一个策略接口,定义了对事务的操作,如开启事务、提交事务、回滚事务等。而 Spring 框架提供了多种实现 PlatformTransactionManager
接口的类,每个类代表了一种不同的事务管理策略,比如:
DataSourceTransactionManager
:基于 JDBC 数据源的事务管理策略。JtaTransactionManager
:Java 事务 API (JTA) 的事务管理策略,通常用于容器管理的分布式事务。JpaTransactionManager
:用于管理 JPA(Java Persistence API)的事务。
通过配置文件或者注解,开发者可以选择合适的事务管理策略并将其应用到应用程序中。
例如,通过 XML 配置声明式事务管理:
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
或者通过注解配置:
@Configuration
@EnableTransactionManagement
public class AppConfig {
@Bean
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource());
}
}
这些示例中,DataSourceTransactionManager
就是策略模式的实现,它根据配置来管理基于数据源的事务。Spring 框架将事务管理的实现抽象成接口,并允许开发者根据应用的需要选择合适的策略,这正是策略模式的典型应用。
.Net Core
中间件(Middleware)
ASP.NET Core 中的中间件管道是策略模式的一个经典应用。每个中间件都可以看作是一个处理 HTTP 请求的策略。请求通过管道时,会按照配置的顺序依次通过每个中间件,每个中间件都可以对请求进行处理,或者决定是否将请求传递给下一个中间件。这种设计允许开发者根据需求添加、移除或重新排序中间件,从而实现不同的请求处理逻辑。
认证和授权
在 ASP.NET Core 的安全系统中,认证和授权都采用了策略模式。例如,认证可以配置为使用不同的认证方案(如 Cookie 认证、JWT 认证等),而授权则可以基于不同的策略(如角色、策略、声明等)来决定是否允许用户访问某个资源。这些策略和方案都可以动态配置和替换。
日志记录
.NET Core 的日志系统支持多种日志提供者,如控制台、文件、数据库等。每个提供者都实现了相同的日志接口,允许开发者根据需要选择或添加新的提供者。这种设计使得日志记录的行为可以根据应用程序的需求进行动态配置。
数据格式化
在数据序列化或响应输出时,.NET Core 支持多种格式(如 JSON、XML、CSV 等)。每种格式都有对应的格式器和解析器。当需要序列化或反序列化数据时,框架会根据配置或请求头选择相应的格式化策略。
路由策略
ASP.NET Core 的路由系统允许开发者定义不同的路由策略,如基于属性的路由、基于约定的路由等。这些策略可以根据应用程序的需要进行选择和配置。
依赖注入
在 .NET Core 的依赖注入容器中,策略模式也体现在服务解析的过程中。对于具有多个实现的接口,依赖注入容器可以根据配置或特性选择解析哪个具体的实现。这允许开发者在运行时动态地替换服务的实现。
配置系统
.NET Core 的配置系统支持从多种源加载配置,如 JSON 文件、环境变量、命令行参数等。每种源都有一个对应的配置提供者,这些提供者实现了相同的接口,使得框架能够灵活地处理来自不同源的配置数据。
微信:17873041739