减少代码的嵌套层数
软件开发中的“心智模型”用于描述开发人员在编码时心理活动,每段代码其实是人在编写这段代码时的心智模型投射,不能把代码看成是客观的存在,而是主观的产物,参合了当时心理活动或各种直觉感知。在编程时,必须不断维护心智模型,例如关于整体代码交互和功能实现的心智模型,代码是否具有可读性,代码风格,变量命名规范等。可读性强的代码需要的心智模型更简单,更容易阅读和维护。
代码嵌套层级的数量是影响可读性的一个关键因素,假设正在一个新项目上进行开发工作,并且需要读懂下面join函数实现的功能。此join函数实现的功能是将两个字符连接起来,如果连接起来的字符串超过给定的长度max,则返回前max个字符的子串。在处理的过程中,对参数进行检查并判断concatenate是否有返回错误。
func join(s1, s2 string, max int) (string, error) {
if s1 == "" {
return "", errors.New("s1 is empty")
} else {
if s2 == "" {
return "", errors.New("s2 is empty")
} else {
concat, err := concatenate(s1, s2)
if err != nil {
return "", err
} else {
if len(concat) > max {
return concat[:max], nil
} else {
return concat, nil
}
}
}
}
}
func concatenate(s1 string, s2 string) (string, error) {
// ...
}
从功能实现的角度来看,上述函数是正确的。然而,建立一个涵盖所有不同输入参数情况的心智模型可能不是一项简单的任务。为啥呢?由于代码嵌套的数量层级过多。现在,对上面的代码进行重构,得到新的实现如下。
func join(s1, s2 string, max int) (string, error) {
if s1 == "" {
return "", errors.New("s1 is empty")
}
if s2 == "" {
return "", errors.New("s2 is empty")
}
concat, err := concatenate(s1, s2)
if err != nil {
return "", err
}
if len(concat) > max {
return concat[:max], nil
}
return concat, nil
}
func concatenate(s1 string, s2 string) (string, error) {
// ...
}
对比这两种实现,虽然功能一样,但是阅读这个新版本的心智模型需要的认知负荷更少,因为新版实现中函数嵌套层级更少。正如Mat Ryer(Go Time播客小组成员)所说:
将正常情况逻辑(happy path)向左对齐,阅读时能够快速向下扫描一列可以查看预期的执行流程。
由于存在嵌套的if/else语句,很难看出重构前版本中的预期执行流程。相反,重构后的版本眼睛只需向下扫描一列便可知道预期的执行流程,通过第二列了解各种特殊情况的处理逻辑。
一般来说,函数需要的嵌套层数越多,阅读和理解就越复杂。下面来看看如何使用这条规则优化代码的可读性。
- 当if语句在满足和不满足都有逻辑处理时,我们应该省略else语句块,不应该像下面这样写.
if foo() {
// ...
return true
} else {
// ...
}
而应该像下面这样写,将else语句省略掉,并将else语句块中的逻辑移动到顶层,使其更易于阅读。
if foo() {
return true
}
// ...
- 对于像下面这种if语句是一条non-happy路径,可以通过改变比较条件来减少代码块数量。
if s != "" {
// ...
} else {
return errors.New("empty string")
}
上面s为空是一个non-happy路径,我们可以通过翻转条件,得到如下实现。下面这种实现就更容易阅读,因为它将快乐路径放在了左边并减少了代码块的数量。
if s == "" {
return errors.New("empty string")
}
// ...
编写可读的代码对每个开发人员来说都是一项重要的挑战,努力减少代码块嵌套的层级数量,将快乐路径对齐放在左侧,并尽早返回是提高代码可读性的具体手段,在工作中,我们应该应用这些手段。