关于防御式编程的一点思考

原创
2017/09/18 22:35
阅读数 869

上周看了代码大全里面的防御式编程那一章,颇有感触,结合平日里的编程实践,对自己的一些编程方式与想法记录一下,也探讨一下如何写出更安全、更有可读性的代码。

防御式编程

定义

防御式编程这一概念来自防御式驾驶,即要建立起这样一种思维:你永远也不知道另一位司机将要做什么,时刻提高警惕,这样才能在其他司机做出危险动作时不受伤害。防御式编程的主要思想是子程序应该不因传入错误数据而被破坏,哪怕是由其他子程序产生的错误。以怀疑的眼光看待任何外部数据,建立自己的准入机制,这样才能使自己的程序更加健壮。

保护数据免遭非法数据的破坏

  • 检查所有外部输入的数据,包括外部文件,读取的用户输入等
  • 检查子程序的输入参数
  • 决定如何处理错误的输入数据

防御式编程的理念就是在一开始就不要引入错误。

错误处理

在我看来使用assert关键字来判断数据的合法性是不合适的,这样的语句数量多了散落在程序的各处,会导致线上与线下环境的不一致。而且assert在断言失败后抛出error,使程序终止运行,这在企业编码实践中是不可行的,因此直接来看书中的错误处理一节。

在碰到错误后,如何处理呢?

  • 返回中立的值。在某些场景下是很有用的,在Java中可以直接用 Optional类的API来做相关处理
  • 换用下一个正确的数据。书中给出的例子是体温计,但在我们平常开发中,这种情况不怎么常见。
  • 返回与前次相同的数据。
  • 换用最近的合法值
  • 记录到日志文件中。这个是必须的,需要跟其他的手段结合起来一起用。
  • 返回一个错误码。
  • 返回一个错误信息。 这两个通常我们结合起来使用,在rpc调用或与前端交互时,我们需要定义通用的格式来表示请求是否成功。
  • 用妥当的方式在局部处理错误。这个要看具体的设计,具体产品的容错性。

既然有这么多的错误处理选择,我们需要在高层对错误处理进行一定的设计和规范,保证整个程序采用一致的错误处理方式。比如在遇到非法数据时,按照统一格式返回错误码和错误信息,并记录到日志中;遇到某些不可知原因抛出异常,就要约到在哪个层次来处理这些异常,并确保异常得到了处理。

异常

异常也是我们工具箱中一个有力的工具,但是不能滥用异常,需要审慎明智的使用。

  • 用异常通知程序的其他部分,发生了不可忽略的错误。
  • 只有在真正例外情况下才抛出异常。
  • 不能用异常来推卸责任。
  • 避免在构造函数和析构函数中抛出异常,除非在同一地方将其捕获。
  • 在恰当的抽象层次抛出异常。意为抛出本身同一层次的异常,譬如在从文件中读取员工id时,不要抛出FileNotExistedException等异常,可以封装成EmployeeNotAvailableException再向上抛出
  • 在异常消息中加入关于导致异常发生的全部消息。也就是在构造异常时,一定要把cause带上。
  • 避免使用空的catch。捕获异常不做任何处理是最无耻的行为,会导致后续的维护异常艰难。
  • 创建一个集中的异常报告机制
  • 把异常的使用标准化。创建项目异常类,规定什么时候局部处理异常,什么时候向上抛出,定义全局的异常报告机制。
  • 考虑异常的替换方案。尽可能不使用异常,而使用错误处理机制来处理常见的错误。

异常在有些时候可以简化很多需要处理的流程,但我们还是需要根据上面的这些原则来谨慎的使用异常。

对防御式编程保持防御姿态

不要过度防御,过多的检查会使得项目变得臃肿,主线处理逻辑不清晰。

对防御式编程的一点实践

  • 对所有的输入参数进行合法性校验
  • 对所有函数的返回值进行非空、错误码等校验
  • 对函数的处理流程就行校验,比如说必须满足同一任务不能重复处理等等。

好处:能写出很健壮的程序,如果能在编码阶段把所有的异常情况都考虑进去,那么程序的崩溃可能性是很小的,bug减少到最小。 坏处:破坏了程序的主线处理逻辑,错误处理代码散落在函数的各处,让代码可读性下降。

