最近收到这样的问题:
领域服务做业务逻辑校验时应该返回错误码还是抛出业务异常?
这其实不算是领域服务的问题,而是Java异常处理[1]问题。
之前总结过一次如何处理异常[2]
上面的文章基本上就解决异常相关问题了。
这儿再回顾总结一下:
返回错误码
在异常没有出现时,像C语言是如何处理问题的?
在 C 语言中,错误码的返回方式有两种:一种是直接占用函数的返回值,函数正常执行的返回值放到出参中;另一种是将错误码定义为全局变量,在函数执行出错时,函数调用者通过这个全局变量来获取错误码
// 错误码的返回方式一:pathname/flags/mode为入参;fd为出参,存储打开的文件句柄。 int open(const char *pathname, int flags, mode_t mode, int* fd) { if (/*文件不存在*/) { return EEXIST; } if (/*没有访问权限*/) { return EACCESS; } if (/*打开文件成功*/) { return SUCCESS; // C语言中的宏定义:#define SUCCESS 0 } // ... } //使用举例 int fd; int result = open(“c:\test.txt”, O_RDWR, S_IRWXU|S_IRWXG|S_IRWXO, &fd); if (result == SUCCESS) { // 取出fd使用 } else if (result == EEXIST) { //... } else if (result == EACESS) { //... } // 错误码的返回方式二:函数返回打开的文件句柄,错误码放到errno中。 int errno; // 线程安全的全局变量 int open(const char *pathname, int flags, mode_t mode){ if (/*文件不存在*/) { errno = EEXIST; return -1; } if (/*没有访问权限*/) { errno = EACCESS; return -1; } // ... } // 使用举例 int hFile = open(“c:\test.txt”, O_RDWR, S_IRWXU|S_IRWXG|S_IRWXO); if (-1 == hFile) { printf("Failed to open file, error no: %d.\n", errno); if (errno == EEXIST ) { // ... } else if(errno == EACCESS) { // ... } // ... }
错误码没有性能问题,但带来了维护性,尤其没有了异常堆栈信息,失去了快速定位问题的能力。
抛异常
在OO世界中,更推荐使用异常方式,显得更OO些
Checked Exception
Spring创始人Rod Johnson列举了检查异常几个问题:
1、太多的代码
开发人员不得不捕捉他们无法处理的检查异常
2、难以读懂的代码
捕捉不能处理的异常并重新抛出,没有执行一点有用的功能,反而会使查找实际做某件事的代码变得更困难
3、异常无休止封装
4、易毁坏的方法签名
一旦这么多调用者使用一个方法,添加一个额外的检查异常到该接口上将需要这么多代码被修改。也些违背OCP原则[3]
5、检查异常对接口不一定管用
接口有很多种实现,有些实现会出现异常,但有些是不会出现异常的,比如存储数据,放在文件会抛IO相关异常,但数据是数据库,刚不是此异常。
Runtime Exception
运行期异常被很多大牛接受推荐,但也有弊病,就是调用方需要知道内部实现细节,了解抛出了些什么异常,需在javadoc中给出明确说明。
当然我们现在可以使用AOP技术,对异常进行兜底处理,以防泄漏到用户层。
性能
当使用异常时,性能得确会下降,尤其调用链路很深时,更加明显。但异常总归是少数情况,不影响正常情况的性能。
但有些系统对性能要求比较高,怎么办?如正常情况是百万QPS,就算出现少许异常情况,对系统可用性也带来不小的影响。
怎么办?退回错误码时代
但从设计角度可改良一下,可以不再简单返回错误码,如可以使用vavr的Either
Either<ExceptionMessage,Result> do();
让调用方式来最终确定,当either.isLeft()时,是向上抛异常,还是额外处理。
良好实践
使用检查异常还是运行时异常是个见解问题,不管如何选择,只要团队达成共识,统一规范就可以。
良好的异常,不管是对开发人员,还是运维,用户都应该有全面友好的提示信息
对开发人员,在异常中包含相关信息,使用getMessage()打印日志,方便定位问题
对于用户,可以使用错误代码,字符串比数值语义更明确些。在spring初期代码中,Rod Johnson设计了一个接口ErrorCoded
public interface ErrorCoded { /** Constant to indicate that this failure isn't coded */ public static final String UNCODED = "uncoded"; /** Return the error code associated with this failure. * The GUI can render this anyway it pleases, allowing for Int8ln etc. * @return a String error code associated with this failure */ String getErrorCode(); }
其实也不并是说所有场景都去使用异常,如openapi,最好使用errorCode。
异常与契约
乔新亮指出异常是那些让产品无法履行当初承诺用户的契约的问题。
对于异常设计有5点认知:
1、异常一定要消灭;有异常基本就意味着系统存在风险,一定要消灭异常
2、异常一定要管理:消灭异常是个长期工程,短期要通过管理行为来进行控制
3、对异常的处理水平,会极大影响产品的用户体验:用户规模越大,异常的影响往往越大
4、每个异常都要有具体负责人
5、与终端用户相关的异常,要以最高优先级处理
异常设计包含:异常注册、异常事件触发、异常协作流程以及异常统计。
要关注异常数据、异常发生频次、异常数据的增速和降速。
总结
回到起始问题,对于领域服务,自然OO更好些,抛出特定业务异常,业务语义更加清晰。
性能问题,一是避免发生异常情况,二是通过横向扩展。毕竟业务可运维性更重要。
References
[1]
Java异常处理: https://www.zhuxingsheng.com/blog/java-exception-practice.html
[2]
如何处理异常: https://www.zhuxingsheng.com/blog/java-exception-handling.html
[3]
OCP原则: https://note.youdao.com/