Clean Code系列之异常处理

先前已经对异常如何设计,如何实践异常都写了几篇阐述了。再一次从Clean Code角度来谈谈异常的使用。

1、使用异常替代返回错误码

为什么?是从函数的角度去考虑:

函数要么做什么事,要么回答什么事,但二者不可得兼。也就是修改某对象状态,或者是返回该对象的有关信息。也就是指令与询问分隔开来


1
boolean set(String attribute,String value);

该函数设置某个指定属性,如果成功,就返回true,如果不存在那个属性,就返回false。

1
2
3
if(set("website","zhuxingsheng.com")){
//
}

但从读者角度考虑一下,它是在问websit属性值是否之前已经设置为zhuxingsheng.com,还是在问websit属性值是否成功设置为zhuxingsheng.com呢?从该行语句很难判断其含义,因为set是动词还是形容词并不清楚。

作者本意是,set是一个动词,但在if语句的上下文中,感觉它是一个形容词。该语句读起来像是在说“如果websit属性值之前已经被设置为zhuxingsheng.com”,而不是“设置websit属性值为zhuxingsheng.com,看看是否可行,然后…”。

要解决这个问题,可以将set函数重命名为setAndCheckIfExists,但这对提高if语句的可读性帮助不大。真正的解决方案是把指令与询问分隔开来,防止产生混淆:

1
2
3
if(attributeExists("website"){
setAttribute("website","zhuxingsheng.com");
}

《领域服务是抛出异常还是返回错误码》,提到过如何编写返回错误码

1
2
3
if(deletePage(page)) == OK){

}

但这样,从指令式函数返回错误码,有些违反指令与询问分隔的规则。

虽然这儿没有像上面的示例一样,引起动词与形容词的混淆,却会导致更深层次嵌套结构

1
2
3
4
5
6
7
8
9
10
11
12
13
if(deletePage(page) == OK){
if(deleteRefrence(page.name) == OK) {
if (deleteKey(page.name.key()) == OK) {
//
} else {
//
}
} else {
//
}
} else {
//
}

使用异常替代错误码,错误处理代码能从主路径代码中分离出来:

1
2
3
4
5
6
7
try {
deletePage(page);
deleteRefrence(page.name);
deleteKey(page.name.key());
} catch (Exception e) {

}

抽离try/catch 代码块

try/catch代码块丑陋不堪,搞乱了代码结构,把错误处理与正常流程结构分离开来。

1
2
3
4
5
6
7
void delete(Page page) {
try {
deltePageAndAllReferences(page)
} catch(Exception e) {
log;
}
}

正常流程结构:

1
2
3
4
5
void deletePageAndAllReferences(Page page) {
deletePage(page);
deleteRefrence(page.name);
deleteKey(page.name.key());
}

这样子代码干净了些,而且函数只干一件事。错误处理就是一件事。

想要更简化一下try/catch代码块,可以使用vavr工具包中的Try类

1
Try.of((page) -> deltePageAndAllReferences(page)).onFailure(e -> log(e));

ErrorCode枚举类

返回的错误码,我们常会使用一个常量类或者枚举定义所有错误码。

当新增逻辑需要增加新错误码时,就会增加新代码,而且还要来修改这个错误码类。

这样的类被称为依赖磁铁,当这个类修改时,其他所有类都需要重新编译和部署。

使用异常类代替错误码,新异常可以从异常类派生出来,而无须重新编译或重新部署。

2、使用未检查异常

在之前的异常文章中,提到检查异常有很强的穿透力,当类调用链路长,在底层方法上增加新检查异常就会导致上层所有方法修改声明,有点违反OCP。

3、异常防腐

在DDD中有防腐层的概念,通过防腐层去隔离两个界限上下文的变化。

异常也有类似的情况。

当调用第三方API时,会需要处理异常情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
ThirdPartAPI third = new ThirdPartAPI();

try {
third.open();
} catch (Third1Exception e) {
//
} catch (Thrid2Exception e) {
//
} catch (Third3Exception e) {
//
} finally {

}

首先我们需要打包这个第三方API,降低对它的依赖;也不必绑死在某一特定供应商API上,定义自己的API还要抽象异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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。

总结

经过上面的三种手法,可以让代码在处理异常时,更加整洁。

公众号:码农戏码
欢迎关注微信公众号『码农戏码』