举个例子,用户在线支付,我们可能的处理逻辑:

public String pay(Long money, Long userId) {

    if (money == null || money < 0) {
        return "无效金额";
    }
    if (userId == null) {
        return "无效用户";
    }

    User user = userService.getUserById(userId);
    if (!user.isValid()) {
        return "无效用户";
    }

    Account account = accountService.getAccount(user);
    if (account == null) {
        return "用户还未开通账户";
    }
    if (account.getBalance() < money) {
        return "账户余额不足";
    }
    boolean result = account.reduceBalance(money);
    if (!result) {
        logger.info("账户扣款失败");
        throw new AccountBalanceReduceFailException();
    }
    return "success";
}

这里面其实我们的主线逻辑就是获取用户->获取用户账户->扣减余额,但是由于充斥了过多的错误处理代码,使得各个部分割裂开了。

Java8中的Optional可以让我们很好的处理NULL值,但是对这种情况似乎也没有太多办法,因为我们需要的不仅仅是处理结果,对问题产生的失败原因也是我们所关心的。

有一个与Exception结合的办法,可以约略的来使流程清晰一点点,异常可以由顶层来处理,也可以在内部处理

public String pay(Long money, Long userId) {

    if (money == null || money < 0) {
        return "无效金额";
    }
    if (userId == null) {
        return "无效用户";
    }
    try {
        User user = Optional.ofNullable(userService.getUserById(userId))
                .filter(User::isValid)
                .orElseThrow(() -> new RuntimeException("无效用户"));

        Account account = Optional.ofNullable(accountService.getAccount(user))
                .orElseThrow(() -> new RuntimeException("用户还未开通线上账户"));
        
        if (account.getBalance() < money) {
            return "账户余额不足";
        }
        boolean result = account.reduceBalance(money);
        if (!result) {
            logger.info("账户扣款失败");
            throw new AccountBalanceReduceFailException();
        }
    } catch (Exception e) {
        logger.info("Something's wrong", e);
        return e.getMessage();
    }
    return "success";
}

当然可以看出,为了这么一点点的可读性,处理的方式并不优雅,甚至引入了异常,在判断账户余额处也无法优雅处理。而且处理的过程也并不连贯,由于需要在很多地方返回错误信息,而Optional类并没有提供更好的处理方式,我们不得不在每个获取外部信息的地方都orElseThrow一下。那我们是不是可以扩充一下Optional类来适应我们的情况呢?很可惜Optional是final类,我们只能自己新建一个OptionalAdvance类了,我们在Optional的基础上添加一点功能

//新增函数,为空抛出异常
public OptionalAdvance<T> ifNotPresentThrow(RuntimeException r) {
    Objects.requireNonNull(r);
    if (value == null) {
        throw  r;
    }
    return this;
}

我们再来重写一下上面的例子:

public String pay(Long money, Long userId) {
    if (money == null || money < 0) {
        return "无效金额";
    }
    if (userId == null) {
        return "无效用户";
    }
    try {
        return OptionalAdvance.ofNullable(userService.getUserById(userId))
                .filter(User::isValid)
                .ifNotPresentThrow(new RuntimeException("无效用户"))
                .map(user -> accountService.getAccount(user))
                .ifNotPresentThrow(new RuntimeException("用户还未开通线上账户"))
                .filter(account -> account.getBalance() > money)
                .ifNotPresentThrow(new RuntimeException("用户余额不足"))
                .filter(account -> account.reduceBalance(money))
                .ifNotPresentThrow(new AccountBalanceReduceFailException())
                .map(account -> "success")
                .get();
    } catch (Exception e) {
        logger.info("Something's wrong", e);
        return e.getMessage();
    }
}

这样我们将判断不符合条件的if内化为类操作,结合了异常和Optional相关的类实现了链式操作,无需那么多分支判断。 这只是一个小栗子,可能使用方式也并不太合适,日常编程过程中会有更多的情况需要处理,这就需要我们根据实际情况来做出合适的判断,到底是需要使用异常,还是使用分支,或者使用语言提供的一些工具来使一些操作变得更加连贯。

展开阅读全文
打赏
0
1 收藏
分享
加载中
更多评论
打赏
0 评论
1 收藏
0
分享
返回顶部
顶部