【笔记】《编写可读代码的艺术》前端篇
Aug 21, 2018笔记代码规范《编写可读代码的艺术》前端篇
学会代码自然化,不被其他开发打。
命名
无论是命名变量、函数、类,都可以使用很多相同的原则。某种程度上来说,变量也是一种注释。核心:把信息装入名字中。
选择专业的词
命名时尽量选择专业词并且避免使用空洞的词。
- 比如有这么个函数为
getPage(url)
,我们虽然可以知道是获取一个页面,但是如果用fetchPage
或者downloadPage
进行更具体的动作区分命名会更好。 - 再比如有个函数为
getSize()
,更为形象化的话可以命名为getHeight()
、getNodeNum()
、getArrLength()
- 再比如有个函数为
stopAnimation()
,我们不知道它将是终止动画还是中止动画,我们可以命名为killAnimation()
、pauseAnimation()
。
下面是一些例子,这些单词可以使命名更有表现力且适合语境(还是得学好英语呐):
单词 | 更多选择 |
---|---|
send | deliver、dispatch、announce、distribute、route |
find | search、extract、locate、recover |
start | launch、create、begin、open |
make | create、set up、build、generate、compose、add、new |
避免使用像val、tmp和retval这样泛泛的名字
与其使用这样空洞的名字,不如挑一个能描述这个实体的值或者目的的名字。
如1
2
3
4
5
6
7
8function factorial (len) {
let retval = 0;
for (let i = 0; i < len; i++) {
retval += i;
}
return retval;
}
这里的retval我们可以用更贴切的名字,如sumMults,这样就提前声明了这个变量的目的。
泛泛名字的意义
有些情况下泛泛的名字也有承载着意义,就是说明当前语境下这个名字应该平平无奇。
如tmp用于短期存在且临时性为其主要存在因素的变量。如交换变量1
2
3
4
5if (right < left) {
tmp = right;
right = left;
left = tmp;
}
因为这个变量唯一的目的就是临时存储,它的整个生命周期只在几行代码之间,它没有其他职责。
当然我们可以在其基础之上丰富情景,如right和left都是数字,我们可以命名为tmpNum,right和left都是文件时,我们可以命名为tmpFile。
循环迭代器
像i、j、iter和it等名字常用作索引和循环迭代器,虽然很空泛但是大家都知道它们的意思。
当然我们可以在其基础之上丰富情景,如循环对象为人员名单数组,那么索引可以命名为people_i或peopleI,也可以简化为pi。
养成习惯多花几秒钟想出个好名字,你会发现你的“命名能力”很快提升。
用具体的名字替代抽象名字
假如你有个方法函数叫serverCanStart(),它检测服务是否可以监听某个给定的TCP/IP端口,然而我们可以命名为canListenOnPort(),这个名字直接地描述了这个方法要做什么事情。
如有个程序我们设置了命名标识叫做--run_locally
,可以看出是本地运行,但我可以更具体,如--extra_logging
、--use_local_database
。
为名字附带更多信息
如果一个变量有什么重要性信息,那么值得吧额外的词添加到名字中。如我们定义了个变量为id,如果它将是哈希值的话,可以改为hash_id,如果是二进制数值id,可以改为binary_id。
带单位的值
假如我们用Date对象写了一串代码用于统计执行时间:1
2
3
4
5let start = +new Date();
// runing...
let elapsed = +new Date() - start;
如果我们给两个变量结尾都追加_ms,可以让所有的地方更明确:1
2
3
4
5let start_ms = +new Date();
// runing...
let elapsed_ms = +new Date() - start;
除了时间,我们可能还有其他可带单位的命名,体积大小size_mb、网络限速max_kbps等等等等。
附带其他重要属性
对于这个变量存在危险或者意外的任何时候你都该采用它。
例如,很多安全漏洞来源于没有意识到你的程序接受到的某些数据还没有处于安全状态。这种情况下,你可能想要使用像untrustedUrl或者unsafeMessageBody这样的名字。在调用了查清不安全输入的函数后,得到的变量可以命名为trustedUrl或者safeMessageBody。
还可能有如下情景:
情形 | 变量名 | 更好的名字 |
---|---|---|
一个纯文本格式的密码,需要加密后才能进一步使用 | password | plaintext_password |
一条用户提供的注释,需要转义之后才能用于显示 | comment | unescaped_comment |
已转化为UTF-8格式的html字节 | html | html_utf8 |
以“url方式编码”的输入数据 | data | data_urlenc |
我记得高程书里有一种表示方法,是以前缀进行变量的命名,如数字变量a:n_a
,字符串变量b:s_b
,这种用标识的形式进行的变量命名也很有作用,不过需要熟悉标识规范。
名字应该有多长
d、days还是days_since_last_update呢?这个需要根据变量的使用场景,可以遵循以下几条原则:
在小的作用域里可以使用短的名字
当你去短期度假时,你带的行李通常会比长假少。同样,“作用域”小的标识符也不用带上太多信息。也就是说,因为所有的信息都很容易看到,所以可以用很短的名字。
如lookUpNames(m)。
如果一个标识符有较大的作用域,那么它的名字就要包含足够的信息以便含义更清楚。
输入长名字——不再是个问题
有很多避免使用长名字的理由,但“不好输入”这一条已经不再有效。了解下编辑器的单词补全功能。
首字符缩略词和缩写
例如把一个类命名为FEManager而不是FrontEndManager,这样会让人费解。
所以是否需要使用缩略词,取决于团队的新成员是否能理解这个名字的含义。
像eval可以代替evaluation,doc可以代替document,str可以代替string,然而像FEManager这种就可能会让人难以了解。
丢掉没用的词
有时候名字中的某些单词可以拿掉而不会损失任何信息。如convertToString()可以改为toString()。
利用名字的格式来传递含义
对于下划线、连字符和大小写的使用方式也可以把更多信息装到名字中。如首字符小写驼峰用于表示函数,如setUserName()
,首字符大写用于表示类(或构造函数),如TeamManager
,用小写字符与下划线连接表示变量,如is_user
,全大写字符与下划线连接表示常量,如LIBRARY_NAME
。
特殊的,如jQuery/Zepto变量可以加上$,如1
let $btn = $('#submit_btn');
css里的规范可参考Moo-css
不会误解的名字
要多问自己几遍:“这个名字会被别人解读成其他的含义吗?”,要仔细审视这个名字。
比如filter的含义可能是“挑出”,也有可能是“减掉”。
推荐用min和max来表示(包含)极限
命名极限最清楚的方式是在摇限制的东西前加上max_或min_。
如:MAX_ITEMS_IN_CART表示购物车的最大限制。
推荐用first和last来表示包含的范围
对于表示包含的范围,first/last比start/end或begin/end效果要好。
推荐用begin和end来表示包含/排除范围
给布尔值命名
当为布尔值变量或者返回布尔值的函数选择名字时,要确保返回true和false的意义很明确。
如定义这样一个布尔变量read_password
,这会有两种截然不同的解释:
- 我们需要读取密码
- 已经读取了密码
因此这个例子里最好避免使用“read”这个词,用need_password或user_is_authenticated这样的名字来代替。
通常来讲,加上像is、has、can或should这样的词可以把布尔值变得更明确。
最好避免使用反义名字。如disable_ajax不如use_ajax好。
与使用者的期望相匹配
有些名字之所以会让人误解是因为用户对它们的含义有先入为主的印象,就算你的本意并非如此。这种情况下,最好放弃这个名字而改用一个不会让人误解的名字。
比如get,在设计大量计算的话我们可以重命名为compute,后者更像是有些代价的操作。
当你要选一个好名字时,可能会同时考虑多个备选方案,通常你要在头脑中盘算一下每个名字的好处,然后才能得出最后的选择。
可以参考下面这个命名网站:https://codeif.xinke.org.cn。当然它也有编辑器插件。
审美
代码的排版:
- 使用一致的布局,让读者很快就习惯这种风格
- 让相似的代码看上去相似
- 把相关的代码行分组,形成代码块
重新安排换行来保持一致和紧凑
比如我们定义某个方法:1
2
3function getTime () {
return new Date(2019, 1, 1, 1, 1, 1, 1)
}
很明显我们很难直观了解后面这堆1的含义,那么可以通过换行和注释提高可读性1
2
3
4
5
6
7
8
9
10
11function getTime () {
return new Date(
2019, /* year */
1, /* month */
1, /* date */
1, /* hour */
1, /* minute */
1, /* second */
1 /* milliseconds */
)
}
或者为了保持紧凑,可以如下1
2
3
4function getTime () {
// Date(year, month, date, hour, minute, second, milliseconds)
return new Date(2019, 1, 1, 1, 1, 1, 1)
}
用方法来整理不规则的东西
简单来说就是用抽象方法来处理一系列东西,实现如下的代码形式1
2
3handleSomething(argumentA, argumentB, argumentC);
handleSomething(argumentD, argumentE);
handleSomething(argumentD);
尽管我们的目的仅仅是让代码更有美感,但这样的好处还有几个附带效果:
- 它会消除大量的重复,让代码更紧凑
- 标识变得很直白
- 添加新内容/处理更简单
“看上去漂亮”通常会带来不限于表明层次的改进,它可能会帮你把代码的结构做得更好。
在需要时使用对齐
整齐的边和列让读者可以轻松地浏览文本。
如:1
2
3
4checkFullName('Micheal Wayne', 'Mr. Micheal Wayne', '');
checkFullName('Roger Federer', 'Mr. Roger Federer', '');
checkFullName('No Such Guy' , '' , 'no match found');
checkFullName('john' , '' , 'more than one result');
又如1
2
3
4details = data.details;
location = data.location;
phone = data.phone;
url = data.url;
还如1
2
3
4
5
6obj = {
{ key: 'timeout', value: null, desc: 'cmd_timeout' },
{ key: 'tries', value: 'opt.ntry', desc: 'number_inf' },
{ key: 'useproxy', value: 'boolean', desc: 'use_proxy' },
{ key: 'useragent', value: null, desc: 'spec_useragent' },
};
不过有些程序员不喜欢这种方式,因为建立和维护对齐的成本很大,改动一行可能会牵动其他所有行。如果真的很费工夫,可以不这么做。
选一个有意义的顺序,始终一致地使用它
原则:
- 让变量的顺序与HTML结构对应,如表单变量
- 从最重要到最不重要排序
- 按字母排序
如一个表单:1
2
3
4
5
6
7
8
9
10<form>
<input id="name"/>
<input id="sex"/>
<input id="email"/>
</form>
<script>
var name = document.getElementById('name').value;
var sex = document.getElementById('sex').value;
var email = document.getElementById('email').value;
</script>
把声明按块组织起来
我们的大脑很自然地会按照分组和层次结构来思考,因此你可以通过这样的组织方式来帮助读者快速地理解你的代码。
如1
2
3
4
5
6
7a();
b();
c();
d();
e();
f();
g();
根据逻辑分组后1
2
3
4
5
6
7
8
9
10
11
12// get info
a();
b();
c();
// set header
d();
e();
// set footer
f();
g();
把代码分成“段落”
书面文字要分为段落是由于一下几个原因:
- 它是一种把相似的想法放在一起并与其他想法分开的方法。
- 它提供了可见的“脚印”,如果没有它,会很容易找不到你读到哪里了。
- 它便于段落之间的导航。
因为同样的原因,代码也应当分成“段落”,如上例。
个人风格与一致性
如定义函数类,写惯了c的会有下习惯1
2
3
4
5
6
7
8
9function func ()
{
// ...
}
class ClassA
{
// ...
}
还有一部分人1
2
3
4
5
6
7function func () {
// ...
}
class ClassA {
// ...
}
选择一种风格而非另一种,不会真的影响到代码的可读性。但如果把两种风格混到一起就不好了。一致性的风格比“正确”的风格更重要。
该写什么样的注释
注释的目的是尽量帮助读者了解得和作者一样多
什么不需要注释
如下面注释都是没有价值的:1
2
3
4
5
6
7// The class definition for Account
class Account {
// Constructor
constructor () {
this.Account();
}
}
这些注释没有价值是因为它们并没有提供任何新的信息,也不能帮助读者更好地理解代码。不要为那些从代码本身就能快速腿短的事实写注释。
如下:1
2// remove everything after the '*'
name = '*'.join(line.split('*')[1]);
虽然这里的注释也没有表达出任何信息,不过它能让人明白它到底在做什么。
不要为了注释而注释
比如函数的声明跟注释一样的情况。如果要写注释,它最好能给出更多重要的细节。
不要给不好的名字加注释——应该把名字改好
通常来讲,我们不需要“拐杖式注释”——试图粉饰可读性差的代码的注释。写代码的人常常把这条规则表述成:好代码 > 坏代码 + 好注释
记录你的思想
很多好的注释仅通过“记录你的想法”就能得到,也就是那些你在写代码时有过的重要想法。
加入“导演评论”
电影中常有这部分,电影制作者在其中给出自己的见解并通过讲故事来帮助你理解这部电影是如何制作的。
如:1
2// 出乎意料的是,对于这些数据用二叉树比用哈希表快40%
// 哈希运算的代价比左/右比较大得多
这段注释教会读者一些事情,并且防止他们为无谓的优化而浪费时间。
又如:1
2// 这个类正在变得越来越乱
// 也许我们应该建立一个'ResourceNode'子类来帮助整理
一目了然,需要后人或以后的自己来优化。
为代码中的瑕疵写注释
要好意思把写代码过程中的瑕疵记录下来
其中有几个标记很流行:
标记 | 通常的意义 |
---|---|
TODO | 我还没有处理的事情 |
FIXME | 已知的无法运行的代码 |
HACK | 对一个问题不得不采用的比较粗糙的解决方案 |
XXX | 危险,这里有重要的问题 |
我们还可以用小写或添加前缀作为比较来表示重要优先级,如todo、maybe-later:todo。
1 | // TODO: 换个更快的算法 |
重要的是你应该可以随时把代码将来应该如何改动的想法用注释记录下来。这种注释给读者带来对代码质量和当前状态的宝贵见解,甚至可能会给他们指出如何改进代码的方向。
给常量加注释
虽然很多常量名字本身已经很清楚感觉不需要注释,但是可以通过加注释得以改进,比如定义这个常量时的想法。
站在读者的角度
想象你的代码对于一个并不了解此项目的开发来讲看起来是什么样子的。
我们可以注释有疑惑的点或者可能的陷阱,以及类/文件/业务直接的交互或总结。
最后的思考——克服“作者心理阻滞”
步骤:
- 不管心里想什么,先写下来
- 读一下这段注释,看看有没有什么地方可以改进
- 不断改进
写出言简意赅的注释
如果你要写注释,最好把它写得精确——越明确和细致越好。另外,由于注释子啊屏幕上也要占很多地方,并且需要花费更多的时间来读。因此注释也需要很紧凑。注释应当有很高的信息/空间率
让注释保持紧凑
避免使用不明确的代词
代词可能会让事情变得令人困惑。读者需要花费更多的工夫来解读一个代词,比如一些情况下,it或者this到底指代什么是不清楚的。如1
// Insert the data into the cache, but check if it's too big first.
可以改成1
// Insert the data into the cache, but check if the data is too big first.
润色粗糙的句子
精确地描述函数的行为
用输入/输出的例子来说明特别的情况
声明代码的意图
如1
connect(10, false);
看上去难以理解,可以改为:1
connect(/*timeout_ms = */10, /*use_async = */false);
采用信息含量高的词
(学好英语,精练语句)
简化循环和逻辑
把控制流变得易读
条件语句中参数的顺序
如1
if (length > 10)
比1
if (10 < length)
更易读。
1 | while(bytes_received < bytes_expected) |
比1
while(bytes_expected > bytes_received)
更易读。
为什么呢,下面这条指导原则很有帮助:
比较的左侧 | 比较的右侧 |
---|---|
“被询问的”表达式,它的值更倾向于不断变化 | 用来比较的表达式,它更倾向于常量 |
避免尤达表达法
如1
if (obj = null) ...
读者会很自然得理解为if (obj == null)
if/else语句块的顺序
逻辑
- 首先处理正逻辑而不是负逻辑。例如用
if (debug)
而不是if (!debug)
- 先处理掉简单的情况。这种方式可能还会使得if和else在屏幕之内都可见。
- 先处理有趣或者可疑的情况。
有时这些倾向性之间会有冲突,那么就需要自己判断了。
三目?:表达式
三目表达式对于可读性的影响是有争议的。拥护者认为这种方式可以只写一行而不用写成多行。反对者这说这可能会造成阅读的混乱并且很难用调试器来调试。如何选择,一个较好的度量方法就是根据最小化人们理解它们所需的时间。
因此默认情况都使用if/else,三目运算符只有在最简单的情况下使用。
避免do/while循环
do/while的奇怪之处是一个代码块是否执行是由其后的一个条件来决定的。通常来讲逻辑条件应该出现在它们所“保护”的代码之前。因此do/while会显得有点不自然,而while循环相对更易读。另一个要避免do
/while的原因是其中的continue会让人迷惑。如1
2
3do {
continue;
} while (false);
“我的经验是,do语句是错误和困惑的来源……我倾向于把条件放在“前面我能看到的地方”。其结果是,我倾向于避免使用do语句。” ——《C++程序设计语言》
从函数中提前返回
有些程序员认为函数中永远不应该出现多条return语句,我认为这是不合理的。从函数中提前返回没有问题,而且常常很受欢迎。如1
2
3
4function func (str, num) {
if (!str || !num) return false;
return str + num;
}
这里可以称为保护语句(guard clause),不用的话反而会不自然。
想要单一出口点的一个动机是保证调用函数结尾的清理代码。但现代的编程语言为这种保证提供了更精细的方式:
语言 | 清理代码的结构化术语 |
---|---|
C++ | 析构函数 |
Java、Python | try finally |
Python JavaScript | with |
C# | using |
臭名昭著的goto
还好js没有,不解释。
最小化嵌套
嵌套很深的代码向来难以理解。每个嵌套层次都在读者的“思维栈”上又增加了一个条件。当读者见到一个右大括号(})时,可能很难“出栈”来回忆起它背后的条件是什么。无论是条件判断还是请求回调都要注意这点。
嵌套是如何累积而成的
往往是开发到需要插入一段新代码的时候,往往在最容易的地方插入。对当时的开发者来讲这个新代码很整洁也很明确,但当以后其他人遇到这段代码时,当时所有的上下文早已不在了,他不得不一下子全盘接受代码逻辑。如何避免呢:当你对代码做改动时,从全新的角度审视它,把它作为一个整体来看待。
通过提早返回来减少嵌套
如1
2
3
4
5
6
7
8
9
10
11
12
13if (//...) {
// ...
return;
}
if (//...) {
// ...
return;
}
if (//...) {
// ...
return;
}
// ...
减少循环内的嵌套
提早返回这个技术并不总是合适的,在循环中与提早返回类似的技术是continue。一般来讲continue语句让人很困惑,因为它让读者不能连续得阅读,但在某些语境下读者可以很容易地领悟到continue的意思就是“跳过该项”
你能理解执行的流程吗
不要滥用新功能。
拆分超长的表达式
简单来说代码中的表达式越长,它就越难以理解。把你超长表达式拆分成更容易理解的小块。
用作解释的变量
拆分表达式最简单的方法就是引入一个额外的变量,让它表示一个小一点的子表达式。这个额外的变量称为“解释变量”。如
1 | if (url.split(':')[1].toUpperCase() === 'root') { |
可写为1
2
3
4let username = url.split(':')[1].toUpperCase();
if (username === 'root') {
// ...
}
总结变量
即使一个表达式不需要解释,你可能明显看出它的含义,但把它装入一个新变量中仍然有用。我们把它称为“总结变量”。它的目的只是用一个短很多的名字来代替一大块代码,这个名字会更容易管理和思考。
如:1
2
3
4
5
6
7
8
9if (request.user.id === document.ower_id) {
// ....
}
// ...
if (request.user.id !== document.ower_id) {
// ...
}
可改为:1
2
3
4
5
6
7
8
9
10
11let user_owns_document = request.user.id === document.ower_id;
if (user_owns_document) {
// ...
}
// ...
if (!user_owns_document) {
// ...
}
使用德摩根定理
对于一个布尔表达式,有两种等价的写法:
- not (a or b or c) <=> (not a) and (not b) and (not c)
- not (a and b and c) <=> (not a) or (not b) or (not c)
有时候利用这个定理会让布尔表达式更具可读性。如1
if (!(data_exist && !is_protected)) { // ... }
可改为:1
if (!data_exist || is_protected) { // ... }
滥用短路逻辑
包括js的很多编程语言中,布尔操作会做短路逻辑。如if (a || b)
会在a为真值时不会计算b。这种方式很方便但有时可能会被滥用以实现复杂逻辑。
如:1
2let tmp;
if (!(tmp = getTypeA()) || !(tmp = getTypeB())) { // ... }
虽然和简洁但需要花一段时间来理解,这样的话可能还不如1
2
3
4
5let tmp;
tmp = getTypeA();
if (!tmp) tmp = getTypeB();
if (!tmp) // ...
要小心“智能”的小代码,它们往往在以后会让别人读起来感到困惑。
变量和刻度性
会讨论三个问题:
- 变量越多,就越难全部跟踪它们的动向
- 变量的作用域越大,就需要跟踪它的动向越久
- 变量改变得越频繁,就越难以跟踪它的当前值。
减少变量
减少不能改进可读性的变量。
没有价值的临时变量。
如:1
2let now = data.nowdate;
let view_time = now;
减少中间结果
如一个函数用来从数组中删除一个值(不考虑所有):1
2
3
4
5
6
7
8
9
10
11
12
13function remove_one (array, value_to_remove) {
let index_to_remove = null;
for (let i = 0, len = array.length; i < len; i++) {
if (array[i] === value_to_remove) {
index_to_remove = i;
break;
}
}
if (index_to_remove !== null) {
array.splice(index_to_remove, 1);
}
}
看起来很完美,但在这里我们可以进一步优化:1
2
3
4
5
6
7
8function remove_one (array, value_to_remove) {
for (let i = 0, len = array.length; i < len; i++) {
if (array[i] === value_to_remove) {
array.splice(i, 1);
return;
}
}
}
通常来讲,速战速决是一个好的策略。
减少控制流变量
有时候你会看到这样的循环代码:1
2
3
4
5
6
7
8
9let done = false;
while (/* 条件 */ && !done) {
// ...
if (//...) {
done = true;
continue;
}
}
像done这样的变量,称为“控制流变量”。它们唯一的目的就是控制程序的执行——它们没有包含任何程序的数据。通常控制流变量可以通过更好地运用结构化编程而消除。
1 | while (/* 条件 */) { |
如果是在复杂的情况,如多层嵌套下一个break不能满足怎么办呢。解决方法通常是把代码挪到一个新函数中(要么是循环中的代码,要么是整个循环)
缩小变量的作用域
我们都听说过“避免全局变量”这条建议,因为很难跟踪这些全局变量在哪以及如何使用它们。并且通过“命名空间污染”,代码可能会意外地改变全局变量的值而导致发生问题。实际上,让所有的变量都缩小作用域是一个好主意,并非只是针对全局变量。原则:让你的变量对尽量少的代码行可见。
原因也很简单,因为这样有效地减少了读者同时需要考虑的变量个数。如果你能把所有的变量作用域都减半,那么这就意味着同时需要思考的变量个数平均来讲是原来的一半。
有几个较好的方案:
- 大文件拆分成小文件,大类拆分为小类,大函数拆分为小函数
- 尽量使用静态方法或属性
- 变量私有化(js通常要利用闭包)
只写一次的变量更好
操作一个变量的地方越多,越难确定它的当前值。永久固定的变量更容易思考,如常量。
重新组织代码
抽取不相关的子问题
所谓工程学就是关于把大问题拆分成小问题再把这些问题的解决方案放回一起。这里的建议是“积极地发现并抽取出不相关的子逻辑”,如
- 看看某个函数或代码块,想一下:这段代码高层次的目标是什么?
- 对于每一行代码,问一下:它是直接为了目标而工作吗?这段代码高层次的目标是什么呢?
- 如果足够的行数在解决不相关的子问题,抽取代码到独立的函数中。
这里重点关注抽取不相关的子问题。
如有个函数作用是找到距离给定点最近的位置。函数如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23function findClosetLocation (lat, lng, array) {
let closest;
let closest_dist = Number.MAX_VALUE;
for (let i = 0, len = array.length; i < len; i++) {
let lat_rad = radians(lat);
let lng_rad = radians(lng);
let lat2_rad = radians(array[i].latitude);
let lng2_rad = radians(array[i].longitude);
let dist = Math.acos(Math.sin(lat_rad) * Math.sin(lat2_rad) +
Math.cos(lat_rad) * Math.cos(lat2_rad) *
Math.cos(lng2_rad - lng_rad));
if (dist < closest_dist) {
closest = array[i];
closest_dist = dist;
}
}
return closest;
}
逻辑清晰,但循环中大部分代码都旨在解决一个不相关的子问题:计算两个坐标点之间的距离。抽1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25function spherical_distance (lat1, lng1, lat2, lng2) {
let lat_rad = radians(lat1);
let lng_rad = radians(lng1);
let lat2_rad = radians(lat2);
let lng2_rad = radians(lng2);
return Math.acos(Math.sin(lat_rad) * Math.sin(lat2_rad) +
Math.cos(lat_rad) * Math.cos(lat2_rad) *
Math.cos(lng2_rad - lng_rad));
}
function findClosetLocation (lat, lng, array) {
let closest;
let closest_dist = Number.MAX_VALUE;
for (let i = 0, len = array.length; i < len; i++) {
if (dist < closest_dist) {
closest = array[i];
closest_dist = dist;
}
}
return closest;
}
纯工具代码
即一组核心任务代码,弥补现有功能上的不足。
创建大量通用代码
从项目中拆分出越多的独立代码越好,因为你代码的其他部分会更小而且更容易思考。
自顶向下和自底向上的编程方式。自定向下:先设计高层次模块和函数,然后根据支持他们的需要来实现低层次函数。自底向上:尝试首先预料和解决所有的子问题,然后用这些代码段来简历更高层次的组件。大多数编程都包括了两者的组合,重要的是最终的结果:移除并单独解决子问题。
简单来说,把一般代码和项目专有代码分开,通过建立一大组库和辅助函数来解决一般问题,剩下的就是该程序与众不同的核心业务部分。
一次只做一件事
一个代码块可能初始化对象,清楚数据,解析输入,然后应用业务逻辑,所有这些都同时进行。如果所有这些代码都纠缠在一起,对于每个人物都很难靠其自身来帮你理解它从哪里开始到哪里结束。应该把代码组织得一次只做一件事情。
一次只做一件事的流程:
- 列出代码所做的所有任务。这里的任务没有很严格的定义——它可以小得如确保这个对象有效,或者含糊得如遍历树种所有节点。
- 尽量把这件任务拆分到不同的函数中,或者至少是代码中不同的段落。
如某个投票插件,我们可以把解析数值和更新分数两件事分开。
从对象中抽取值
把想法变成代码
如果你不能把一件事解释给你祖母听的话说明你还没有真正理解它。 ——阿尔伯特.爱因斯坦
类似于橡皮鸭技术。
基本思路:
- 像对着一个同事一样用自然语言描述代码要做什么
- 注意描述中所用的关键词和短语
- 写出描述所匹配的代码
清楚地描述逻辑
少写代码
知道什么时候不写代码可能对于一个程序员来讲是他所要学习的最重要的技巧。最好读的代码就是没有代码。
别费神实现那个功能——你不会需要它
当你开始一个项目,自然会很兴奋并且想着你希望实现的所有很酷的功能。但是程序员倾向于高估有多少功能真的对他们的项目来讲是必不可少的。很多功能结果没有完成,或者没有用到,也可能只是让程序更复杂。
质疑和拆分你的需求
不是所有的程序都有需要运行得快,100%准确,而且能处理所有的输入。如果你真的仔细检查你的需求,有时你可以把它削减成一个简单的问题,只需要较少的代码。重新考虑需求,解决版本最简单的问题,只要能完成工作。
如要写个商店的定位器,你认为的需求是:对于任何给定用户的经纬度,找到距离该经纬度最近的商店。
为了100%实现,你要处理:
- 当位置处于国际日期分界线两侧的情况
- 接近北极或南极的位置
- 按每英里所跨经度不同,处理地球表面的曲度
如果你的应用范围只有杭州的几十家店,那么上面的三个问题并不重要,需求可以缩减为:对于杭州附近的用户,在杭州找到最近的商店。
解决这个问题很简单,因为你只要遍历每个商店并计算它们与这个经纬度之间的欧几里得距离就可以了。
保持小代码库
在你第一次开始一个软件项目,并且只有一两个源文件时,一切都很顺利。编译和运行代码转眼就完成,很容易做改动,并且很容易记住每个函数或类定义在哪里。
然后随着项目的增长,你的目录中加进了越来越多的源文件。很快你就需要多个目录组织来组织他们了。很难再记得哪个函数调用了哪个函数,并且跟踪bug也需要做更多的工作。
最后你就有了很多源代码分布在很多不同的目录中。项目很大维护很是麻烦。
宇宙的自然法则——随着任何坐标系统的增长,把它粘合在一起所需的的复杂度增长得更快。
最好的解决办法就是“让你的代码库越小,越轻量级越好”。
- 创建越多越好的“工具”代码来减少重复代码
- 减少无用代码或没有用的功能
- 让你的项目保持分开的子项目状态
- 总的来说,要小心代码的重量,让它保持又轻又灵。
好比园丁经常修剪植物,修剪掉碍事和没用的代码也是个好主意。
熟悉你周边的库
每隔一段时间,花15分钟来阅读标准库中的所有函数/模块/类型的名字。也就是熟悉语法跟常用库,目的不是记住这些库而是为了了解各个库的用途。([前端开发常用文档/网站地址、样式/js方法封装库、项目模板。
测试与可读性
这里的测试指任何仅以检查另一段(“真实”)代码的行为为目的的代码。
使测试易于阅读和维护
测试代码的可读性和非测试代码是同样重要的。其他程序员会经常来把测试代码看做非正式的文档,它记录了真实代码如何工作和应该如何使用。
使这个测试更可读
对使用者隐去不重要的细节,以便更重要的细节会更突出。
创建最小的测试声明
大多数测试的基本内容都能精炼成“对于这样的输入(情形),期望有这样的输出(行为)”,并且很多时候这个目的可以用一行代码来表达。这除了让代码紧凑又易读,让测试的表述保持很短还会让增加测试变得很简单。
实现定制的“微语言”
让错误消息具有可读性
选择好的测试输入
基本原则是,你应当选择一组最简单的输入,它能完整地使用被测代码。
- 简化输入值
- 一个功能的多个测试:与其建立单个“完美”输入来完整地执行你的代码,不如写多个小测试,后者往往会更容易、更有效并且更有可读性。每个测试都应把代码推往某一个方向,尝试找到某种bug。
为测试函数命名
为测试函数选择一个好名字可能看上去很无聊而且也无关紧要,但是不要因此取一些如test()、test1()这样的名字。
反而,你应当用这个名字来描述这个测试的细节。如果读测试代码的人可能很快搞明白这些的话,这一点尤其便利:
- 被测试的类
- 被测试的函数
- 被测试的情形或bug
一个构造好的测试函数名的简单方式是把这些信息拼接在一起,可能再加一个“test_”前缀。
一般格式为1
test_<FunctionName>
或追加更详细的情形1
test_<FunctionName>_<Situation>
对测试比较好的开发方式
有些代码比其他代码更容易测试,对于测试来讲理想的代码要有明确定义的接口,没有过多的状态或者其他的“设置”,并且没有很多需要审查的隐藏数据。
对测试友好的设计往往很自然地会产生有良好组织的代码,其中不同的部分做不同的事情。
测试驱动开发TDD
一种编程风格,子啊写真实代码之前就写出测试。
在所有的把一个程序拆分成类和方法的途径中,解耦合最好的那一个往往就是最容易测试的那个。
可测试性差的代码的特征,以及它所带来的设计问题
特征 | 可测试性的问题 | 设计问题 |
---|---|---|
使用全局变量 | 对于每个测试都要重置所有的全局状态(否则,不同的测试之间会相互影响) | 很难理解哪些函数有什么副作用。没办法独立考虑每个函数,要考虑整个程序才能理解是不是所有的代码都能工作 |
对外部组件有大量依赖的代码 | 很难给它写出任何测试,因为要先搭起太多的脚手架。写测试会比较无趣,因此人们会避免写测试 | 系统会更可能因某一依赖失败而失败。对于改动来讲很难知道会产生什么样的影响。很难重构类。系统会有更多的失败模式,并且要考虑更多恢复路径。 |
代码有不确定的行为 | 测试会很古怪,而且不可靠。经常失败的测试最终会被忽略 | 这种程序更可能会有条件竞争或其他难以重现的bug。这种程序很难推理。产品中的bug很难跟踪和改正。 |
可测试性较好的代码的特征,以及它所产生的优秀设计
特征 | 对可测试性的好处 | 对设计的好处 |
---|---|---|
类中只有很少或者没有内部状态 | 很容易写出测试,因为要测试一个方法只要较少的设置,并且有较少的隐藏状态需要检查 | 有较少状态的类更简单,更容易理解 |
类/函数只做一件事 | 要测试它只需要较少的测试用例 | 较小/较简单的组件更加模块化,并且一般来讲系统有更少的耦合 |
每个类对别的类依赖很少;低耦合 | 每个类可以独立地测试 | 系统可以并行开发。可以很容易修改或者删除类,而不会影响系统的其他部分 |
函数的接口简单,定义明确 | 有明确的行为可以测试。测试简单接口所需的工作量较少 | 接口可以更容易让程序员学习,并且重用的可能性更大 |
但注意不要走火入魔:
- 牺牲真实代码的可读性,只是为了使能测试
- 着迷于100%的测试覆盖率
- 让测试称为产品开发的阻碍
Author
My name is Micheal Wayne and this is my blog.
I am a front-end software engineer.
Contact: michealwayne@163.com