本文由Tim Severien和Mark Brown进行同行评审。 感谢所有SitePoint的同行评审人员使SitePoint内容达到最佳状态!
在代码中找到完全不合适且无用的注释不是很有趣吗?
这是一个容易犯的错误:您更改了一些代码,却忘记了删除或更新注释。 不好的注释不会破坏您的代码,但请想象一下调试时会发生什么。 您阅读了评论。 它说一件事,而代码则做另一件事。 您可能最终会浪费时间弄清楚它,在最坏的情况下,它甚至可能误导您!
但是编写带有零注释的代码不是一种选择。 在我超过15年的编程经验中,我从未见过完全不需要注释的代码库。
但是,有一些方法可以减少对注释的需求。 我们可以利用某些编码技术来澄清我们的代码,只需利用编程语言的功能即可。
这不仅有助于使我们的代码更易于理解,还可以帮助改善程序的整体设计!
这种类型的代码通常称为自我记录 。 让我向您展示如何立即采用这种方法进行编码。 尽管我将在此处介绍的示例是使用JavaScript的,但您也可以将其他技术应用到其他语言中。
技术概述
一些程序员将注释作为自我记录代码的一部分。 在本文中,我们将只关注代码。 评论很重要,但它们是一个很大的主题,需要单独讨论。
我们可以将自我记录代码的技术分为三大类:
- 结构的 ,其中使用代码或目录的结构来阐明目的
- 与命名相关 ,例如函数或变量命名
- 与语法相关的语言 ,其中我们利用(或避免使用)语言的功能来澄清代码。
其中许多都是在纸上简单的。 挑战来自知道何时使用哪种技术。 当我们处理每个问题时,我将向您展示一些实际示例。
结构性
首先,让我们看一下结构类别。 结构更改是指为了提高清晰度而四处移动代码。
将代码移入函数
这与“提取函数”重构相同-意味着我们将现有代码移入新函数:将代码“提取”到新函数中。
例如,尝试猜测以下行的作用:
var width = (value - 0.5) * 16;
不太清楚; 这里的评论可能会很有用。 或者,我们可以提取一个函数使其自我记录:
var width = emToPixels(value);
function emToPixels(ems) {
return (ems - 0.5) * 16;
}
唯一的变化是我将计算移到了一个函数中。 该函数的名称描述了它的功能,因此不再需要澄清代码。 另外一个好处是,我们现在有一个有用的帮助程序功能,您可以在其他地方使用它,因此此方法还有助于减少重复。
用函数替换条件表达式
如果没有注释,带有多个操作数的if子句通常很难理解。 我们可以应用与上述类似的方法来澄清它们:
if(!el.offsetWidth || !el.offsetHeight) {
}
上述条件的目的是什么?
function isVisible(el) {
return el.offsetWidth && el.offsetHeight;
}
if(!isVisible(el)) {
}
再次,我们将代码移入了一个函数,该代码立即变得更容易理解。
用变量替换表达式
用变量代替某些东西类似于将代码移入函数,但是我们只使用变量而不是函数。
让我们再次看一下带有if子句的示例:
if(!el.offsetWidth || !el.offsetHeight) {
}
除了提取函数外,我们还可以通过引入变量来阐明这一点:
var isVisible = el.offsetWidth && el.offsetHeight;
if(!isVisible) {
}
与提取函数相比,这是一个更好的选择-例如,当您要阐明的逻辑非常特定于仅在一个地方使用的特定算法时。
此方法最常见的用法是数学表达式:
return a * b + (c / d);
我们可以通过拆分计算来澄清以上内容:
var multiplier = a * b;
var divisor = c / d;
return multiplier + divisor;
因为我在数学方面很糟糕,所以可以想象上面的示例有一些有意义的算法。 无论如何,关键是您可以将复杂的表达式移到变量中,这些变量会为难以理解的代码增加含义。
类和模块接口
类或模块的接口(即,公共方法和属性)可以用作其用法的文档。
让我们看一个例子:
class Box {
setState(state) {
this.state = state;
}
getState() {
return this.state;
}
}
该类还可以包含其他一些代码。 我故意使该示例保持简单,以说明公共接口是如何编写文档的
您能告诉我们该如何使用该类吗? 也许需要一点点工作,但这不是很明显。
这两个函数都有合理的名称:从名称中可以清楚地看出它们的作用。 但是尽管如此,仍不清楚如何使用它们。 您很可能需要阅读更多代码或该类的文档才能弄清楚。
如果我们将其更改为以下内容,该怎么办:
class Box {
open() {
this.state = 'open';
}
close() {
this.state = 'closed';
}
isOpen() {
return this.state === 'open';
}
}
看用法要容易得多,您不觉得吗? 注意,我们只更改了公共接口; 内部表示与this.state
属性相同。
现在您可以一目了然地知道Box
类是如何使用的。 这表明,尽管第一个版本在功能上有很好的称呼,但完整的软件包仍然令人困惑,并且如何通过这样的简单决策可以产生很大的影响。 您始终必须考虑全局。
代码分组
对代码的不同部分进行分组也可以作为文档的一种形式。
例如,您应该始终致力于将变量声明为尽可能靠近变量的使用位置,并尝试将变量使用分组在一起。
这可以用来指示代码不同部分之间的关系,以便将来进行更改的任何人都可以更轻松地找到他们可能还需要触摸的部分。
考虑以下示例:
var foo = 1;
blah()
xyz();
bar(foo);
baz(1337);
quux(foo);
您一眼就能看到foo
被使用了多少次吗? 对此进行比较:
var foo = 1;
bar(foo);
quux(foo);
blah()
xyz();
baz(1337);
将foo
所有使用组合在一起,我们可以轻松地看到代码的哪些部分依赖于它。
使用纯函数
纯函数比依赖状态的函数更容易理解。
什么是纯函数? 当调用具有相同参数的函数时,如果它总是产生相同的输出,则很可能是所谓的“纯函数”。 这意味着该函数不应有任何副作用或依赖状态-例如时间,对象属性,Ajax等。
这些类型的函数更容易理解,因为任何影响其输出的值都将显式传递。 您无需四处寻找出什么东西来了,或什么会影响结果,因为这些都是显而易见的。
这些类型的函数需要更多自记录代码的另一个原因是您可以信任它们的输出。 无论如何,该函数将始终仅根据您提供的参数返回输出。 它也不会影响任何外部因素,因此您可以相信它不会引起意外的副作用。
一个很好的例子是document.write()
。 经验丰富的JS开发人员知道您不应该使用它,但是许多初学者都迷失了它。 有时效果很好-但是在某些情况下,有时候它可以将整个页面擦干净。 谈论副作用!
有关什么是纯函数的更好的概述,请参见文章函数式编程:纯函数 。
目录和文件结构
命名文件或目录时,请遵循项目中使用的相同命名约定。 如果项目中没有明确约定,请遵循所选择语言的标准。
例如,如果要添加新的与UI相关的代码,请查找项目中类似功能的位置。 如果与UI相关的代码放在src/ui/
,则应该执行相同的操作。
根据您对项目中其他代码的了解,这使查找代码和显示目的更加容易。 毕竟,所有UI代码都在同一位置,因此它必须与UI相关。
命名
关于计算机科学中的两个困难,有一个流行的说法:
在计算机科学中只有两件难事:缓存无效和命名。 — 菲尔·卡尔顿
因此,让我们看一下如何使用命名事物来使我们的代码自我记录。
重命名功能
函数命名通常不太困难,但是您可以遵循一些简单的规则:
- 避免使用诸如“ handle”或“ manage”之类的模糊词:
handleLinks()
,manageObjects()
。 这些都做什么? - 使用主动动词:
cutGrass()
,sendFile()
-主动执行某些功能的函数。 - 指示返回值:
getMagicBullet()
,readFile()
。 这并不是您总是可以做的,但是在有意义的地方很有帮助。 - 具有强类型的语言也可以使用类型签名来帮助指示返回值。
重命名变量
对于变量,这里有两个好的经验法则:
- 指示单位:如果您有数字参数,则可以包括期望的单位。 例如,用
widthPx
而不是width
来表示值以像素为单位,而不是其他单位。 - 不要使用快捷方式:
a
或b
是不可接受的名称,循环中的计数器除外。
遵循既定的命名约定
尝试在代码中遵循相同的命名约定。 例如,如果您有特定类型的对象,请使用相同的名称:
var element = getElement();
不要突然决定将其称为节点:
var node = getElement();
如果您遵循与代码库中其他地方相同的约定,那么阅读它的任何人都可以根据事物在其他地方的含义来对事物的含义做出安全的假设。
使用有意义的错误
未定义不是对象!
每个人的最爱。 我们不要遵循JavaScript的示例,而要确保我们的代码抛出的任何错误中都包含有意义的消息。
是什么使错误消息有意义?
- 它应该描述问题所在
- 如果可能的话,它应该包括引起错误的任何变量值或其他数据
- 关键点:错误应该可以帮助我们找出问题出在哪里,因此可以作为有关该函数工作方式的文档。
句法
自我记录代码的与语法相关的方法可能与语言有关。 例如,Ruby和Perl允许您执行各种奇怪的语法技巧,通常应避免使用这些技巧。
让我们看一下JavaScript发生的一些情况。
不要使用语法技巧
不要使用奇怪的把戏。 这是使人迷惑的好方法:
imTricky && doMagic();
这等效于看起来更加理智的代码:
if(imTricky) {
doMagic();
}
始终喜欢后一种形式。 语法技巧不会给任何人带来任何好处。
使用命名常量,避免魔术值
如果您的代码中有特殊值(例如数字或字符串值),请考虑改用常量。 即使现在看来似乎很清楚,但通常在一两个月后又恢复到原来的水平时,没人会知道为什么将这个特定的数字放在那里。
const MEANING_OF_LIFE = 42;
(如果您不使用ES6,则可以使用var
,它将同样有效。)
避免布尔标志
布尔标志可以使代码难以理解。 考虑一下:
myThing.setData({ x: 1 }, true);
true
是什么意思? 除非您深入研究setData()
的源并找出setData()
,否则您绝对不知道。
相反,您可以添加另一个功能,或重命名现有功能:
myThing.mergeData({ x: 1 });
现在,您可以立即知道发生了什么。
充分利用语言功能
我们甚至可以使用所选语言的某些功能来更好地传达某些代码背后的意图。
JavaScript中的一个很好的例子是数组迭代方法:
var ids = [];
for(var i = 0; i < things.length; i++) {
ids.push(things[i].id);
}
上面的代码将ID列表收集到一个新数组中。 但是,为了知道这一点,我们需要阅读循环的整个主体。 与使用map()
进行比较:
var ids = things.map(function(thing) {
return thing.id;
});
在这种情况下,我们立即知道这会产生一个新的东西数组,因为那是map()
的目的。 如果您具有更复杂的循环逻辑,这将特别有益。 在MDN上还有其他迭代功能的列表 。
JavaScript的另一个示例是const
关键字。
通常,您在应该永远不更改值的位置声明变量。 一个非常常见的示例是使用CommonJS加载模块时:
var async = require('async');
我们可以使从未改变的意图更加明确:
const async = require('async');
作为一项额外的好处,如果有人不小心尝试更改此设置,我们现在将收到错误消息。
反模式
使用所有这些方法,您可以做很多事情。 但是,有些事情您应该注意...
为了简短起见而进行提取
有人提倡使用微小的微小函数,如果您将所有内容提取出来,那是可以得到的。 但是,这可能会影响代码的易读性。
例如,假设您正在调试一些代码。 您查看函数a()
。 然后,您发现它使用b()
,然后使用c()
。 等等。
虽然短函数可能很好并且易于理解,但是如果仅在单个位置使用该函数,则可以考虑使用“用变量替换表达式”方法。
不要强迫事情
和往常一样,没有绝对正确的方法来执行此操作。 因此,如果某些事情看起来不是一个好主意,请不要尝试强行执行。
结论
使您的代码自我记录对于提高代码的可维护性大有帮助。 每个评论都是必须保留的附加内容,因此在可能的情况下消除评论是一件好事。
但是,自记录代码不能代替文档或注释。 例如,代码在表达意图方面受到限制,因此您也需要提供良好的注释。 API文档对于库也非常重要,因为除非您的库很小,否则必须阅读代码是不可行的。
From: https://www.sitepoint.com/self-documenting-javascript/