策略模式初探

引入 其实介绍设计模式本身并不难,难的在于其该如何在具体的场景中合适的使用,上周在介绍了Shiro之后,我原本打算趁热打铁介绍一下责任链模式,但是责任链模式本身并不复杂,我们接触的也不少,比如拦截器链,过滤器链,每个拦截器和过滤都有机会处理请求。
策略模式初探
文章图片

但在怎样的场景中可以引入责任链模式呢, 我其实也在查对应的资料,在B站上看了不少该模式的视频,但是底下的评论倒不是一边到的完全叫好,底下的评论倒是说这是过度设计,不如策略模式,所以我们还是要认清楚一件事情,设计模式可以帮助我们解决一些问题,就像做饭的酱油,合适的使用能够让我们的菜变的更美味,但是如果你用不好,酱油放多了或者有些菜不该放酱油,做的菜就会不好吃。这一点我们可以在Shiro(Java领域的安全框架)对Java的加密库设计的评价,可以一窥二三。

The JDK/JCE’s Cipher and Message Digest (Hash) classes are abstract classes and quite confusing, requiring you to use obtuse factory methods with type-unsafe string arguments to acquire instances you want to use.
JDK的密码和MD5相关的类是非常抽象而且让人非常困惑的,要求开发者使用类型不安全的String参数中从简单工厂模式中获取对应的加密实例,这是非常愚蠢的。
【策略模式初探】PS: 我觉得Shiro吐槽的不无道理,下面是JDK中使用MD5算法和Shiro中使用MD5算法的对比,大家可以体会下,也许就会明白为什么Shiro的设计者吐槽JDK在加密库的设计是愚蠢的。
// JDK 中的 private static void testMD5JDK() { try { String code = "hello world"; MessageDigest md = MessageDigest.getInstance("MD5"); byte[] targetBytes = md.digest(code.getBytes()); System.out.println(targetBytes); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } } // 这是Shiro的,我个人觉得Shiro的设计更好 private static void testMD5Shiro() { String hex = new Md5Hash("hello world").toHex(); System.out.println(hex.getBytes()); }

所以我写设计模式的时候基本上会花很大的篇幅介绍这种模式的理念、以及应用场景,我还将UML图逐了出去,我不认为UML图有助于对设计模式的理解,因为看懂UML图本身又需要一定的学习成本, 普通的图一样能表达设计模式的理念,并且不需要更多的预知概念。
为了介绍策略模式,我们要引入这样一个场景(每种设计模式都有自己的场景),假设你有一堆代码if else, 每个判断对应的又是一个执行方法,这些方法目前已经很庞大了, 像下面这样:
public class StrategyDemo { public void strategy(){ if (条件A){ doA(); }else if (条件B){ doB(); }else if (条件C){ doC(); }else if (条件D){ doD(); }else if (条件E){ doE(); } } }

这里我们又假定满足条件的方法处理的业务已经不少了, 这个StrategyDemo的文件已经很可观了,而且新的需求还在持续涌入,也就是说还会不断的加入else if, 那该怎么降低StrategyDemo这个类的复杂度呢,一个文件太大也不利于后面人的维护,增加其他开发者的阅读成本,我们可以将其类比到微服务架构上, 业务在演进的过程中,代码量不断的上升,服务需要面对的请求量也在不断上升,为了加快开发速度、增强服务的请求能力,我们将单体服务拆分成了微服务:
策略模式初探
文章图片

我们这里简单介绍一下Module,JDK 9之前,JDK原生给我们提供的管理代码的单元是包,包里面放类,我们借助maven的父子工程来实现模块化概念:
策略模式初探
文章图片

