策略模式

经常在网上看到一些名为“别再if-else走天下了”,“教你干掉if-else”等之类的文章,大部分都会讲到用策略模式去代替if-else。策略模式实现的方式也大同小异。主要是定义统一行为(接口或抽象类),并实现不同策略下的处理逻辑(对应实现类)。客户端使用时自己选择相应的处理类,利用工厂或其他方式。

注解实现

本文要说的是用注解实现策略模式的方式,以及一些注意点。
话不多说,还是以最常 见的订单处理为例。首先定义这样一个订单实体类:

@Data
public class Order {/*** 订单来源*/private String source;/*** 支付方式*/private String payMethod;/*** 订单编号*/private String code;/*** 订单金额*/private BigDecimal amount;// ...其他的一些字段
}

假如对于不同来源(pc端、移动端)的订单需要不同的逻辑处理。项目中一般会有OrderService这样一个类,如下,里面有一坨if-else的逻辑,目的是根据订单的来源的做不同的处理。

@Service
public class OrderService {public void orderService(Order order) {if(order.getSource().equals("pc")){// 处理pc端订单的逻辑}else if(order.getSource().equals("mobile")){// 处理移动端订单的逻辑}else {// 其他逻辑}}
}

策略模式就是要干掉上面的一坨if-else,使得代码看起来优雅且高大上。
现在就让我们开始干掉这一坨if-else。先总览下结构:

1.首先定义一个OrderHandler接口,此接口规定了处理订单的方法。

public interface OrderHandler {void handle(Order order);
}

2.定义一个OrderHandlerType注解,来表示某个类是用来处理何种来源的订单。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Service
public @interface OrderHandlerType {String source();
}

3.接下来就是实现pc端和移动端订单处理各自的handler,并加上我们所定义的OrderHandlerType注解。

@OrderHandlerType(source = "mobile")
public class MobileOrderHandler implements OrderHandler {@Overridepublic void handle(Order order) {System.out.println("处理移动端订单");}
}@OrderHandlerType(source = "pc")
public class PCOrderHandler implements OrderHandler {@Overridepublic void handle(Order order) {System.out.println("处理PC端订单");}
}

4.以上准备就绪后,就是向spring容器中注入各种订单处理的handler,并在OrderService.orderService方法中,通过策略(订单来源)去决定选择哪一个OrderHandler去处理订单。我们可以这样做:

@Service
public class OrderService {private Map<String, OrderHandler> orderHandleMap;@Autowiredpublic void setOrderHandleMap(List<OrderHandler> orderHandlers) {// 注入各种类型的订单处理类orderHandleMap = orderHandlers.stream().collect(Collectors.toMap(orderHandler -> AnnotationUtils.findAnnotation(orderHandler.getClass(), OrderHandlerType.class).source(),v -> v, (v1, v2) -> v1));}public void orderService(Order order) {// ...一些前置处理// 通过订单来源确定对应的handlerOrderHandler orderHandler = orderHandleMap.get(order.getSource());orderHandler.handle(order);// ...一些后置处理}
}

在OrderService中,维护了一个orderHandleMap,它的key为订单来源,value为对应的订单处理器Handler。通过@Autowired去初始化orderHandleMap(这里有一个lambda表达式,仔细看下其实没什么难度的)。这样一来,OrderService.orderService里的一坨if-else不见了,取而代之的仅仅是两行代码。即,先从orderHandleMap中根据订单来源获取对应的OrderHandler,然后执行OrderHandler.handle方法即可。

这种做法的好处是,不论以后业务如何发展致使订单来源种类增加,OrderService的核心逻辑不会改变,我们只需要实现新增来源的OrderHandler即可,且团队中每人开发各自负责的订单来源对应的OrderHandler即可,彼此间互不干扰。

到此,似乎已经讲完了通过注解实现策略模式,干掉if-else的方法,就这样结束了吗?不,真正的重点从现在才开始。

现在回过头看orderHandleMap这个Map,它的key是订单来源,假如,我们想通过订单来源+订单支付方式这两个属性来决定到底使用哪一种OrderHandler怎么办?比如PC端支付宝支付的订单是一种处理逻辑(PCAliPayOrderHandler),PC端微信支付的订单是另外一种处理逻辑(PCWeChatOrderHandler),对应的还有移动端支付宝支付(MobileAliPayOrderHandler)和移动端微信支付(MobileWeChatOrderHandler)。

这时候我们的key应该存什么呢,可能有人会说,我直接存订单来源+订单支付方式组成的字符串不就行了吗?确实可以,但是如果这时业务逻辑又变了(向pm低头),PC端支付宝支付和微信支付是同一种处理逻辑,而移动端支付宝支付和微信支付是不同的处理逻辑,那情况就变成了PCAliPayOrderHandler和PCWeChatOrderHandler这两个类是同一套代码逻辑。我们干掉了if-else,但却造出了两份相同的代码,这是一个作为有强迫症的程序员所不能容忍的。怎么干掉这两个逻辑相同的类呢?

首先,我们可以回顾下,注解它究竟是个什么玩意?不知道大家有没有注意到定义注解的语法,也就是@interface,与定义接口的语法想比,仅仅多了一个@。翻看jdk,可以找到这么一个接口Annotation,如下

/*** The common interface extended by all annotation types.  Note that an* interface that manually extends this one does <i>not</i> define* an annotation type.  Also note that this interface does not itself* define an annotation type.** More information about annotation types can be found in p 9.6 of* <cite>The Java&trade; Language Specification</cite>.** The {@link java.lang.reflect.AnnotatedElement} interface discusses* compatibility concerns when evolving an annotation type from being* non-repeatable to being repeatable.** @author  Josh Bloch* @since   1.5*/
public interface Annotation {// …省略
}

开头就表明了,The common interface extended by all annotation types。说的很明白了,其实注解它就是个接口,对,它就是个接口而已,@interface仅仅是个语法糖。那么,注解既然是个接口,就必然会有相应的实现类,那实现类哪里来呢?上述中我们仅仅定义了OrderHandlerType注解,别的什么也没有做。这时候不得不提动态代理了,一定是jdk在背后为我们做了些什么。

为了追踪JVM在运行过程中生成的JDK动态代理类。我们可以设置VM启动参数如下:

-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true

该参数可以保存所生成的JDK动态代理类到本地。额外说一句,若我们想追踪cglib所生成的代理类,即对应的字节码文件,可以设置参数:

System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "保存的路径");

添加参数后再次启动项目,可以看到我们项目里多了许多class文件(这里只截取了部分,笔者启动的项目共生成了97个动态代理类。由于我的项目是springboot环境,因此生成了许多如ConditionalOnMissingBean、Configuration、Autowired等注解对应的代理类):

那接下来就是要找到我们所关心的,即jdk为我们自定义的OrderHandlerType注解所生成的代理类。由于jdk生成的动态代理类都会继承Proxy这个类,而java又是单继承的,所以,我们只需要找到实现了OrderHandlerType的类即可。遍历这些类文件,发现$Proxy63.class实现了我们定义的OrderHandlerType注解:

public final class $Proxy63 extends Proxy implements OrderHandlerType {private static Method m1;private static Method m2;private static Method m4;private static Method m3;private static Method m0;public $Proxy63(InvocationHandler var1) throws  {super(var1);}// …省略
}

我们知道,jdk动态代理其实现的核心是:

也就是这个构造函数的InvocationHandler对象了。那么注解的InvocationHandler是哪个具体实现呢?不难发现就是:

这个类里面的核心属性,就是那个 memberValues,我们在使用注解时给注解属性的赋值,都存储在这个map里了。而代理类中的各种方法的实现,实际上是调用了 AnnotationInvocationHandler 里的 invoke 方法。

好了,现在我们知道了注解就是个接口,且通过动态代理实现其中所定义的各种方法。那么回到我们的OrderService,为什么不把key的类型设置为OrderHandlerType?就像这样。

private Map<OrderHandlerType, OrderHandler> orderHandleMap;

如此一来,不管决定订单处理器orderhandler的因素怎么变,我们便可以以不变应万变(这不就是我们所追求的代码高扩展性和灵活性么)。那当我们的map的key变成了OrderHandlerType之后,注入和获取的逻辑就要相应改变,注入的地方很好改变,如下:

public class OrderService {private Map<OrderHandlerType, OrderHandler> orderHandleMap;@Autowiredpublic void setOrderHandleMap(List<OrderHandler> orderHandlers) {// 注入各种类型的订单处理类orderHandleMap = orderHandlers.stream().collect(Collectors.toMap(orderHandler -> AnnotationUtils.findAnnotation(orderHandler.getClass(), OrderHandlerType.class),v -> v, (v1, v2) -> v1));}// ...省略
}

那获取的逻辑要怎么实现?我们怎么根据order的来源和支付方式去orderHandleMap里获取对应的OrderHandler呢?问题变成了如何关联order的来源和支付方式与OrderHandlerType注解。

还记得刚才所说的注解就是个接口吗,既然是个接口,我们自己实现一个类不就完事了么,这样就把order的来源和支付方式与OrderHandlerType注解关联起来了。说干就干,现在我们有了这么一个类,

public class OrderHandlerTypeImpl implements OrderHandlerType {private String source;private String payMethod;OrderHandlerTypeImpl(String source, String payMethod) {this.source = source;this.payMethod = payMethod;}@Overridepublic String source() {return source;}@Overridepublic String payMethod() {return payMethod;}@Overridepublic Class<? extends Annotation> annotationType() {return OrderHandlerType.class;}}

在获取对应OrderHandler时我们可以这样写,

public void orderService(Order order) {// ...一些前置处理// 通过订单来源确以及支付方式获取对应的handlerOrderHandlerType orderHandlerType = new OrderHandlerTypeImpl(order.getSource(), order.getPayMethod());OrderHandler orderHandler = orderHandleMap.get(orderHandlerType);orderHandler.handle(order);// ...一些后置处理
}

看起来没什么问题了,来运行一下。不对劲啊,空指针,那个异常它来了。

我们断点打在NPE那一行,

一定是姿势不对,漏掉了什么。那我们就来分析下。orderHandleMap中确实注入了所定义的几个OrderHandler(PCAliPayOrderHandler、PCWeChatOrderHandler、MobileAliPayOrderHandler、MobileWeChatOrderHandler),但是get却没有获取到,这是为什么呢?我们来回忆下,map的get方法逻辑,还记得最开始学习java时的hashCode和equals方法吗?

不知道大家注意到没有,map中key对应的类型是$Proxy63(jdk动态代理给我们生成的),跟我们自己实现的OrderHandlerTypeImpl是不同类型的。
梳理下,在Autowied时,我们放进orderHandleMap的key是动态代理类对象,而获取时,自定义了OrderHandlerTypeI实现类OrderHandlerTypeImpl,而又没有重写hashCode和equals方法,才导致从map中没有获取到我们所想要的OrderHandler,那么,我们把实现类OrderHandlerTypeImpl的hashCode和equals这两个方法重写,保持跟动态代理类生成的一样不就行了吗?

再回看下动态代理给我们生成的这两个方法,前面说了,注解对应的代理类方法调用实际上都是AnnotationInvocationHandler里面的方法,翻看AnnotationInvocationHandler里面的hashCode和equals方法:

private int hashCodeImpl() {int var1 = 0;Entry var3;for(Iterator var2 = this.memberValues.entrySet().iterator(); var2.hasNext(); var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())) {var3 = (Entry)var2.next();}return var1;
}private Boolean equalsImpl(Object var1) {if (var1 == this) {return true;} else if (!this.type.isInstance(var1)) {return false;} else {Method[] var2 = this.getMemberMethods();int var3 = var2.length;for(int var4 = 0; var4 < var3; ++var4) {Method var5 = var2[var4];String var6 = var5.getName();Object var7 = this.memberValues.get(var6);Object var8 = null;AnnotationInvocationHandler var9 = this.asOneOfUs(var1);if (var9 != null) {var8 = var9.memberValues.get(var6);} else {try {var8 = var5.invoke(var1);} catch (InvocationTargetException var11) {return false;} catch (IllegalAccessException var12) {throw new AssertionError(var12);}}if (!memberValueEquals(var7, var8)) {return false;}}return true;}
}

具体的逻辑也比较简单,就不分析了。那我们就按照AnnotationInvocationHandler中的实现,在我们的OrderHandlerTypeImpl中按照相同的逻辑重写下这两个方法,如下

public class OrderHandlerTypeImpl implements OrderHandlerType {// …省略@Overridepublic int hashCode() {int hashCode = 0;hashCode += (127 * "source".hashCode()) ^ source.hashCode();hashCode += (127 * "payMethod".hashCode()) ^ payMethod.hashCode();return hashCode;}@Overridepublic boolean equals(Object obj) {if (!(obj instanceof OrderHandlerType)) {return false;}OrderHandlerType other = (OrderHandlerType) obj;return source.equals(other.source()) && payMethod.equals(other.payMethod());}
}

再次运行看看是否达到我们预期,果不其然,这次可以正常获取到了handler,至此,大功告成。

这样以来,不管以后业务怎么发展,OrderService核心逻辑不会改变,只需要扩展OrderHandler即可。


往期推荐
求求你们了,别再写满屏的 try catch 了!!别再一知半解啦!索引其实就这么回事!你居然还去服务器上捞日志,搭个日志收集系统难道不香么!

直面Java第329期:哪个命令可以监控虚拟机各种运行状态信息?

深入并发第013期:拓展synchronized——锁优化

如果你喜欢本文,

请长按二维码,关注 Hollis.

转发至朋友圈,是对我最大的支持。

点个 在看 

喜欢是一种感觉

在看是一种支持

↘↘↘

别再用if-else了,用注解去代替他吧相关推荐

