写可读的代码

最后更新于:2022-04-01 05:22:20

有些人以为写很多注释就可以让代码更加可读,然而却发现事与愿违。注释不但没能让代码变得可读,反而由于大量的注释充斥在代码中间,让程序变得障眼难读。而且代码的逻辑一旦修改,就会有很多的注释变得过时,需要更新。修改注释是相当大的负担,所以大量的注释,反而成为了妨碍改进代码的绊脚石。 实际上,真正优雅可读的代码,是几乎不需要注释的。如果你发现需要写很多注释,那么你的代码肯定是含混晦涩,逻辑不清晰的。其实,程序语言相比自然语言,是更加强大而严谨的,它其实具有自然语言最主要的元素:主语,谓语,宾语,名词,动词,如果,那么,否则,是,不是,…… 所以如果你充分利用了程序语言的表达能力,你完全可以用程序本身来表达它到底在干什么,而不需要自然语言的辅助。 有少数的时候,你也许会为了绕过其他一些代码的设计问题,采用一些违反直觉的作法。这时候你可以使用很短注释,说明为什么要写成那奇怪的样子。这样的情况应该少出现,否则这意味着整个代码的设计都有问题。 如果没能合理利用程序语言提供的优势,你会发现程序还是很难懂,以至于需要写注释。所以我现在告诉你一些要点,也许可以帮助你大大减少写注释的必要: 1. 使用有意义的函数和变量名字。如果你的函数和变量的名字,能够切实的描述它们的逻辑,那么你就不需要写注释来解释它在干什么。比如: ~~~ // put elephant1 into fridge2 put(elephant1, fridge2); ~~~ 由于我的函数名`put`,加上两个有意义的变量名`elephant1`和`fridge2`,已经说明了这是在干什么(把大象放进冰箱),所以上面那句注释完全没有必要。 2. 局部变量应该尽量接近使用它的地方。有些人喜欢在函数最开头定义很多局部变量,然后在下面很远的地方使用它,就像这个样子: ~~~ void foo() { int index = ...; ... ... bar(index); ... } ~~~ 由于这中间都没有使用过`index`,也没有改变过它所依赖的数据,所以这个变量定义,其实可以挪到接近使用它的地方: ~~~ void foo() { ... ... int index = ...; bar(index); ... } ~~~ 这样读者看到`bar(index)`,不需要向上看很远就能发现`index`是如何算出来的。而且这种短距离,可以加强读者对于这里的“计算顺序”的理解。否则如果index在顶上,读者可能会怀疑,它其实保存了某种会变化的数据,或者它后来又被修改过。如果index放在下面,读者就清楚的知道,index并不是保存了什么可变的值,而且它算出来之后就没变过。 如果你看透了局部变量的本质——它们就是电路里的导线,那你就能更好的理解近距离的好处。变量定义离用的地方越近,导线的长度就越短。你不需要摸着一根导线,绕来绕去找很远,就能发现接收它的端口,这样的电路就更容易理解。 3. 局部变量名字应该简短。这貌似跟第一点相冲突,简短的变量名怎么可能有意义呢?注意我这里说的是局部变量,因为它们处于局部,再加上第2点已经把它放到离使用位置尽量近的地方,所以根据上下文你就会容易知道它的意思: 比如,你有一个局部变量,表示一个操作是否成功: ~~~ boolean successInDeleteFile = deleteFile("foo.txt"); if (successInDeleteFile) { ... } else { ... } ~~~ 这个局部变量`successInDeleteFile`大可不必这么啰嗦。因为它只用过一次,而且用它的地方就在下面一行,所以读者可以轻松发现它是`deleteFile`返回的结果。如果你把它改名为`success`,其实读者根据一点上下文,也知道它表示"success in deleteFile"。所以你可以把它改成这样: ~~~ boolean success = deleteFile("foo.txt"); if (success) { ... } else { ... } ~~~ 这样的写法不但没漏掉任何有用的语义信息,而且更加易读。`successInDeleteFile`这种"[camelCase](https://en.wikipedia.org/wiki/CamelCase)",如果超过了三个单词连在一起,其实是很碍眼的东西,所以如果你能用一个单词表示同样的意义,那当然更好。 4. 把复杂的逻辑提取出去,做成“帮助函数”。有些人写的函数很长,以至于看不清楚里面的语句在干什么,所以他们误以为需要写注释。如果你仔细观察这些代码,就会发现不清晰的那片代码,往往可以被提取出去,做成一个函数,然后在原来的地方调用。由于函数有一个名字,这样你就可以使用有意义的函数名来代替注释。举一个例子: ~~~ ... // put elephant1 into fridge2 openDoor(fridge2); if (elephant1.alive()) { ... } else { ... } closeDoor(fridge2); ... ~~~ 如果你把这片代码提出去定义成一个函数: ~~~ void put(Elephant elephant, Fridge fridge) { openDoor(fridge); if (elephant.alive()) { ... } else { ... } closeDoor(fridge); } ~~~ 这样原来的代码就可以改成: ~~~ ... put(elephant1, fridge2); ... ~~~ 更加清晰,而且注释也没必要了。 5. 把复杂的表达式提取出去,做成中间变量。有些人听说“函数式编程”是个好东西,也不理解它的真正含义,就在代码里使用大量嵌套的函数。像这样: ~~~ Pizza pizza = makePizza(crust(salt(), butter()), topping(onion(), tomato(), sausage())); ~~~ 这样的代码一行太长,而且嵌套太多,不容易看清楚。其实训练有素的函数式程序员,都知道中间变量的好处,不会盲目的使用嵌套的函数。他们会把这代码变成这样: ~~~ Crust crust = crust(salt(), butter()); Topping topping = topping(onion(), tomato(), sausage()); Pizza pizza = makePizza(crust, topping); ~~~ 这样写,不但有效地控制了单行代码的长度,而且由于引入的中间变量具有“意义”,步骤清晰,变得很容易理解。 6. 在合理的地方换行。对于绝大部分的程序语言,代码的逻辑是和空白字符无关的,所以你可以在几乎任何地方换行,你也可以不换行。这样的语言设计,是一个好东西,因为它给了程序员自由控制自己代码格式的能力。然而,它也引起了一些问题,因为很多人不知道如何合理的换行。 有些人喜欢利用IDE的自动换行机制,编辑之后用一个热键把整个代码重新格式化一遍,IDE就会自动的把超过行宽限制的代码自动折行。可是这种自动这行,往往没有根据代码的逻辑和读者的理解来进行。你有可能得到这样的代码: ~~~ if (someLongCondition1() && someLongCondition2() && someLongCondition3() && someLongCondition4()) { ... } ~~~ 由于`someLongCondition4()`超过了行宽限制,被编辑器自动换到了下面一行。虽然这样满足了行宽限制,可是却是相当任意的。它并不能帮助人理解这代码的逻辑。这几个boolean表达式,全都用`&&`连接,所以它们其实处于平等的地位。为了表达这一点,当这行太长需要折行的时候,你应该把每一个表达式都做成新的一行,就像这个样子: ~~~ if (someLongCondition1() && someLongCondition2() && someLongCondition3() && someLongCondition4()) { ... } ~~~ 这样每一个条件都对齐,里面的逻辑就很清楚了。再举个例子: ~~~ log.info("failed to find file {} for command {}, with exception {}", file, command, exception); ~~~ 这行因为太长,被自动折行成这个样子。它就不如折成这个样子: ~~~ log.info("failed to find file {} for command {}, with exception {}", file, command, exception); ~~~ 把格式字符串单独放在一行,而把它的参数放在另外一样,这样逻辑就更加清晰。 为了避免IDE把这些手动调整好的换行弄乱,很多IDE(比如IntelliJ)的自动格式化设定里都有“保留原来的换行符”的设定。如果你发现IDE的换行不符合逻辑,你可以修改这些设定,然后在某些地方保留你自己的手动换行。 说到这里,我必须警告你,这里所说的“不需注释,让代码自己解释自己”,并不是说要让代码看起来像某种自然语言。有个叫Chai的JavaScript测试工具,可以让你这样写代码: ~~~ expect(foo).to.be.a('string'); expect(foo).to.equal('bar'); expect(foo).to.have.length(3); expect(tea).to.have.property('flavors').with.length(3); ~~~ 这种做法是极其错误的。程序语言本来就比自然语言简单清晰,这种写法让它看起来像自然语言的样子,反而变得复杂难懂了。
';