上周看了代码大全里面的防御式编程那一章,颇有感触,结合平日里的编程实践,对自己的一些编程方式与想法记录一下,也探讨一下如何写出更安全、更有可读性的代码。
防御式编程
定义
防御式编程这一概念来自防御式驾驶,即要建立起这样一种思维:你永远也不知道另一位司机将要做什么,时刻提高警惕,这样才能在其他司机做出危险动作时不受伤害。防御式编程的主要思想是子程序应该不因传入错误数据而被破坏,哪怕是由其他子程序产生的错误。以怀疑的眼光看待任何外部数据,建立自己的准入机制,这样才能使自己的程序更加健壮。
保护数据免遭非法数据的破坏
- 检查所有外部输入的数据,包括外部文件,读取的用户输入等
- 检查子程序的输入参数
- 决定如何处理错误的输入数据
防御式编程的理念就是在一开始就不要引入错误。
错误处理
在我看来使用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相关的类实现了链式操作,无需那么多分支判断。 这只是一个小栗子,可能使用方式也并不太合适,日常编程过程中会有更多的情况需要处理,这就需要我们根据实际情况来做出合适的判断,到底是需要使用异常,还是使用分支,或者使用语言提供的一些工具来使一些操作变得更加连贯。