生活中我们经常会遇到选择问题,比如当我们要出去旅游时,会考虑是自驾、坐飞机还是坐火车前往目的地;或者在烹饪一条鱼时,是考虑清蒸、水煮还是烧烤;又或者商家在对商品促销时,是使用会员累计积分、打折促销或者买赠的方式进行促销。这个时候就需要根据当前不同的的条件,来选择出对应的具体实现方式,这就是策略模式。在实际开发中,策略模式也是会经常使用的一种设计模式。在实现某个功能有多种方式可供选择时,策略模式就能派上用场。
1. 策略模式
1.1 策略模式简介
不知道在座的各位有没有在维护项目代码时,看到过大段大段的 if else 语句,本人曾有幸遇到过一个方法里面大量 if else 嵌套,并且每一个代码块都很长。这种代码通常是第一版开发时判断分支比较少,就是用 if else 来进行处理,随着版本迭代,功能需求的增加,后面为了快速迭代就直接在原来的 if else 语句基础上继续添加判断分支,久而久之就嵌套出了大量的判断分支。这样写法虽然开发的人写着快,但是对于后面代码维护或者新人阅读代码是非常不友好,甚至感到崩溃的。那么当我们在开发中发现判断分支开始膨胀时,这个时候就可以考虑使用策略模式来进行处理。
策略模式定义了一系列功能的实现,而这些功能实现的目的是相同的,能够使用相同的方式来调用所有的实现,只是调用时根据传入不同参数从而获取到不同的实现。使用这样的方式将方法调用和功能实现进行分割,从而达到具体策略之间相互独立,修改、新增策略实现时,不会对策略调用方和其他策略产生影响。
1.2 策略模式结构
在简单了解了策略模式之后,我们来看看他的结构。
策略模式中需要定义一个策略接口 Strategy,使用具体策略类实现该接口来封装具体的策略实现过程。同时还需要给调用方提供一个管理 Strategy 配置类 Context,调用方通过 Context 来调用具体的策略。
/**
* 策略接口
*/
public interface Strategy {
void strategyMethod();
}
/**
* 具体策略 1
*/
public class SpecificStrategy1 implements Strategy {
@Override
public void strategyMethod() {
}
}
/**
* 具体策略 2
*/
public class SpecificStrategy2 implements Strategy {
@Override
public void strategyMethod() {
}
}
/**
* Strategy 配置管理类 context
*/
public class Context {
private Strategy strategy;
public Context(Strategy strategy) {
this.strategy = strategy;
}
public void strategyMethod() {
strategy.strategyMethod();
}
}
调用方代码
public static void main(String[] args) {
// 调用具体策略 1
Context strategyContext1 = new Context(new SpecificStrategy1());
strategyContext1.strategyMethod();
// 调用具体策略 2
Context strategyContext2 = new Context(new SpecificStrategy2());
strategyContext2.strategyMethod();
}
我们可以看到在这样的结构下,调用方只需要在构建 Context 的时候传入具体的策略实现就可以了,调用方也不会在关心具体是怎么实现的。如果要新增策略实现方式,则新增实现类即可,同样修改实现也只需修改对应的实现类。
1.3 策略模式示例
前面对策略模式的概念和结构进行了介绍,可能还是会感觉有点云里雾里,下面就用具体示例来加深理解。
场景模拟:假设你现在有一条鱼准备烹饪,烹饪的方式有清蒸、烤鱼两种做法,那么我们就可以使用策略模式来得到一条烹饪完成的鱼。
首先我们定义一个烹饪策略类(CookStrategy)并定义 cookFish 方法,然后定义两个具体的策略实现类 SteamedFish (水煮鱼)和 GrillFish (烤鱼)。根据结构定义我们还需要给调用方提供一个 CookContext 类来管理具体的策略和方法调用,代码如下:
// 烹饪策略类
public interface CookStrategy {
void cookFish();
}
// 清蒸鱼
public class SteamedFish implements CookStrategy {
@Override
public void cookFish() {
System.out.println("begin to cook steamed fish.");
System.out.println("ding! you have a steamed fish.");
}
}
// 烤鱼
public class GrillFish implements CookStrategy {
@Override
public void cookFish() {
System.out.println("begin to grill fish.");
System.out.println("ding! you have a grill fish.");
}
}
// 策略管理 Context
public class CookContext {
private CookStrategy cookStrategy;
public CookContext(CookStrategy cookStrategy) {
this.cookStrategy = cookStrategy;
}
public void cookFish() {
cookStrategy.cookFish();
}
}
在定义好烹饪方式策略和策略管理之后,就是编写调用方的调用代码了,当我们想做一条清蒸鱼时,只需在 CookContext 的构造方法里面传入具体的清蒸鱼策略即可
public static void main(String[] args) {
CookContext cookContext = new CookContext(new SteamedFish());
cookContext.cookFish();
// 控制台输出
// begin to cook steamed fish.
// ding! you have a steamed fish.
}
同理,当我们想做一条烤鱼时也是传入具体的烤鱼策略
public static void main(String[] args) {
CookContext cookContext = new CookContext(new GrillFish());
cookContext.cookFish();
// 控制台输出
// begin to grill fish.
// ding! you have a grill fish.
}
那么有人可能会有疑问,如果我们添加添加了更多的烹饪方式比如酸菜鱼、水煮鱼等等,那么方式越来越多,客户端所管理的策略也会越来越多,而我们的的具体策略选择不就又回到了调用者身上了吗?这个时候就要使用策略模式的扩展——策略工厂了。
2. 策略工厂
2.1 减轻客户端的负担
当我们添加的烹饪鱼的方式越来越多的时候就需要根据条件来选择具体的烹饪方式,客户端调用代码就会变成:
public static void main(String[] args) {
CookContext cookContext = null;
String cook = "grill";
if ("grill".equals(cook)) {
cookContext = new CookContext(new GrillFish());
} else if ("steamd".equals(cook)) {
cookContext = new CookContext(new SteamedFish());
} else if ("shuizhu".equals(cook)) {
cookContext = new CookContext(new ShuizhuStrategy());
} else {
cookContext = new CookContext(new SuancaiStrategy());
}
cookContext.cookFish();
}
现在能看到客户端的判断越来越复杂,因此结合简单工厂模式(可参考博客:【深入设计模式】工厂模式—简单工厂和工厂方法),将判断语句下沉到 Context 中,调用者便不在进行条件判断,而是只用传入参数即可。
2.2 策略工厂写法
Context 的代码如下:
public class CookContext {
private CookStrategy cookStrategy;
public CookContext(String key) {
if ("grill".equals(key)) {
cookStrategy = new GrillFish();
} else if ("steamd".equals(key)) {
cookStrategy = new SteamedFish();
} else if ("shuizhu".equals(key)) {
cookStrategy = new ShuizhuStrategy();
} else {
cookStrategy = new SuancaiStrategy();
}
}
public void cookFish() {
cookStrategy.cookFish();
}
}
可以看到在 CookContext 的代码中,原来的构造方法参数从 Strategy 改成了具体 Strategy 对应的 key,当我们在构造 CookContext 的时候就会根据 key 构造出对应的具体 Strategy,因此调用者的代码就变成下面这样:
public static void main(String[] args) {
String cook = "grill";
CookContext cookContext = new CookContext(cook);
cookContext.cookFish();
}
还有一种写法就是在调用 cookFish 时再根据 key 选择对应具体策略方法,而在构造 CookContext 时仅仅将所用到的策略根据 key 进行缓存,代码如下:
public class CookContext {
private Map<String, CookStrategy> strategyMap;
public CookContext() {
strategyMap = new HashMap<>();
strategyMap.put("grill", new GrillFish());
strategyMap.put("steamd", new SteamedFish());
strategyMap.put("shuizhu", new ShuizhuStrategy());
strategyMap.put("suancai", new SuancaiStrategy());
}
public void cookFish(String key) {
strategyMap.get(key).cookFish();
}
}
对应的调用者代码也改成如下:
public static void main(String[] args) {
String cook = "grill";
CookContext cookContext = new CookContext();
cookContext.cookFish(cook);
}
以上两种写法都是没问题的,主要根据个人习惯以及实际场景选择即可。
回到开篇提出的大量 if else 问题上,当我们遇到判断分支很多,并且每个分支逻辑复杂时,我们便可以使用策略工厂,将原来每个分支里面的业务代码进行策略封装,同时使用 Context 将判断条件和封装后的策略进行关联。这样做的好处是在将来如果再次新增判断分支时,只需新增策略类即可,调用者也不再与具体策略耦合。并且代码条理和责任会更清晰,每个分支只会关心自己对应的策略,对策略的修改也不会对调用方产生任何影响。
3. 策略模式在框架源码中的应用
3.1 策略模式在 JDK 中的应用
ThreadPoolExecutor 类
在我们创建线程池时,会调用 ThreadPoolExecutor 的构造函数 new 一个对象,在构造函数中需要传入七个参数,其中有一个参数叫 RejectedExecutionHandler handler 也就是线程的拒绝策略。
public 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;
}
传入拒绝策略之后将对象赋给 ThreadPoolExecutor 对象的成员变量 handler,在需要对加入线程池的线程进行拒绝时,直接调用 RejectedExecutionHandler 中的 reject 方法即可,方法内部调用传入 handler 的 rejectedExecution 方法。
final void reject(Runnable command) {
handler.rejectedExecution(command, this);
}
但是 RejectedExecutionHandler 是一个接口,也就是说我们需要传入具体的实现,这里便是使用的策略模式。RejectedExecutionHandler 接口对应 Strategy 接口,下面四种实现类对应具体策略;RejectedExecutionHandler 对应 Context 类,外部调用 RejectedExecutionHandler 的 reject 方法,再由 RejectedExecutionHandler 内部调用具体策略实现的方法。
TreeMap
在创建 TreeMap 对象的时候可以在构造方法中传入 Comparetor 对象来决定 TreeMap 中的 key 是按照怎样的顺寻进行排列。并且 TreeMap 通过提供 compare 方法调用比较器的 compare 方法进行两个参数的比较,因此该处也是使用的策略模式。
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
final int compare(Object k1, Object k2) {
return comparator==null ? ((Comparable<? super K>)k1).compareTo((K)k2)
: comparator.compare((K)k1, (K)k2);
}
4. 总结
策略模式用于在完成相同工作时有多种不同实现的选择上,这些实现都能以相同的方式进行调用,减少方法实现和方法调用上的耦合。在实际开发中使用策略模式不仅能简化代码,而且能够简化我们的单元测试。策略模式将策略的选择交给了调用者,从而让具体策略仅关注自己的实现逻辑。