在我们的编程开发中,如果能在没有一行注释的代码中找到注释,是不是很意思呢?

  我们经常容易犯一个错误:我们修改了一段代码,但是忘记修改更新注释。混乱的注释并不会打断你代码的执行,但是想象一下debug的时候会发生什么事情。你认真地阅读了注释,它说的是一件事,但是代码干的是另一件事。结果是,你浪费了很多时间发现注释是错误的,甚至最糟糕的是,你可能完全被误导了。

  但是代码中完全不写注释也不是一个很好地选择。在我超过15年的编程经验中,我从未觉得代码库中的注释是完全没用的。

  然而,有一些办法可以来帮助减少代码注释的必要性。我们可以利用某些编码技巧来让我们的代码变得更清晰,例如就是利用编程语言的特点。这样不仅能帮我们的代码变得更加清晰易理解,而且还能能帮助我们改善程序的设计。

  这类代码通常被称为自文档化代码。让我来给大家展示下怎样利用这种方式编码。当然,我这里我演示的代码使用的是JavaScript语言,你们也可以利用这里大部分的技巧运用到其他的语言中。

概述

  一些编程者将注释归纳到自文档化代码的范畴。在这篇文章中,我们只关注代码。注释很重要,但是却单独覆盖了太多东西。我们可以把自文档化代码归为三大类:

  • 结构类自文档化。使用代码的结构和目录来让代码变得清晰
  • 命名自文档化。例如函数或变量命名让代码更易理解
  • 语句相关自文档化。我们利用语言的特性来让代码变得清晰

一、结构类自文档化

  首先我们看下结构类自文档化。这里指的是通过移动部分代码来让代码变得清晰。

  • 将代码移动到函数里面。

  这和提取函数重构一样,意思是我们将已经写好的代码移动到一个新的函数里:即提取代码成为一个新函数。例如:

1
var width = (value - 0.5) * 16;

  不是很清晰,这里添加一个注释会很有帮助,或者,我们将它提取到一个函数里面进行自文档描述:

1
2
3
4
5
var width = emToPixels(value);

function emToPixels(ems) {
    return (ems - 0.5) * 16;
}

  这里唯一的变化是我们把计算放到函数里面。函数的名称描述了代码的作用,所以代码的意思就不言而喻了。另一个好处是,我们现在有一个到处可以调用的辅助函数,所以这种方法也增强了代码复用性。

  • 用函数代替条件表达式

  如果没有注释,含有多个操作运算的语句很难理解。我们可以使用一个简单的方法来描述。

1
2
if(!el.offsetWidth || !el.offsetHeight) {
}

  这段代码的目的是啥?

1
2
3
4
5
6
function isVisible(el) {
    return el.offsetWidth && el.offsetHeight;
}

if(!isVisible(el)) {
}

  同样,我们把代码移动到一个函数里面,代码就立即变得容易理解了。

  • 使用变量代替表达式

  使用变量代替表达式和将代码移入函数里面类似,但是相对于函数,我们是用一个变量。让我们再来看下这个语句:

1
2
if(!el.offsetWidth || !el.offsetHeight) {
}

  这次我们用变量来代替表达式,而不是函数:

1
2
3
var isVisible = el.offsetWidth && el.offsetHeight;
if(!isVisible) {
}

  这样比使用函数更好,例如,如果使用的逻辑非常明确,而且这段逻辑只在某一个地方使用一次,就适合用变量了。这种方法最常用的场景是在数学表达式中:

1
return a * b + (c / d);

  我们可以这样让代码变得清晰:

1
2
3
var multiplier = a * b;
var divisor = c / d;
return multiplier + divisor;

  因为我数学很差,想象一下上面的例子是一个有含义的运算,在任何场景下,你可以将复杂的表达式换成变量来给表达式代码添加本身的含义。

  • 类和模块接口

  接口不仅是类或模块的公用方法和属性,而且在使用中能起到自文档化的作用。来看下这个例子:

1
2
3
4
5
6
7
8
9
class Box {
    setState(state) {
        this.state = state;
    }

    getState() {
        return this.state;
    }
}

  这个类当然可以含有其他的代码部分,我这里是让例子简单些来演示公共接口是怎样自文档化的。你能理解这个类是怎样使用的吗?可能需要通过一段时间的理解,但不是特别明显。

  这两个函数都有合理的命名:他们做的事情从它们的名称来看非常清晰。但是尽管如此,你仍然不知道如何使用它们。很可能你需要阅读更多的代码或文档来理解怎么用。所以我们可以这样改下:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Box {
    open() {
        this.state = 'open';
    }

    close() {
        this.state = 'closed';
    }

    isOpen() {
        return this.state === 'open';
    }
}

  很容易理解了用法,对吧?需要注意的是,我们只改变了公用接口,内部的声明仍然使用的是

1
this.state
来用的。现在你可以一眼就知道
1
Box
类是怎样来使用的了。这就演示了尽管使用了很好命名,但是整个模块使用仍然不好理解的情况,通过这些简单的决策,你会有一个更好的认知。

  • 代码分块。

  对不同部分的代码进行分块也能达到自文档化的目的。例如,你应该养成将变量的声明和它们使用的语句放到一起的习惯,并尽可能将变量的使用放到一起。这样可以显示不同部分代码之间的联系,所以后面任何人修改都能花更少的时间来找到它被使用到的所有位置。看下这个例子:

1
2
3
4
5
6
7
8
var foo = 1;

blah()
xyz();

bar(foo);
baz(1337);
quux(foo);

  你能一眼看出

1
foo
被调用了几次吗?再来看下这个例子:

1
2
3
4
5
6
7
8
var foo = 1;
bar(foo);
quux(foo);

blah()
xyz();

