如何用最小代价重构你的"判空地狱"

在一些老项目中,咱们应该时常会碰到以下场景(若是碰不到能够不用继续看了):一些类的一些方法体中,有针对同一个变量进行判空的逻辑。 若是这些方法体内须要判空的方法很少,那还好说,若是一旦出现多数方法都要针对一个变量进行判空,那么这些代码每每就难以维护了。java

例如:git

这种大量针对同一个变量进行判空的方法,会带来的缺点以下(仔细品味缺点是理解后续改进的重要基础):编程

  • 多个地方重复这种判空的逻辑,会产生大量重复代码,不易维护。
  • 若是一个类里面大部分状况都是这种代码,那么同事们一般会花更多时间来理解他们,要扩展时也会思考好久
  • 这些判空的逻辑 是没法对 新引入的新方法进行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;
    }
}
复制代码

当咱们使用这个支付系统的时候,确定会有多种使用状况,有些场景用了券,有些场景没有用。例如:bash

public static void main(String[] args) {
        //这里是用券的
        PayProcess p1 = new PayProcess();
        p1.setCouponInfo(new CouponInfo());

        //这里是没有用券的
        PayProcess p2 = new PayProcess();
        p1.setCouponInfo(null);

    }
复制代码

肉眼可见的,咱们的PayProcess要写不少判空的代码。 防护式编程总没有坏处。这也是阿里java开发手册中提到的重要的一点,该判空的要判空,该断定数组越界的要数组越界。思路是没错的,可是相似这样的代码,很容易就陷入了判空地狱。出现咱们文章开头说的哪些缺点。ui

如何重构这部分老代码?让他看起来不是这么糟糕?this

咱们新增一个类(其实主要目的就是在这里统一处理为null的状况):spa

//这里面的逻辑 注意看 其实和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 是否为空的时候 也能够利用这种写法。
相关文章
相关标签/搜索