在开发业务系统代码,我们会经常与异常与错误码打交道,但有时傻傻地分不清楚。编写代码时,到底是使用异常还是返回错误码,一直以来都被程序员们广泛争论。
我们先来看看他们的区别,在编程语言上区别:
在接口定义上区别:
从语言设计工程化来说,异常优于错误码,它有如下优点:
因此很多的书籍建议:用抛出异常代替返回错误代码,但实际运用中,异常和继承一样,经常被滥用的东西,适得其反。
以Java为例,异常系统又分为两种:
受检异常也是我们常纠结说的: 是使用异常还是返回错误码
中的异常,它实际上是业务层需要处理的错误,在开发时可以确定。受检异常设计的出发点很好,严谨的处理这些异常会很好地提高软件的健壮性。因为受检异常它强调:一个方法指定自己一定会抛什么异常,调用者必须一定要处理,或者明确声明继续向上抛。那么整个程序对异常的处理是明确而清晰的。
从实际经验来看,受检异常运用在较底层的SDK上,会使SDK与其使用者之间形成一种契约。但我们开发的大多是业务系统,如果强行套用受检异常真是吃力却不讨人喜欢。我曾接手一个业务系统的代码,看到满眼的异常处理与异常转换,增加了很多其实是对业务逻辑无意义的代码,代码显得非常臃肿而不是那么的Clean。原因是在业务系统中,一个典型的业务接口,一个正常结果,却可能有上十个不正常的错误结果返回,如果每种都定义一个异常,则受检异常要求必要一个个声明,一个个处理。
是人大多都有惰性,一个个处理显然是不现实的,常用的手段就是catch根Exception,吃掉所有异常就无法区分不同的错误结果;要么是直接都往上抛,一般来说,业务系统通常会在最上层有一个收底的异常处理。不同层次的代码对异常的理解不一样,到了最上层收底,它只能是像catch根Exception,对外显示系统错误这类非常笼统的东西,让人非常地迷惑。
有非常多的学者都在讨论受检异常应该去掉,理由大概是:
受检异常要求客户端程序员必要处理异常,但是程序员未必能知道如何处理,而从异常中恢复其实挺困难的,强迫程序员去处理的话是不现实的。常变成了编写什么不做的代码来“处理”它,导致“吞食则有害”的问题。吞掉能通过编译,但也隐藏了问题。
所以即使在JVM系统的Scala与Kotlin,他们在设计上没有继承了Java的受检异常机制,方法上异常的签名变成了可选。在Java系统中,也有像apache commons工具类ExceptionUtils.rethrow把受检异常转成运行异常,也有Lombok的@SneakyThrows注解来自动生成转换代码。这也侧面说明受检异常不受欢迎大有它的市场。
再来看看函数式编程中,对于错误处理通用是Result类型,Rust语言吸纳它。而Go语言则更为简化,直接把错误码作为一种返回类型,异常则是panic。从他们设计上可以看出,把逻辑上的Bug(Java中的RunTimeException)与业务可恢复错误机制(Java中的Exception)区分了,而不是像Java那样统一采用异常机制:
回到开头的问题,对于Java程序,我们可以基于异常机制来传递错误码。基于这种开发方式可以避免大量的重复的try/catch(受检异常检查)或者if/else(错误码的判断)语句,让我们的代码更加简洁。
基于个人的经验建议实施如下:
带来收益:
需要注意是,需要区分接口错误码与内部异常。有哪些需要内部消化的异常,不能直接透传给接口响应,如数据库异常,调用其它服务口超时异常
受检异常在新的语言纷纷抛弃,编译上语法约定并不能根本上解决业务场景上错误处理的健壮性(吞食问题)。业务系统主要还是要设计出合理的错误码,异常可以作为传递错误码的载体。切不可采用复杂的受检异常类型体系来映射到每个业务错误码,这只会让代码过于臃肿。