本质上也是一种拆分,我们的天性就是喜欢简单的东西,那如何降低StrategyDemo这个类的复杂度呢,将这个类的体积减小一点呢,策略模式给出的答案是更进一步的单一职责,每个类分摊一种方法,这样就将复杂度分摊到几个类身上,那具体怎么做呢?将判断和行为定义抽取到父类中,哪个子类满足对应的条件,由哪个子类来执行。一个比较经典的例子就是对接各种短信平台,不同的短信平台有不同的行为,上面的if else显然是不利于我们扩展的,这正是策略模式大显身手的场景:
public abstract class AbstractSmsStrategy {/** * 不同短信平台的发送短信的方式不一样 * 有的给sdk,有的用HTTP,所以这里用抽象方法 * @return */ public abstract boolean sendSmsStrategy(String mobile,String content); /** * support 相当于if,哪个子类满足该条件,哪个子类发送短信 * @return */ public abstract boolean support(String name); }public class AliSmsStrategy extends AbstractSmsStrategy{/** * 对应的方法 * @return */ @Override public boolean sendSmsStrategy(String mobile,String content) { System.out.println("阿里发送短信平台成功....."); return false; }@Override public boolean support(String name) { return "ali".equals(name); } }public class TencentSmsStrategy extends AbstractSmsStrategy{ @Override public boolean sendSmsStrategy(String mobile, String content) { System.out.println("腾讯发送短信平台成功....."); return false; }@Override public boolean support(String name) { return "tencent".equals(name); } } public class SmsStrategyContext {public AbstractSmsStrategy abstractSmsStrategy; public SmsStrategyContext(AbstractSmsStrategy abstractSmsStrategy) { this.abstractSmsStrategy = abstractSmsStrategy; }publicboolean sendSms(String mobile,String content){ boolean result = abstractSmsStrategy.sendSmsStrategy(mobile, content); return result; } } public class StrategyDemo { public static void main(String[] args) { AbstractSmsStrategy abstractSmsStrategy = new AliSmsStrategy(); SmsStrategyContext smsStrategyContext = new SmsStrategyContext(abstractSmsStrategy); smsStrategyContext.sendSms("",""); } }

大多数讲策略模式其实都是这么讲的,定义具体的抽象策略由子类来实现, 然后再new 出一个具体的策略,给上下文,由上下文来执行具体的发短信策略。
策略模式初探
文章图片

那我为啥要通过一个上下文来调对应的方法, 我直接用抽象的基类调对应的策略方法不行吗?像下面这样:
public class StrategyDemo { public static void main(String[] args) { AbstractSmsStrategy abstractSmsStrategy = new AliSmsStrategy(); abstractSmsStrategy.sendSms("",""); } }