  1. @order注解_别再用ifelse了,用注解去代替他吧

    来自公众号:咖啡拿铁 策略模式 经常在网上看到一些名为"别再if-else走天下了","教你干掉if-else"等之类的文章,大部分都会讲到用策略模式去代替if ...

  2. @autowired注解 抽象类_别再用ifelse了,用注解去代替他吧

    来自公众号:咖啡拿铁 策略模式 经常在网上看到一些名为"别再if-else走天下了","教你干掉if-else"等之类的文章,大部分都会讲到用策略模式去代替if ...

  3. [JPA知识] 通过@Valu和@ConfigurationProperties注解去拿yml参数?

    以下知识点 我这是在项目发现除了@VALUE 还能通过其他方式去拿参数 这个项目是JPA+BOOT框架 在编写项目代码时,我们要求更灵活的配置,更好的模块化整合.在 Spring Boot 项目中,为 ...

  4. 给失业的互联网人一个思路:别再苦苦找工作了,要去找门槛低、现金流好、天花板低、资本看不上的创业项目,一年也能几百万!...

    失业大潮中的互联网人该何去何从?这大概是许多人在难捱的深夜反复思考的问题. 一位失业很久的网友就在痛苦思索中悟出了适合自己的道路,下面分享给大家,篇幅太长,小编给大家划一下重点. 先说结论:失业的互联 ...

