先前已经对异常如何设计,如何实践异常都写了几篇阐述了。再一次从Clean Code角度来谈谈异常的使用。
为什么?是从函数的角度去考虑:
函数要么做什么事,要么回答什么事,但二者不可得兼。也就是修改某对象状态,或者是返回该对象的有关信息。也就是指令与询问分隔开来。
如
boolean set(String attribute,String value);
该函数设置某个指定属性,如果成功,就返回true,如果不存在那个属性,就返回false。
if(set("website","zhuxingsheng.com")){
//
}
但从读者角度考虑一下,它是在问websit属性值是否之前已经设置为zhuxingsheng.com,还是在问websit属性值是否成功设置为zhuxingsheng.com呢?从该行语句很难判断其含义,因为set是动词还是形容词并不清楚。
作者本意是,set是一个动词,但在if语句的上下文中,感觉它是一个形容词。该语句读起来像是在说“如果websit属性值之前已经被设置为zhuxingsheng.com”,而不是“设置websit属性值为zhuxingsheng.com,看看是否可行,然后...”。
要解决这个问题,可以将set函数重命名为setAndCheckIfExists,但这对提高if语句的可读性帮助不大。真正的解决方案是把指令与询问分隔开来,防止产生混淆:
if(attributeExists("website"){
setAttribute("website","zhuxingsheng.com");
}
在《领域服务是抛出异常还是返回错误码》[1],提到过如何编写返回错误码
if(deletePage(page)) == OK){
}
但这样,从指令式函数返回错误码,有些违反指令与询问分隔的规则。
虽然这儿没有像上面的示例一样,引起动词与形容词的混淆,却会导致更深层次嵌套结构
if(deletePage(page) == OK){
if(deleteRefrence(page.name) == OK) {
if (deleteKey(page.name.key()) == OK) {
//
} else {
//
}
} else {
//
}
} else {
//
}
使用异常替代错误码,错误处理代码能从主路径代码中分离出来:
try {
deletePage(page);
deleteRefrence(page.name);
deleteKey(page.name.key());
} catch (Exception e) {
}
try/catch代码块丑陋不堪,搞乱了代码结构,把错误处理与正常流程结构分离开来。
void delete(Page page) {
try {
deltePageAndAllReferences(page)
} catch(Exception e) {
log;
}
}
正常流程结构:
void deletePageAndAllReferences(Page page) {
deletePage(page);
deleteRefrence(page.name);
deleteKey(page.name.key());
}
这样子代码干净了些,而且函数只干一件事。错误处理就是一件事。
想要更简化一下try/catch代码块,可以使用vavr工具包中的Try类
Try.of((page) -> deltePageAndAllReferences(page)).onFailure(e -> log(e));
写法详情可观看小视频
返回的错误码,我们常会使用一个常量类或者枚举定义所有错误码。
当新增逻辑需要增加新错误码时,就会增加新代码,而且还要来修改这个错误码类。
这样的类被称为依赖磁铁,当这个类修改时,其他所有类都需要重新编译和部署。
使用异常类代替错误码,新异常可以从异常类派生出来,而无须重新编译或重新部署。
在之前的异常文章中,提到检查异常有很强的穿透力,当类调用链路长,在底层方法上增加新检查异常就会导致上层所有方法修改声明,有点违反OCP。
在DDD中有防腐层的概念,通过防腐层去隔离两个界限上下文的变化。
异常也有类似的情况。
当调用第三方API时,会需要处理异常情况。
ThirdPartAPI third = new ThirdPartAPI();
try {
third.open();
} catch (Third1Exception e) {
//
} catch (Thrid2Exception e) {
//
} catch (Third3Exception e) {
//
} finally {
}
首先我们需要打包这个第三方API,降低对它的依赖;也不必绑死在某一特定供应商API上,定义自己的API还要抽象异常
class ThirdPartService {
public void open() {
try {
third.open();
} catch (Third1Exception e) {
//
throw new SelfException(e);
} catch (Thrid2Exception e) {
//
throw new SelfException(e);
} catch (Third3Exception e) {
//
throw new SelfException(e);
} finally {
}
}
}
上面代码,定义了抽象的ThirdPartSevice,并且抽象出SelfException。
经过上面的三种手法,可以让代码在处理异常时,更加整洁。
[1]
《领域服务是抛出异常还是返回错误码》: https://www.zhuxingsheng.com/blog/does-the-domain-service-throw-an-exception-or-return-an-error-code.html