那是因为这个上下文在当前的场景下目前还是比较多余的, 上下文在这个模式中就是屏蔽人和具体策略的交互, 如果你对接过短信平台,你会发现短信平台,短信平台通常还要指定地址、账号、密码。那么我们可以在上下文中加载一些配置或者做一些预处理,存储短信的发送日志,上下文存在的意义就是尽可能的屏蔽加载策略的细节。我们可以将上下文进行改造,大家可以在这个例子中体会到上下文存在的意义。
public class SmsStrategyContext {public AbstractSmsStrategy abstractSmsStrategy; public SmsStrategyContext(AbstractSmsStrategy abstractSmsStrategy) { this.abstractSmsStrategy = abstractSmsStrategy; }/** * 其实对应的操作还有很多。 * 发短信的时候,我们一般是将模板存储在数据库中. * 业务方在调用的时候,我们从数据库中取出对应的模板。 * 需要发送连接的时候,还需要将长链接处理成短链接。 * 你看上下文的作用体现出来了吧。 * 通常发送短信还有异步处理,所以我们这里还可以再做一个线程池, * 这样看上下文的作用是不是体现出来了。 * @param mobile * @param content * @return */ publicboolean sendSms(String mobile,String content){ //生成短信日志,这里假装有一个短信日志表. MessageLog messageLog = new MessageLog(); boolean result = abstractSmsStrategy.sendSmsStrategy(mobile, content); // 这里假装执行对应的入库操作 return result; } }

注意这个上下文不必一定要严格按照设计模式中一定是要注入对应的策略, 然后来调用,我们要理解,设置上下文的目的是尽可能的屏蔽执行对应策略的细节,让调用方仅提供手机号和模板ID,就可以实现短信的发送。 模板方式其实还可以做进一步的变型,在这个例子中我们还要显式的new 出来对应的策略,那不能不能new啊,我给你一个标识符,你来根据标识符来加载对应的策略不行吗? 让调用方少干点事,有效的解耦合。那其实上面的策略模式可以改造成下面这样:
public class SmsStrategyContextPlus {private static final List SMS_STRATEGY_LIST = new ArrayList<>(); /** * 加载所有的短信策略 */ static { AbstractSmsStrategy aliSmsStrategy = new AliSmsStrategy(); AbstractSmsStrategy tencentSmsStrategy = new TencentSmsStrategy(); SMS_STRATEGY_LIST.add(aliSmsStrategy); SMS_STRATEGY_LIST.add(tencentSmsStrategy); } /** * 略去产生日志, 长链转短链。 * 线程池等操作 * @param mobile * @param templateKey * @param platform 哪个平台,如果怕调用方写错,其实这里可以做成枚举值 * platform 也可以从配置中加载,在前台选中启用哪个短信平台。 * 这里其实可以做的很灵活。 * @return */ public boolean sendSms(String mobile,String templateKey,String platform){ boolean sendResult = false; for (AbstractSmsStrategy abstractSmsStrategy : SMS_STRATEGY_LIST) { if (abstractSmsStrategy.support(platform)){ sendResult = abstractSmsStrategy.sendSmsStrategy(mobile,templateKey); } } return sendResult; } }

在Spring中其实我们可以这么用:
@Component public class SmsStrategyContextSpringDemo { @Autowired private List strategyList ; public boolean sendSms(String mobile,String templateKey,String platform){ boolean sendResult = false; for (AbstractSmsStrategy abstractSmsStrategy : strategyList) { if (abstractSmsStrategy.support(platform)){ sendResult = abstractSmsStrategy.sendSmsStrategy(mobile,templateKey); } } return sendResult; } }

注意这个上下文的作用,对调用方屏蔽掉执行对应的策略的细节,不必太过拘泥其实现,我个人的看法是只要是对调用方屏蔽了执行对应策略的细节,就是一个Context。
在shiro中的使用 上面讲的策略模式其实在Java领域里面的安全框架Shiro中也有所体现,Shiro帮我们写好了登录代码,所谓的登录就是拿前台提交的登录信息和数据源中的用户信息进行比对,数据源其实有很多种, JDBC、LDAP等等。Shiro 抽象出了Realm接口来统一的从数据源中获取信息,Shiro支持多数据源,那Shiro是怎么根据对应的数据源来加载用户信息的呢?其实也是通过策略方式来分发的, AuthenticatingRealm 有两个抽象方法:
  • doGetAuthenticationInfo 交给对应的realm来实现获取登录信息
  • supports 是否由当前realm来获取数据源
策略模式初探
文章图片

策略模式初探
文章图片

总结一下 我们通常希望类小一点,这样阅读起来就快一点,假如你有在一个类里面放了很多判断,这个类已经很庞大了,不妨尝试用策略模式来进行分发,来降低代码的复杂度,这样维护起来也容易,或者就是预判,像是系统里面接入短信平台一样,短信平台商有很多,即使你的系统已经支持了知名的短信服务商了,但是还不够,客户希望用他自己的短信服务商,避免每接入一个短信服务商都要加一个判断,我们可以采取策略方式,接入一个短信服务商,继承父类,实现具体的发短信操作即可,通用操作像生成短信日志、异步发送我们放在上下文里面进行操作,也不用编写重复代码,要注意体会上下文的思想,上下文存在的意义在于降低客户端调用的难度,屏蔽加载对应策略的细节,简单点尽量简单点。某种程度上,我理解设计模式是一种预判或者降低代码复杂度的一种方式,在策略模式中就是分发代码,将满足条件的行为移动到对应的子类中,尽可能的减少改动,策略模式的判断,当前if else很多了,即使不会再增加判断,在单一文件中拥挤太多代码,也让读懂代码的人头疼,为了降低复杂度,我将这些代码按照单一职责进行分发。预判的是当前是只有一个if else,但是随着业务的发展,会接入越来越多的短信平台,所以这里用上下文做通用操作,接入一个短信平台实现对应的方法,减少代码的改动量,让代码的可维护性和可阅读性更高。
参考资料
  • Java 9的模块化--壮士断"腕"的涅槃 https://zhuanlan.zhihu.com/p/...

    推荐阅读