在一些老项目中,我们应该时常会碰到如下场景(如果碰不到可以不用继续看了):一些类的一些方法体中,有针对同一个变量进行判空的逻辑。 如果这些方法体内需要判空的方法不多,那还好说,如果一旦出现多数方法都要
在一些老项目中,我们应该时常会碰到如下场景(如果碰不到可以不用继续看了):一些类的一些方法体中,有针对同一个变量进行判空的逻辑。 如果这些方法体内需要判空的方法不多,那还好说,如果一旦出现多数方法都要针对一个变量进行判空,那么这些代码往往就难以维护了。
例如:
这种大量针对同一个变量进行判空的方法,会带来的缺点如下(仔细品味缺点是理解后续改进的重要基础):
- 多个地方重复这种判空的逻辑,会产生大量重复代码,不易维护。
- 如果一个类里面大部分情况都是这种代码,那么同事们通常会花更多时间来理解他们,要扩展时也会思考很久
- 这些判空的逻辑 是无法对 新引入的新方法进行null保护的,如果新编写了方法,但是忘记编写null 逻辑,那么null 错误就有可能发生。
来看个具体的例子,电商项目中,我们处理支付总会有一个统一的出入口,支付的行为有许多种,比如常见的就是用券和不用券。
public class PayProcess { private CouponInfo couponInfo; public void setCouponInfo(CouponInfo couponInfo) { this.couponInfo = couponInfo; } //检查支付的合法性 public boolean checkLegitimate() { if (null != couponInfo) { //其实这里主要就是检查一下券有没有过期 return couponInfo.checkLeg(); } //如果没有券 就意味着合法 return true; } //获取实际支付金额 public int getPayValue(int totalValue) { if (null != couponInfo) { //支付总金额 减去 券的金额 自然就是需要支付的金额 return totalValue - couponInfo.getCouponValue(); } return totalValue; } //获取优惠券的类型 public int getCouponType() { if (null != couponInfo) { return couponInfo.getCouponType(); } //如果压根就没有优惠券 这里就返回0 0代表没有优惠券, 实际业务中 我们不能写这种魔法数字 //一定要定义成常量,这里为了演示方便 我就偷懒了 return 0; }}class CouponInfo { public boolean checkLeg() { //实际中 我们会校验券的时间 等等,现在为了演示方便 我就直接返回一个false了 //大家知道意思就好 return false; } public int getCouponValue() { //返回券的实际价值,这里也是为了演示方便 我直接返回一个固定值 return 3; } public int getCouponType() { //返回券的种类,看看是无敌券?还是限定品类的券 等等 //为了演示方便 我直接写一个int值,实际写的时候 一定要写成常量 return 2; }}
当我们使用这个支付系统的时候,肯定会有多种使用情况,有些场景用了券,有些场景没有用。例如:
public static void main(String[] args) { //这里是用券的 PayProcess p1 = new PayProcess(); p1.setCouponInfo(new CouponInfo()); //这里是没有用券的 PayProcess p2 = new PayProcess(); p1.setCouponInfo(null); }
肉眼可见的,我们的PayProcess要写很多判空的代码。 防御式编程总没有坏处。这也是阿里java开发手册中提到的重要的一点,该判空的要判空,该判定数组越界的要数组越界。思路是没错的,但是类似这样的代码,很容易就陷入了判空地狱。出现我们文章开头说的哪些缺点。
如何重构这部分老代码?让他看起来不是这么糟糕?
我们新增一个类(其实主要目的就是在这里统一处理为null的情况):
//这里面的逻辑 注意看 其实和PayProcess 里面当券为null的时候逻辑一样的public class NullCouponInfo extends CouponInfo { public boolean checkLeg() { return true; } //没有券 那券的价值就为0 public int getCouponValue() { return 0; } // 没有券 自然type为0 public int getCouponType() { return 0; }}
然后我们的支付类 就可以清爽很多:
public class PayProcess { private CouponInfo couponInfo; public void setCouponInfo(CouponInfo couponInfo) { this.couponInfo = couponInfo; } //检查支付的合法性 public boolean checkLegitimate() { //其实这里主要就是检查一下券有没有过期 return couponInfo.checkLeg(); } //获取实际支付金额 public int getPayValue(int totalValue) { //支付总金额 减去 券的金额 自然就是需要支付的金额 return totalValue - couponInfo.getCouponValue(); } //获取优惠券的类型 public int getCouponType() { return couponInfo.getCouponType(); }}
最后调用的时候,当遇到券为空的时候 就不要传null作为参数了
PayProcess p3 = new PayProcess(); p1.setCouponInfo(new NullCouponInfo());
你看这样一改完,整个逻辑上就清晰很多,可读性也很好。也没有那么多重复的代码。当然这里还有一个隐患:当我们券需要新增一些方法的时候,我们除了要改CouponInfo 还需要改NullCouponInfo,如果改漏了,那么就会在PayProcess类中 留下隐患。虽然不会有空指针异常,但是往往不会得到我们想要的结果。
针对这种场景,其实我们只要抽象出一个接口即可。让我们的CouponInfo和NullCouponInfo 都继承一个接口:ICoupon(不要再让CouponInfo作为NullCouponInfo的父类),这样所有新增的方法只需要在接口里面增加即可,这样编译的时候就会提示我们在2个子类中都需要实现。从而规避掉上述的隐患。(这里代码比较简单就不演示了)
总结:将所有的null逻辑 替换为一个null object,就可以解决我们文章开始时抛出的问题。有一些要点如下:
- 如果业务逻辑简单的时候,引入null object的模式,反而会增加代码。所以使用者需要自己对整个业务的复杂度有一定的判断。
- 使用null object这种写法,需要写好注释,尤其是重构的过程中,重构结束要通知到调用者,因为你如果引入了这种模式,而同事们都不知道,则他们可能不会为null的情况编写逻辑。当然使用接口可以规避这种情况。
- 即使使用接口,但是整体代码的复杂度会略微上升。
- 不是强校验的null场景,一味的模仿null object 会增加设计的复杂度。
- 不要拘泥于判空这种场景,仔细想想其实很多时候我们判定一个list 是否为空的时候 也可以利用这种写法。

- 0