一个应用于软件的心理模型是系统行为的内部表示。在编程时,我们需要维护心理模型(例如,关于整体代码交互和函数实现)。根据命名、一致性、格式化等多重标准,代码被认为具有可读性。可读性高的代码需要较少的认知努力来维持心理模型;因此,它更容易阅读和维护。
可读性的关键方面之一是嵌套层次的数量。我们来做个练习。假设我们正在开展一个新项目,需要理解以下 join
函数的作用:
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) {
// ...
}
这个 join
函数将两个字符串连接起来,如果长度大于 max
,则返回一个子字符串。同时,它还处理对 s1
和 s2
的检查,以及调用连接时是否返回了错误。
从实现的角度来看,这个函数是正确的。然而,构建一个包含所有不同情况的心理模型可能并不是一项简单的任务。为什么?因为嵌套层次的数量。
现在,让我们再次尝试这个练习,但这次使用相同函数的不同实现方式:
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) {
// ...
}
您可能已经注意到,构建这个新版本的心智模型所需的认知负荷较小,尽管它与之前做的工作相同。在这里,我们只保留了两个嵌套层次。正如《Go Time》播客的嘉宾 Mat Ryer 所提到的(medium.com/@matryer/li…):
将主要执行路径(快乐路径)左对齐;你应该能够迅速地沿一列向下扫描,以查看预期的执行流程。
第一个版本由于嵌套的 if/else 语句,很难区分预期的执行流程。
相反,第二个版本只需要扫描一列就能看到预期的执行流程,通过扫描第二列则能看到如何处理边缘情况,如图 2.1 所示。
一般来说,一个函数所需的嵌套层次越多,它就越难阅读和理解。让我们看看这个规则的一些不同应用,以优化我们的代码可读性:
复制再试一次分享:
-
当一个
if
代码块使用了return
语句时,我们应该在所有情况下省略else
代码块。例如,我们不应该这样写:if foo() { // ... return true } else { // ... }
相反,我们应该像这样省略
else
代码块:if foo() { // ... return true } // ...
使用这个新版本,之前位于
else
代码块中的代码被移动到了最外层,这样做使得代码更易于阅读。 -
我们也可以将这个逻辑应用于非主要执行路径:
if s != "" { // ... } else { return errors.New("empty string") }
在这里,一个空的
s
表示非主要执行路径。因此,我们应该像这样翻转条件:if s == "" { // 翻转 if 条件 return errors.New("empty string") } // ...
这个新版本更容易阅读,因为它将主要执行路径保持在左侧边缘,并减少了代码块的数量。
编写可读性强的代码是每位开发者面临的重要挑战。努力减少嵌套代码块的数量、将主要执行路径左对齐,并且尽可能早地返回都是提高代码可读性的具体方法。
在下一节中,我们将讨论 Go 项目中常见的一个误用:初始化(init)函数。