编程的智慧 2

写可读的代码

  有些人以为写很多注释就可以让代码更加可读,然而却发现事与愿违。注释不但没能让代码变得可读,反而由于大量的注释充斥在代码中间,让程序变得碍眼难读。而且代码的逻辑一旦修改,就会有很多的注释变得过时,需要更新。修改注释是相当大的负担,所以大量的注释,反而成为了妨碍改进代码的绊脚石。
  
  实际上,真正优雅可读的代码,是几乎不需要注释的。如果你发现需要写很多注释,那么你的代码肯定是含混晦涩,逻辑不清的。其实,程序语言相比自然语言,是更加强大而严谨的,它具有自然语言最主要的元素:主语,谓语,宾语,名词,动词,如果,那么,否则,是,不是,……所以,如果你充分利用了程序语言的表达能力,你完全可以用程序本身来表达它到底在干什么,而不需要自然语言的辅助。
  
  有少数的时候,你也许会为了绕过其他代码的设计问题,采用一些违反直觉的作法。这时候你可以使用很短的注释,说明为什么要写成那奇怪的样子。这样的情况应该少出现,否则就意味着整个代码的设计都有问题。
  
  如果没能合理利用程序语言提供的优势,你会发现程序还是很难懂,以至于需要写注释。所以我现在告诉你一些要点,也许可以帮助你大大减少写注释的必要:
  
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、 局部变量名字应该简短

  这貌似跟第1点相冲突,简短的变量名怎么可能有意义呢?注意,我这里说的是局部变量,因为它们处于局部,再加上第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,如果超过了三个单词连在一起,其实是很碍眼的。所以,如果你能用一个单词表示同样的意义,那当然更好。
  
4、不要重用局部变量

  很多人写代码不喜欢定义新的局部变量,而喜欢“重用”同一个局部变量,通过反复对它们进行赋值,来表示完全不同的意思。比如这样写:

String msg;
if (...) {
  msg = "succeed";
  log.info(msg);
} else {
  msg = "failed";
  log.info(msg);
}

  虽然这样在逻辑上是没有问题的,然而却不易理解,容易混淆。变量msg两次被赋值,表示完全不同的两个值。它们立即被log.info使用,没有传递到其它地方去。这种赋值的做法,把局部变量的作用域不必要的增大,让人以为它可能在将来改变,也许会在其它地方被使用。更好的做法,其实是定义两个变量:

if (...) {
  String msg = "succeed";
  log.info(msg);
} else {
  String msg = "failed";
  log.info(msg);
}

  由于这两个msg变量的作用域仅限于它们所处的if语句分支,你可以很清楚的看到这两个msg被使用的范围,而且知道它们之间没有任何关系。
  
5、把复杂的逻辑提取出去,做成“帮助函数”

  有些人写的函数很长,以至于看不清楚里面的语句在干什么,所以他们误以为需要写注释。如果你仔细观察这些代码,就会发现不清晰的那片代码,往往可以被提取出去,做成一个函数,然后在原来的地方调用。由于函数有一个名字,这样你就可以使用有意义的函数名来代替注释。举个例子:

...
// 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);
...

更加清晰,而且注释也没必要了。

6、把复杂的表达式提取出去,做成中间变量

  有些人听过“函数式编程”是个好东西,也不理解它的真正含义,就在代码里大量使用嵌套的函数。像这样:

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);

  这样写,不但有效地控制了单行代码的长度,而且由于引入的中间变量具有“意义”,步骤清晰,变得很容易理解。

7、在合理的地方换行

  对于绝大部分的程序语言,代码的逻辑是和空白字符无关的,所以你可以在几乎任何地方换行,你也可以不换行。这样的语言设计是个好东西,因为它给了程序员自由控制自己代码格式的能力。然而,它也引发了一些问题,因为很多人不知道如何合理的换行。

  有些人喜欢利用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);

  这行因为太长,被自动折行成这个样子。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);

  这种做法不可取。程序语言本就比自然语言简单清晰,这种写法让它看起来像自然语言,反而变得复杂难懂了。

猜你喜欢

转载自blog.csdn.net/Gnd15732625435/article/details/80302994