baz(1337);

  通过把所有的

1
foo
调用的地方放到一起,我们可以很容易的看到那几个部分的方法函数式依赖它的。

  • 使用纯函数。

  纯函数比普通的函数更容易理解,什么是纯函数?当使用同样的参数调用,如果输出的内容相同,这就是所谓的”纯函数”。这就是说,这个函数不会对状态有任何依赖或影响,例如时间、对象属性、ajax方法等等。这类函数很容易理解,因为很多影响他们输出结果的值都被明确指定了,你不用研究某个东西到底是从哪里来的,或者什么东西会影响输出,而是很直观能理解到的。

  这类函数能够进行自文档化描述的另一个原因是你可以信任它们的输出。无论怎样,这种函数会根据你的输入返回特定的结果。它不会影响任何额外的东西,所以你能确定它不会带来任何副影响。

  一个这类函数出错的例子是

1
document.write()
,有经验的js开发者知道我们不应该使用它,但是很多新手还在那折腾。有时它没问题,但是有时,在某些特定情况,它会清除页面所有内容。这就是副影响。

  为了了解什么是纯函数,可以看下这篇文章《Functional Programming: Pure Functions》。

  • 目录和文件结构

  在文件或目录命名时,根据项目中的命名规范进行命名。如果没有明确的规范,那就根据你语言选择合适的标准。例如:如果你添加一个与UI相关的代码,发现项目中有相似功能的内容,如果UI相关的内容放在

1
src/ui/
的文件夹下,你应该也这样做。这样可以更容易的找到代码,并且根据项目中其它的代码,你可以很容易知道它的作用。所有的UI代码都在相同的地方,然后,它必须是与UI相关的。

二、命名自文档化

  计算机科学领域有句名言:

在计算机凌云有量大难题:缓存失效和命名。–Phil Karlton

  所以让我们来看下我们怎样使用命名来使我们的代码自文档化。

  • 重命名函数。

  函数命名常常比较复杂,但是有一些简单的规则我们可以参考:

  1、避免使用想handle或manage这类单词:handleObject,manageObject。轻微这些什么意思。   2、使用主动词:cutGruss(),sendFile。根据函数的具体功能来决定。   3、使用返回值:getMagicBullet(),readFile。这种情况并不经常使用,但是在语义化方面很有作用   4、使用强类型语言编码能明确知道返回值是什么类型

  • 重命名变量

  对于变量,有两个比较好的方法。

  1、指明单位:如果你含有数字参数,你可以带上单位。例如

1
widthPx
就比
1
width
要好。   2、不要使用简写:例如a和b,这些是不能理解的变量,除非是在循环计数里面。

  • 遵循已有的命名规范

  代码中尽量使用原有的规范。例如,如果你有一个具体类型的对象,那么使用相同的名字:

1
var element = getElement();

  千万不要傻傻的这样写:

1
var node = getElement();

  如果你遵循其它地方代码的规范,任何一个读代码的人都能正确的认为某个东西在任何地方出现的含义都是相同的。

  • 使用有含义的错误

  Undefined is not an object。很多人都喜欢这样提示到页面上,所以让我们提示一些具有提示意义的内容给页面使用者。怎样让错误变得有含义:

  1、应该描述具体问题所在。   2、如果可能,应该包含是哪个变量或数据导致的问题。   3、关键一点:错误应该能帮助我们找到哪里出了问题。例如错误上报的方式收集解决问题。

三、 声明类自文档化

  让我们看下一个JavaScript的例子:

1
imTricky && doMagic();

尽量这样写:

1
2
3
if(imTricky) {
    doMagic();
}

  声明技巧并不适合每个人。

  • 使用命名常量。例如
    1
    const MEANING_OF_LIFE = 42;
    
  • 避免bool值。例如
    1
    myThing.setData({ x: 1 }, true);
    
  • 使用编程语言的优势。

  我们甚至可以使用编程语言某些特有的特性来描述一段代码的作用:

1
2
3
4
var ids = [];
for(var i = 0; i < things.length; i++) {
  ids.push(things[i].id);
}

  上面代码收集了一个数组元素里面的id。但是我么可以这样写。

1
2
3
var ids = things.map(function(thing) {
  return thing.id;
});

  在这个例子中。我们立即知道了这个过程的作用,因为这就是map方法的目的。另一个JavaScript的例子是

1
const
关键字,常常,我们通过它来声明不会更变的变量。一个非常通用的例子是加载CommonJS模块的时候:

1
var async = require('async');

  我们可以描述得更清晰:

1
const async = require('async');

  另一个好处是,如果有人修改,会报错。

四、反模式

  当这些你都熟悉了之后,你可以做更多的事情,然而有些事情你必须要小心:

  • 提取更短的函数。

  一些人主张使用很小的函数,将所有的东西函数化,你可以这样做,但是,这会增加代码理解的难度。例如,试想一下,你debug的时候,你看了A函数,然后A调用了B函数,然后B调用了C函数…短的函数比较容易理解,但是如果你只是在一个地方使用,推荐使用变量来代替。

  • 不要强迫去使用

  通常,没有绝对正确的方式,所以,如果听起来某件事情很不错,但是不要强制自己去使用。

结论

  让自己的代码自文档化是一件是你的代码更容易维护的事情,也是一件很难的事情。每一行添加的代码注释不一定会被维护起来,所以消除代码注释也是一件很好的事情。

  然而,自文档化代码并不能代替文档或注释。例如,代码在表达方面是有限的,所以你也需要很好的注释来补充。API文档对于代码库来说是很重要的,除非你的代码很小,否则自文档化的方式并不可行。

原文作者:Jani Hartikainen

原译:ouven

原文地址: https://www.sitepoint.com/self-documenting-javascript/