设计模式(九):策略模式(Strategy Pattern)
软件开发过程中,当实现某一个功能可能需要支持多种算法或者策略(方案),例如电商促销,可以采用直接打折、满减计算、抵扣券、积分兑换 等方式,可以灵活地根据促销场景选择相应的促销方案(策略)。
每种策略都有自己的算法,选择具体的算法(策略)由客户端决定。策略模式可以将责任与算法分离,使得算法扩展更方便,易维护。
策略模式的实现方式在一定程序上可以解决和优化过多条件判断的操作,但这不是主要目的,只是附加效果。
模式定义
策略模式(Strategy Pattern):定义一系列算法(算法家族),并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。
策略模式 属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。
模式分析
策略模式是准备一组算法,并将这组算法封装到一系列的策略类里面,作为一个抽象策略类的子类。策略模式的重心不是如何实现算法,而是如何组织这些算法,从而让程序结构更加灵活,具有更好的维护性和扩展性。
策略模式实现了算法本身(算法实现)与责任(算法使用)的分离,算法的使用由客户端来决定,客户端需要理解所有具体策略类之间的区别,以便选择合适的算法。即分离算法,选择实现。
模式结构
策略模式包含的主要角色:
- 抽象策略类(Strategy):定义了一个公共接口,各种不同的算法以不同的方式实现这个接口,环境角色使用这个接口调用不同的算法,一般使用接口或抽象类实现。
- 具体策略类(Concrete Strategy):实现了抽象策略类定义的接口,提供具体的算法实现。
- 环境(Context):持有一个策略类的引用,最终给客户端调用,Context 负责执行策略。
优缺点
- 优点
- 可以避免多重条件语句不易维护问题。
- 可以提供相同行为的不同实现。
- 提供一系列可重用的算法族,使用继承把算法的公共代码放到父类,避免重复代码。
- 对供了对开闭原则的完美支持,可以在不修改原代码的情况下,灵活增加新算法。
- 把算法使用放到了环境类,算法实现移到具体策略中,实现了使用与实现分离。
- 缺点
- 客户端必须理解所有策略算法的区别,以便选择合适的算法,在一定程度上增加了客户端的使用难度。。
- 策略模式会造成很多的策略类,可以通过使用享元模式在一定程度上减少对象的数量。
注意事项:如果一个系统的策略多于四个,就需要考虑使用混合模式,解决策略类膨胀的问题。
应用场景
- 一个系统需要动态地在几种算法中选择一种,可将每个算法封装到策略类中。
- 一个对象有多种行为,这些行为在这个类的操作中以多个条件语句的形式出现,可用策略类替换这些条件语句。
- 将客户端使用算法与算法本身的实现分离,在具体策略类中封装算法,提高算法的保密性与安全性。
与状态模式区别
策略模式与状态模式有些相似,并且都是行为型模式,但是状态模式是通过状态转移来改变 Context 所组合的 State 对象,而策略模式是通过 Context 本身的决策来改变组合的 Strategy 对象。
所谓的状态转移,是指 Context 在运行过程中由于一些条件发生改变而使得 State 对象发生改变,注意必须要是在运行过程中。
状态模式主要是用来解决状态转移的问题,当状态发生转移了,那么 Context 对象就会改变它的行为;而策略模式主要是用来封装一组可以互相替代的算法族,并且可以根据需要动态地去替换 Context 使用的算法。
模式应用
策略模式的应用还是比较常见的,如计算个人所得税,不同的收入层次计算所得税的算法是不同的;商场促销,有多种结算方式等。
在 Spring Cloud Ribbon 中,有多种负戴均衡策略,提供了默认的策略,也可由用户配置选择策略,详见 Spring Cloud系列(四):客户端负载均衡 Ribbon - 负载均衡策略。
消息中间件对未被消费的消息处理有多种策略,可以丢弃,可以放进死信对列,可以重试等,里面也采用了策略模式。
典型应用
在 JDK 提供的线程池管理器,常使用一个工厂类 Executors 来创建线程池,而 Executors 内部是使用 ThreadPoolExecutor,最终 ThreadPoolExecutor 的构造函数如下:
ThreadPoolExecutor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}- corePoolSize:线程池中的核心线程数量,即使线程空闲,也不会被销毁。
- maximumPoolSize:最多能够创建的线程数量。
- keepAliveTime:当线程池中的线程数量超过 corePoolSize 时,多出的线程等待新任务的时长,即空闲存活时长。
- workQueue:保存未来得及执行的任务的队列(线程池中的线程都有任务在执行时,仍有新任务进来则保存在 workQueue 队列中)。
- threadFactory:创建线程的工厂中。
- handler:当线程池中没有多余的线程来执行任务,并且保存任务的多列也满了(指的是有界队列),对仍在提交给线程池的任务的处理策略。
handler
线程池中的 handler 是 RejectedExecutionHandler 类型,RejectedExecutionHandler 是一个策略接口,处理无法被 执行的任务(线程池没有空余的线程,线程队列已满(指有界队列),对仍在提交给线程池的任务的处理策略)。
1
2
3
4
5
6
7
8
9public interface RejectedExecutionHandler {
/**
* 当 ThreadPoolExecutor#execute 无法接收任务时,ThreadPoolExecutor
* ThreadPoolExecutor 会调用该方法。
* 当没有更多的线程或队列空间来接收任务时,或在执行程序关闭时,可能会发生这种情况。
*/
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}rejectedExecution 策略接口有四个具体策略实现类:
AbortPolicy:直接丢弃任务,并抛出 RejectedExecutionException 异常。
1
2
3
4
5
6
7
8
9
10public static class AbortPolicy implements RejectedExecutionHandler {
public AbortPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
}DiscardPolicy:也是丢弃任务,静默丢弃被拒绝的任务,什么也不做,不抛出错误。
1
2
3
4
5
6
7public static class DiscardPolicy implements RejectedExecutionHandler {
public DiscardPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
}DiscardOldestPolicy:当执行器未关闭时,从任务队列 workQueue 中取出第一个(即最早未被执行)任务并将之丢弃,使 workQueue 有空间存储刚刚提交的任务。
1
2
3
4
5
6
7
8
9
10
11
12public static class DiscardOldestPolicy implements RejectedExecutionHandler {
public DiscardOldestPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
//移除队列头元素
e.getQueue().poll();
e.execute(r);
}
}
}CallerRunsPolicy:在当前线程(调用者线程)中直接执行被拒绝的任务,而不是丢弃任务。
1
2
3
4
5
6
7
8
9
10public static class CallerRunsPolicy implements RejectedExecutionHandler {
public CallerRunsPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
}
ThreadPoolExecutor(Context 角色)
ThreadPoolExecutor 定义了 RejectedExecutionHandler (策略抽象接口)引用,以便在构造函数中可以由外部客户端决定传入的策略。
ThreadPoolExecutor 定义了一个 RejectedExecutionHandler 默认的具体策略(AbortPolicy),在其中两个构造方法中默认引入。
1
private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();
1
2
3
4
5
6
7
8public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}1
2
3
4
5
6
7
8
9public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, defaultHandler);
}
代码示例
Strategy 抽象策略接口
1
2
3
4
5
6
7public interface Strategy {
/**
* 策略方法
*/
void strategyMethod();
}ConcreteStrategy 具体策略类,实现抽象接口中的方法,在方法里定义各自具体的算法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public class ConcreteStrategyA implements Strategy {
public void strategyMethod() {
System.out.println("具体策略A的策略方法被访问");
}
}
public class ConcreteStrategyB implements Strategy {
public void strategyMethod() {
System.out.println("具体策略B的策略方法被访问");
}
}Context 上下文环境,定义了 Strategy 接口引用,在创建 Context 实例时传入具体的策略类,具体策略类调用策略方法。客户端创建 Context 对象,Context 对象负责调用策略方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25public class Context {
private Strategy strategy;
public Context() {
}
public Context(Strategy strategy) {
this.strategy = strategy;
}
public Strategy getStrategy() {
return strategy;
}
public Context setStrategy(Strategy strategy) {
this.strategy = strategy;
return this;
}
//持有算法方法
public void strategyMethod(){
strategy.strategyMethod();
}
}客户端
1
2
3
4
5
6
7
8
9
10
11
12
13
14public class StrategyMain {
public static void main(String[] args) {
Context context = new Context();
Strategy strategyA = new ConcreteStrategyA();
context.setStrategy(strategyA);
context.strategyMethod();
Strategy strategyB = new ConcreteStrategyB();
context.setStrategy(strategyB);
context.strategyMethod();
}
}
注意:策略模式中的上下文 环境 Context 的职责是隔离客户端与策略类的耦合,无须关注具体的策略。但在上面示例中,客户端直接自己指定了具体策略(Strategy strategyA = new ConcreteStrategyA()
),客户端与具体策略了耦合了,而 Context 在这里的作用只是负责调度执行,获取结果,并没有完全起到隔离客户端与策略类的作用。
在实际开发中,可以通过简单工厂模式将具体策略对象的创建与客户端进行隔离,或通过 策略枚举 将 Context 下具体策略类融合在一起,简化代码。
而在 Spring 开发框架中,将具体策略类注册为 Bean,在 Context 中根据外部参数来注入相应的策略 Bean。
计价策略
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37public enum Calculator {
VIP("vip") {
public double calac(double price) {
return price * 0.8;
}
},
MVP("mvp") {
public double calac(double price) {
return price * 0.7;
}
},
COMMON("common") {
public double calac(double price) {
return price * 0.95;
}
};
private String type;
Calculator(String type) {
this.type = type;
}
public String getType() {
return type;
}
public Calculator setType(String type) {
this.type = type;
return this;
}
public abstract double calac(double price);
}客户端调用
1
2
3
4
5
6
7public class StrategyMain {
public static void main(String[] args) {
double result = Calculator.MVP.calac(108.33);
System.out.println(result);
}
}
相关参考
- 策略模式(策略设计模式)详解
- 图说设计模式 - 策略模式
- RUNOOB - 策略模式
- 深入解析策略模式
- JAVA设计模式学习17—策略模式,这篇不错,评论挺有意思。
设计模式(九):策略模式(Strategy Pattern)
http://blog.gxitsky.com/2019/10/06/DesignPatterns-09-Strategy/