  5. 求你了,别再用 pip 那乌龟的速度去安装库了!

    关注上方"GitHuboy",选择星标, 关键时间,第一时间送达! 作者:pk哥 转自:Python知识圈 学习 Python 的话,仅掌握标准库是远不够的,有很多好用的第三方库我 ...

  6. 让SpringBoot不需要Controller、Service、DAO、Mapper,卧槽!这款工具

    编辑:架构师专栏 跟大家分享一个开源工具,Dataway 是基于 DataQL 服务聚合能力,为应用提供的一个接口配置工具.使得使用者无需开发任何代码就配置一个满足需求的接口.整个接口配置.测试.冒烟 ...

  7. 秒杀场景的九个细节,细思极恐!

    往期热门文章:1.放弃MyBatis!我选择 JDBCTemplate! 2.生成订单30分钟未支付,则自动取消,该怎么实现? 3.Lombok代码"亚健康"元凶? 4.IDEA的 ...

  8. 40 个 Spring Boot 常用注解

    以下文章来源方志朋的博客,回复"666"获面试宝典 作者 | 谭朝红 链接 | ramostear.com 一.Spring Web MVC 与 Spring Bean 注解 Sp ...

  9. 40 个 SpringBoot 常用注解

    以下文章来源方志朋的博客,回复"666"获面试宝典 来源:https://ramostear.com/ 一.Spring Web MVC 与 Spring Bean 注解 Spri ...

最新文章

