我们在web开发中,经常使用数据库表中的字段作为“标记”来表示多个“状态”,比如:
我们就以某宝的在线购物流程为例进行分析。在订单表中,使用zt字段来表示定单的状态,常见的状态就有:
当我们想按条件查询各个类型的订单的时候,只需要一个接口,在前端传入相应的状态码就可以了。在dao层大概也就是通过如下的语句进行查询:
select * from orders where zt = #{zt}
假设有这么几个“不成需求的需求”:
// 2 表示待收货 if(zt == 2){ //按照需求,按照订单发货时间或者预计送达时间排序 }else{ //其他状态的订单,全部按照订单创建时间排序 }
上边这个代码的修改量已经很小了,但是如果我要把和种不同的状态订单全部按照不同的排序方式排序呢?你可能会写如下代码
if(zt==0){ // 待付款的订单处理代码... }else if(zt==1){ }else if(zt==2){ }else if(zt==3){ }else if(zt==4){ }
上边代码太low了,有些小伙伴可能会使用switch进行优化(这里就不写代码了,因为和上边并没有任何区别)。
但是这样会产生什么样的问题呢?
首先可读性,不言而喻,过多的 if-else 代码和嵌套,会使阅读代码的人很难理解到底是什么意思。尤其是那些没有注释的代码。
其次是可维护性,因为 if-else 特别多,想要新加一个分支的时候,就会很难添加,极其容易影响到其他的分支。
笔者曾经看到过一个支付的核心应用,这个应用支持了很多业务的线上支付功能,但是每个业务都有很多定制的需求,所以很多核心的代码中都有一大坨 if-else。
每个新业务需要定制的时候,都把自己的 if 放到整个方法的最前面,以保证自己的逻辑可以正常执行。这种做法,后果可想而知。
其实,if-else 是有办法可以消除掉的,其中比较典型的并且使用广泛的就是借助策略模式和工厂模式,准确的说是利用这两个设计模式的思想,彻底消灭代码中的 if-else。
本文就结合这两种设计模式,介绍如何消除 if-else,并且,还会介绍如何和 Spring 框架结合,这样读者看完本文之后就可以立即应用到自己的项目中。
上边的方式可以完成我们的需求,但是有以下几点不足:
1. 面对“各种各样奇怪的需求”,我们要频繁地修改上边的代码,时间久了,岂不成了渣渣。甚至我们自己都不愿意再去看这些代码了; 2. 如果新增加一个状态表示,也就是给zt字段新的状态含义表示,我们又要添加if-else,这太复杂了。
是的,就是使用策略模式来解决进行太多的状态判断代码就是一个好办法。比如,就上边每一个if-else中的代码抽成一个类或者方法进行处理。
主要的代码我就不写了,因为下边才是我们的主菜,这里说的这种方式只能解决if-else里边的代码复杂问题,将代码进行一定程度上的解耦。但并没有实质地解决if-else的问题,而且这也是网上大多数的解决办法。
如果对策略模式不太了解的小伙伴,可以看下这篇文章,不看也没关系,在下边你会看到怎么用的。策略模式的学习之道
程序设计的一大原则“对扩展开放,对修改关闭”,定义一个接口类,用来查询不同状态的订单列表。如下:
public interface OrderService { /** * 查询对应状态的订单列表 * @param zt * @return */ List getOrderList(String zt); }
然后根据不同的订单状态创建不同的实现类,比如,“待付款”的订单查询类如下:
虽然以下的命名方式属于错误示范,但是却能很好地理解
@Service("orderServiceDfk") // 这个命名确实很不友好,但是我相信你能理解哈 public class PendingParymentOrderSeviceImpl implements OrderService { /** * 查询待付款的订单列表 * * @param zt * @return */ @Override public List getOrderList(String zt) { //这里要利用dao层从数据库中查询出来相应的订单列表 return null; }
看了这两个类的代码,我相信小伙伴们应该能理解了要怎么做了,就是根据前端传来不同的zt值,后台使用不同的类来处理,但是我们可以通过Spring来完全取掉if-else。
我们的controller层代码如下:
@RestController public class OrderController { private String orderServiceBeanNamePrefix = "orderService"; @RequestMapping("getOrderList/{zt}") public List getOrderList(@PathVariable("zt") String zt) { //获取对应的处理状态的bean来处理 //就通过这样一句代码,完全解决了if-else的判断逻辑 OrderService orderService = (OrderService) SpingContext.getBean(orderServiceBeanNamePrefix + zt); List orderList = orderService.getOrderList(); return orderList; } }
上边用了一个工具类,就是从Spring 容器中获取相应的bean,代码如下:
/** * 微信公众号 “小鱼与Java” * * 原理很简单,我们写的类实现这个接口,具体可以查阅Spring生命周期相关内容 * Spring会自动调用其中的setApplicationContext方法,传入Spring容器上下文 * 我们就在这里把Spring上下文保存下来 * * @date 2020/5/18 * @auther Lyn4ever */ @Component public class SpingContext implements ApplicationContextAware { private static ApplicationContext applicationContext; /** * 根据name从Spring容器中获取bean * @param name * @return */ public static Object getBean(String name){ return applicationContext.getBean(name); } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { System.out.println("我保存了Spring上下文"); applicationContext = applicationContext; } }
解决if-else的思路就是使用策略模式,针对不同“状态”的订单,使用不同的类来处理逻辑,这样就可以很好地进行了“解耦”操作。但是,如果新增一个“状态表示 ”,我们就要在主逻辑处添加if-else进行判断要用哪个类来处理。
而解决这个“判断 ”的中使用的if-else就有很多方法:抽象工厂也是一个不错的方法。而我们使用Spring的控制反转同样也可以很好地解决这个问题。
这么做的好处如下:
来看代码实现
工厂模式
为了方便我们从 Spring 中获取 UserPayService 的各个策略类,我们创建一个工厂类:
/** * @author mhcoding */ public class UserPayServiceStrategyFactory { private static Map services = new ConcurrentHashMap(); public static UserPayService getByUserType(String type){ return services.get(type); } public static void register(String userType,UserPayService userPayService){ Assert.notNull(userType,"userType can't be null"); services.put(userType,userPayService); } }
这个 UserPayServiceStrategyFactory 中定义了一个 Map,用来保存所有的策略类的实例,并提供一个 getByUserType 方法,可以根据类型直接获取对应的类的实例。还有一个 Register 方法,这个后面再讲。
有了这个工厂类之后,计算价格的代码即可得到大大的优化:
/** * @author mhcoding */ public BigDecimal calPrice(BigDecimal orderPrice,User user) { String vipType = user.getVipType(); UserPayService strategy = UserPayServiceStrategyFactory.getByUserType(vipType); return strategy.quote(orderPrice); }
以上代码中,不再需要 if-else 了,拿到用户的 vip 类型之后,直接通过工厂的 getByUserType 方法直接调用就可以了。
通过策略+工厂,我们的代码很大程度的优化了,大大提升了可读性和可维护性。
但是,上面还遗留了一个问题,那就是 UserPayServiceStrategyFactory 中用来保存所有的策略类的实例的 Map 是如何被初始化的?各个策略的实例对象如何塞进去的呢?
Spring Bean 的注册
还记得我们前面定义的 UserPayServiceStrategyFactory 中提供了的 Register 方法吗?他就是用来注册策略服务的。
接下来,我们就想办法调用 Register 方法,把 Spring 通过 IOC 创建出来的 Bean 注册进去就行了。
这种需求,可以借用 Spring 中提供的 InitializingBean 接口,这个接口为 Bean 提供了属性初始化后的处理方法。
它只包括 afterPropertiesSet 方法,凡是继承该接口的类,在 Bean 的属性初始化后都会执行该方法。
那么,我们将前面的各个策略类稍作改造即可:
/** * @author mhcoding */ @Service public class ParticularlyVipPayService implements UserPayService,InitializingBean { @Override public BigDecimal quote(BigDecimal orderPrice) { if (消费金额大于30元) { return 7折价格; } } @Override public void afterPropertiesSet() throws Exception { UserPayServiceStrategyFactory.register("ParticularlyVip",this); } } @Service public class SuperVipPayService implements UserPayService ,InitializingBean{ @Override public BigDecimal quote(BigDecimal orderPrice) { return 8折价格; } @Override public void afterPropertiesSet() throws Exception { UserPayServiceStrategyFactory.register("SuperVip",this); } } @Service public class VipPayService implements UserPayService,InitializingBean { @Override public BigDecimal quote(BigDecimal orderPrice) { if(该用户超级会员刚过期并且尚未使用过临时折扣){ 临时折扣使用次数更新(); returen 8折价格; } return 9折价格; } @Override public void afterPropertiesSet() throws Exception { UserPayServiceStrategyFactory.register("Vip",this); } }
只需要每一个策略服务的实现类都实现 InitializingBean 接口,并实现其 afterPropertiesSet 方法,在这个方法中调用 UserPayServiceStrategyFactory.register 即可。
这样,在 Spring 初始化的时候,当创建 VipPayService、SuperVipPayService 和 ParticularlyVipPayService 的时候,会在 Bean 的属性初始化之后,把这个 Bean 注册到 UserPayServiceStrategyFactory 中。
以上代码,其实还是有一些重复代码的,这里面还可以引入模板方法模式进一步精简,这里就不展开了。
还有就是,UserPayServiceStrategyFactory.register 调用的时候,第一个参数需要传一个字符串,这里的话其实也可以优化掉。
比如使用枚举,或者在每个策略类中自定义一个 getUserType 方法,各自实现即可。
好了,不知道这几种干掉if--else的方法学会了没有,如果大家还有更好的办法,可以在下方评论区说出你的想法
觉得写的还不错并且有帮助的,欢迎点赞+关注,转发给更多人学习,谢谢
更多技术好文,关注公众号:Java架构师联盟,谢谢