开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 1 天,点击查看活动详情
前言
这篇文文章主要想探讨一下对于策略的开发习惯,会涉及策略模式。我不敢说我理解的就是正确的,所以是探讨,如果有大佬有独特的见解也可以说出来,让我学习学习。
这类型的文章,很难用语言来组织梳理清楚,说得高大上一点就是只可意会不可言传,说得low一点就是不知从何说起。
1.策略模式
但是说到策略,第一反应能想到的是策略模式,我从网上找个例子大概改一下拿来用。
public interface IPayStrategy {
void pay(String params);
}
public class PayContext {
private String name;
private IPayStrategy iPayStrategy;
public PayContext(String name, IPayStrategy iPayStrategy) {
this.name = name;
this.iPayStrategy = iPayStrategy;
}
public void pay(String params) {
iPayStrategy.pay(params, this);
}
}
public class AliPayStrategy implements IPayStrategy {
@Override
public void pay(String params, PayContext payContext) {
// todo 支付宝支付
}
}
public class WxPayStrategy implements IPayStrategy {
@Override
public void pay(String params, PayContext payContext) {
// todo 微信支付
}
}
PayContext payContext1 = new PayContext("ali", new AliPayStrategy());
payContext1.pay("");
PayContext payContext2 = new PayContext("wx", new WxPayStrategy());
payContext2.pay("");
随便写一下,那个例子大概是这样,然后我再配一些官方的用语,“策略模式定义了一些策略的算法,并将每个算法封装起来,而且它们还可以相互替换,策略模式让算法独立与使用它的客户而独立变化。”
结合起来一看,哦!原来策略模式是这样(我们当时大学的时候第一次看就这感觉,当然现在的大学生比我们那时候的牛逼多了)。一看,这个策略模式,有点感觉,但是又说不出来,那什么是策略模式,心里还是觉得似懂非懂,是觉得有那么一点意思,但是好像又不太清楚,感觉说了跟没说一样。那我再举个例子(换肤,正常模式和夜间模式):
public interface ISkinStrategy {
void setSkin();
}
public class NormalStrategy implements ISkinStrategy{
@Override
public void setSkin(Object obj) {
obj.getxxx().setRid(R.id.白天皮肤id);
}
}
public class NightStrategy implements ISkinStrategy{
@Override
public void setSkin(Object obj) {
obj.getxxx().setRid(R.id.夜间皮肤id);
}
}
这是我乱写的,但是感觉和上面的差不多,那这个算策略模式吗?这时候再回去看,感觉又迷茫了,我不是说别人写的文章有问题,我是觉得当你不了解这个东西的设计思想的时候,光去看Demo,是理解不了的。就算你嘴硬说,我看了之后我理解了,我找到了那种感觉,那你能在开发中合理使用出来吗?不是无脑去仿照这个Demo的形式写代码,是合理的使用。
2. 策略参数
写这篇文章是因为遇到两个场景,也算是机缘巧合吧,但是这两个场景确实很贴切“策略”这个词。
第一件事是几个月前,我去面试,面试官问了我一个问题,搜索框输入内容的补全列表存在背压如何处理,我记得我说了几种办法,用最新或者卡住之类的。
但是现在想想,为什么我要说“或者”,都要不行吗?
第二件事是刚刚看的一篇文章,就因为看了那篇文章才有思路要写下这篇文章。是讲flow的背压处理方式,然后作者就分析源码,我就看到(因为看的比较快)大概是提供了几种方法提供给外部调用。
这时候我就想到了“策略参数”这么一个东西,他的做法不是像我之前一样,只根据需求去提供特定的解决办法,而是把能想到的解决办法都提供出来,至于什么场景要使用什么解决方法,调用者自己去决定。
话说到这里,很多人肯定都会联想到一个东西,没错,线程池。
ThreadPoolExecutor的构造方法中有一个参数,RejectedExecutionHandler
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
了解过线程池的都知道,它表示饱和策略,不了解的可以去了解一下,我就不解释基础的知识了。
线程池,加上flow的做法,都有一个很明显的共同点,没错,他们都有一个表示策略的参数。然后再看ThreadPoolExecutor的设计,就会发现,代码确实不复杂,看起来也就那样,但是这设计确实让人觉得牛逼。
首先他有个默认策略,当你不传任何的策略的时候,就会使用默认策略
private static final RejectedExecutionHandler defaultHandler =
new AbortPolicy();
public static class AbortPolicy implements RejectedExecutionHandler {
/**
* Creates an {@code AbortPolicy}.
*/
public AbortPolicy() { }
/**
* Always throws RejectedExecutionException.
*
* @param r the runnable task requested to be executed
* @param e the executor attempting to execute this task
* @throws RejectedExecutionException always
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
}
看得出默认的策略就是抛异常,写底层的就是硬气,我也不和你纠结这么多怎么做好,我直接抛异常,你不想要这个结果就自己处理。
其次,他会定义一些基础的策略提供给你使用。CallerRunsPolicy、DiscardPolicy、DiscardOldestPolicy之类的,比如DiscardPolicy就是什么都不做
public static class DiscardPolicy implements RejectedExecutionHandler {
/**
* Creates a {@code DiscardPolicy}.
*/
public DiscardPolicy() { }
/**
* Does nothing, which has the effect of discarding task r.
*
* @param r the runnable task requested to be executed
* @param e the executor attempting to execute this task
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
}
最后,你也可以去实现这个接口,然后去写自己的策略。所以这样来看,RejectedExecutionHandler的这个设计就非常的好,扩展性好,也方便在不同的场景下的调用。
到这里其实就能很明显的看出ThreadPoolExecutor用的就是策略模式,而且无论从逻辑设计上,还是代码的这个写法上,都非常的符合策略模式。但是开头我举的那个IPayStrategy,是设策略模式吗?我也不知道怎么回答,这个写法确实符合,但是在实际开发中你会这么写吗?或者说你只看这个Demo,你知道在实际开发中你要怎么写吗?总感觉有点牵强。
3. 思考
我觉得这东西,主要还是要理解它的一个设计思想,是基于什么场景设计出来的,为了解决什么样的问题,理解了它的设计思想之后,你再基于它的设计思想去写代码,不管怎么写,应该都没什么大问题。
所以这东西确实不应该是一开始就随便用一个Demo能解散清楚的,这种东西没有规范,如果硬要一套规范,我确实觉得ThreadPoolExecutor能体现得淋漓尽致。
那么什么是策略呢?我觉得是“为了解决某个问题”那方案,就是策略。ThreadPoolExecutor的RejectedExecutionHandler是为了解决饱和的问题。我上面的面试题,是为了解决背压的问题,所以这个场景,适合用策略。针对背压这个问题,如果你希望在某个情况下用最新的内容,某个情况下抛弃超过缓冲区的内容,那很明显,按照RejectedExecutionHandler来写,最合适。
但是你看最上面举的换肤的例子,就是那个ISkinStrategy,它为了解决什么问题?这么一想,这代码强行这么写就是依托答辩了。