  1. python 进程间通信效率_Python进程间通信 multiProcessing Queue队列实现详解
  2. 别做菜鸟!莫让摄像头参数迷惑头脑
  3. 【广告技术】如何科学地划分用户群体?在聚类中考虑用户特征和社会关系
  4. php操作xml文件,php xml文件操作代码(一)
  5. new操作符的作用是什么
  6. linux虚拟机怎么显示桌面,虚拟机中如何开启Linux的3d特效桌面?
  7. 结构体定义的三种方法总结
  8. matlab gpa排名,GPA如何决定申请排名多少的大学?(附TOP100大学要求)
  9. 2021期中考试总结
  10. 数据可视化:基本图表
  11. WinRAR压缩软件去除广告
  12. 微信公众号运营助手,可以在手机上回复粉丝留言
  13. 数据采集系统的抗干扰措施
  14. _iq16 c语言,[转载]【转】IQMATH使用
  15. 浙江工商大学计算机考试科目,浙江工商大学(专业学位)计算机技术研究生考试科目和考研参考书目...
  16. springboot easyexcel导出百万数据优化
  17. tensorflow2.10.0+CUDA11.2+cuDNN8.1 for cuda11.2
  18. 2021-06-18四种方法帮你排除电磁兼容测试故障
  19. Pandas 数据挖掘 分析
  20. 可修改性(Mutability)

热门文章

  1. linux读取dmp备份数据打开,Linux 中 Oracle dmp 文件导入导出(转)
  2. python中对字符串进行编码_Python 中的字符串编码
  3. 官网下载的mysql密码忘了怎么办_MySQL密码忘了怎么办?MySQL重置root密码方法
  4. 云南省行政村谷歌图层_云南省基本农田划定工作实施细则
  5. python判断回文数_Python实现判断一个整数是否为回文数算法示例
  6. 计算机网络之网络层:4、ARP协议
  7. 【专栏必读】(计算机组成原理题目题型总结)计理期末考试408统考算机组成原必考题型总结
  8. (软件工程复习核心重点)第六章实现和测试-第四节:集成测试
  9. (计算机组成原理)第七章输入和输出系统-第四节3:I/O方式之DMA方式
  10. 挖漏经验:在密码重置请求包中添加X-Forwarded-Host实现受害者账户完全劫持