[译] 我在编程初级阶段常犯的错误

学会辨识错误,养成习惯去避免它们

我要先声明一点,如果你是一个初级程序员,本文并非要让你因为你可能正在犯这些错感到蓝瘦香菇,而是想让你意识到它们,学会如何辨识这些错误,并且提醒你避免犯这些错。

过去我经常犯这类错误,从每一个错误中我都吸取了很多教训。可喜的是如今我已经养成了很好的编程习惯,这些习惯能帮我避免再次犯同样的错。你也应该尝试着去这样做。

以下错误排名不分先后。

1)毫无计划地写代码

高质量的写作内容大都不那么容易产出,它要求仔细的思考和研究。高质量的程序代码也不例外。

编写高质量代码是有一个工作流的:
思考调研计划编码验证修改
很遗憾,这个工作流没有一个很好的(英文)首字母缩写来帮助记忆。你需要养成好的习惯来履行工作流中的各个阶段,一个都不能少

在我编程初级阶段的时候,我犯过最严重的错误之一就是写代码之前没有思考和调研。虽然这在小型独立的项目中能够奏效,但在更大的工程中就会有很严重的负面影响了。

在说出你可能会后悔的话之前,你要三思而后说。编程也一样,在你写下可能会后悔的代码之前,那你就要三思而后写了。代码也是交流思想的一种方式。

生气的时候,如果要说话,先从 1 数到 10。如果非常生气,那就数到 100。

— Thomas Jefferson.

套用一下这句话:

复审代码的时候,如果要重构代码,先从 1 数到 10。如果没有测试代码,那就数到 100。

— Samer Buna

编程大部分情况下都是关于如何阅读以前的代码,调研需求及其如何适应当前系统,以及使用可测试的增量代码实现新功能。实际写代码的时间在整个过程中可能才占比 10% 。

不要觉得编程就是写一行一行的代码。编程是一个有逻辑的创造过程,是需要培养教育的。

2)写代码前过度计划

是的。在一头钻进代码前做点计划是好事,但是即便是好事,也可能物极必反。喝太多的水都会使你中毒呢。

在编程的世界里寻找完美的计划?不存在的。要寻找一个足够好的计划,足够让你启动项目就行了。因为计划总是赶不上变化,但是计划可以推动你向有组织的方向进行,这会使你的代码更清晰。计划太多不过是浪费时间而已。

我现在讲的都只是对于小功能的计划,想在一开始就计划好所有功能的做法就更不可取!这在软件工程中被叫做瀑布模型,是一种系统线性的计划,每一个步骤都按顺序完成。可以设想这种瀑布模型需要多少计划啊。这不是这里所讨论的计划类型。瀑布模型在大多数软件工程中都是无效的。任何复杂的事物都只能根据现实灵活适应来实现。

编写程序必须是一个响应的过程。你会添加以前从未想过的新功能,这在瀑布模型中是无法想象的(译注:因为瀑布模型把整个生命周期都计划好了,不会有意外的功能),你也会因为从未想过的原因移除一些功能。你需要修复 bug 以适应变化。你需要灵活一些。

虽然如此(不能过度计划),但一定要计划一下后续的少数新功能。要很小心地计划,因为不足或者过度的计划都可能损害你代码的质量,可不能拿代码质量来冒风险。

3)低估代码质量的重要性

如果你只能够关注你所写的代码的一个方面,那么肯定是可读性。表意不明的代码就是垃圾,甚至是不可回收的垃圾。

永远都不要低估代码质量的重要性。把编写代码看作是一种沟通实现的方式。作为程序员最主要的工作就是清晰地交流当前解决方案的实现。

关于编程,我最喜欢的名言之一是:

总是以这样的心态写代码:仿佛最终维护你代码的那个人是个变态暴力狂,他知道你住在哪里。

— John Woods

Jonh, 真是个明智的建议!

即便是微小的细节也很重要。举个例子,如果你在你的代码中,缩进和大小写的风格不一致,你简直就该被“吊销编程执照”。

1
2
3
4
5
tHIS is
WAY MORE important

than
you think

另一点是关于长行的使用。任何超过 80 个字符的代码行都要难读很多。你可能试图把一些很长的条件判断放在同一行,好让 if 语句更清晰,别这么做。永远都不要让一行代码超过 80 个字符,永远都不要。

很多这样的简单问题都可以通过 lintingformatting 工具修复。在 JavaScript 中,有两个非常棒的工具可以完美协作:ESLintPrettier。给自己行行好,把它们用起来吧。

以下还有一些和代码质量相关的误区:

  • 在一个方法或一个文件中写非常多行代码。你应该总是把很长的代码拆分成小的片段,以便测试和单独管理。我个人认为超过 10 行的方法都算太长了,但这只是个经验之谈。

  • 使用双重否定。拜托,请别不要不这么做(译注:此处故意使用双重否定╮(╯▽╰)╭)。

使用双重否定也并不是不会错(译注:此处作者故意使用双重否定)

  • 使用短而通用、或基于类型的变量名。给你的变量一个描述性和没有歧义的名字。

计算机科学中只有两件难事:清除缓存和命名。

— Phil Karlton

  • 毫无描述地硬编码字符串字面量和数字字面量(译注:即“魔数”)。如果你需要写一些依赖固定字面量字符串值或数字值的代码,那就使用常量保存这些固定值,并起一个好变量名。
1
const answerToLifeTheUniverseAndEverything = 42;
  • 使用邋遢的捷径和奇技淫巧避免在简单问题上花费更多时间。不要与问题共舞,直面现实吧。

  • 觉得代码越长越好。然而在大多数情况下,代码是越短越好。只有以代码可读性更强为前提,才用长代码的写法。举个例子,不要内嵌大量三目运算符(?:)来让代码变短。当然,也不要有意让代码变得没必要的冗长。删除没必要的代码在任何项目中都是你能做的最好的事。

使用代码行数来衡量编程进度就像使用重量来衡量飞行器的建造进度一样。

— Bill Gates

  • 过度使用条件逻辑。你觉得需要条件逻辑的大多数情况都可以不使用条件逻辑来实现。考虑所有的代替方案,基于可读性,仅仅选择其中的一种。除非你已经可以测量性能了,否则不要优化性能。相关:避免 Yoda conditions 和根据条件赋值。

4)使用初次方案

还记得在我刚开始编程的时候,当我遇到一个问题时,我能找到一个解决方案然后马上就投入这个方案中,我急忙忙地实现它,没有考虑过这个初次方案复杂度和潜在的失败情况。

虽然初次方案可能很诱人,但是好的方案却往往是在你开始盘查多种方案时才发现的。如果你没办法找出这个问题的多种解决方案,那很可能是你没有完全明白这个问题。

作为专业的程序员,你的职责不是找出问题的一个解决方案,而是找出问题的最简单的解决方案。我所说的“简单的解决方案”是指这个解决方案必须是准确的,性能足够好,还要简单易读、易理解和易维护。

有两种方式来构建软件设计。一种是把它做得足够简单以至于明显没有缺陷,另一种是把它做得足够复杂以至于没有明显的缺陷。

— C.A.R. Hoare

5)不放弃

另一个我经常犯的错误是我坚持我的初次解决方案,即使我已经确认了这可能不是最简单的解决方案。这可能就是心理学上所说的“不放弃”心态吧。这在大多数活动中是一种好的心态,但在编程中却不适用。事实上,当说到编程的时候,正确的心态应该是尽快失败和多多失败

当你开始怀疑一个解决方案的时候,你就应该考虑抛弃它,并且重新思考这个问题。不管你已经在这个解决方案中投入了多少精力。像 GIT 这样的版本控制系统能够帮助你分开管理和尝试多种不同的解决方案,把它利用起来吧。

不要因为你在代码中花费了很多精力就为它着了魔。坏的代码就应该被丢弃。

6)不谷歌(译注:不使用搜索引擎)

我花费了很多宝贵的时间去尝试解决一个问题,但我本可以简单地使用搜索引擎就能搜索到结果,这样的例子数不胜数。

除非你正在使用一种极其边缘的技术,否则当你遇到一个问题时,很可能别人早就遇到过同样的问题了,并且也找到了解决方案了。给自己省点时间,先 Google 一下

有时候,Google 一下可能会披露这样一个事实:你觉得这是个问题但其实那并不是,你需要做的并非修复它,而是拥抱它。也不要觉得你知道了寻找解决方案必备的所有知识,Google 会让你吃惊。

尽管如此,你在 Google 搜索时需要小心。一个新手的标志就是在没有理解的情况下就复制粘贴别人的代码,尽管这些代码可能正确地解决了你的问题,但你永远都不应该使用你没有完全理解的代码,哪怕只有一行。

如果你想成为一个有创造性的程序员,不要以为你知道自己在做什么。

作为一个有创造力的人,最危险的想法就是以为你知道自己在做什么。

— Bret Victor

7)没有封装

这一点不只是关于面向对象范式的。使用封装总是有用的。不使用封装往往导致难以维护的系统。

在一个应用中,一个功能应该只在一个地方被处理,这通常就是一个对象应有的职责,这个对象应该只向其他需要用到它的对象暴露必须的接口。这无关乎机密,而是为了降低一个应用中的不同部分之间的相互依赖。坚持这些法则能够让你安全地改变你的类、对象、函数的内部实现,而不用担心破坏了修改之处外更大范围的东西。

概念的逻辑单元和状态应该有他们自己的。我说的类是指一个蓝图模版,这可能确实是一个对象,也可能是一个函数对象,你也可以认为是一个模块或一个

在一个逻辑单元以内,自包含的任务块应该有他们自己的方法,一个方法应该只做一件事,并把这件事做好。相似的类应该使用相同的方法名。

作为一个初级程序员,我以前经常都无法自然地写一个新的类来组织概念性的单元,也经常无法辨识什么才是自包含的。如果你看到了一个“Util“工具类,就像一个垃圾场,堆放了很多互不关联的代码,这就是新手写的代码的特点。如果你做了个微小的改动,发现这个改动有连锁反应,需要改动很多其他地方,那就是新手写的代码的另一个特点。

在往一个类添加一个方法或者向一个方法添加更多职责的时候,思考一下,并且问问你的本能。这里你需要花费点时间,不要跳过或者想着“我稍后再来重构”,刚开始的时候就要做。

基本的想法就是你想你的代码高内聚低耦合,这只是个时髦一点的术语,意思是说保持相关的代码在一起(在一个类中),降低不同类之间的相互依赖。

8)为未知做计划

经常会倾向于去考虑超出当前正在写的解决方案的问题。每写一行代码就有各种各样的“万一”在你脑海浮现。这在测试边界情况的时候是很好的习惯,但如果仅仅是作为驱动你去考虑可能的需求的时候,就大错特错了。

你需要辨识你的这些“万一”属于上面说的两种类型中的哪一类。不要编写你现在不需要的代码。不要为未知的将来作计划。

因为你觉得可能以后会使用到一个功能,就去编写代码实现它,这是很显然的错误。不要这样做。

尽可能编写目前正在实现的方案所需的最少量代码。当然,要处理边界情况,但不要添加边界功能

为了增长而增长是癌细胞的思想。

— Edward Abbey

9)没有使用合适的数据结构

初级程序员在准备面试的时候通常会太过关注算法。能够辨识好的算法并在需要的时候使用是很好的事。但记忆这些算法可不会给你的编程技能带来提升。

不同的是,记忆你所用的编程语言中的各种数据结构的优缺点肯定能使你成为更好的开发者。

使用错误的数据结构是一个巨大和明显的广告牌,上面写着“这是新手的代码“。

本文并不是要教你数据结构方面的知识,但这里快速举几个例子:

- 使用列表(数组)而不是映射表(对象)来管理记录

最经常犯的数据结构方面的错误就是使用列表而不是映射表来管理一系列对象。没错,你应该使用映射表来管理一个记录列表

要注意的是我这里讨论的记录列表是其中的每一项记录都有一个可以用于查找对象的唯一标识。使用列表来管理标量值是可以的,并且通常也是更好的选择,特别在重点用法是“压入”一些值到列表的情况下。

在 JavaScript 中,最常用的列表结构是数组,最常用的映射表结构是对象(在现代 JavaScript 中也有映射表的结构)。

使用列表而不使用映射表来管理对象通常都是错的。尽管这个说法确实是在管理大量记录的时候才成立,而我想说坚持一直这么做吧。这么做很重要的主要原因就是使用唯一标识来查找对象的时候,映射表比列表要快得多。

- 没有使用堆栈

当编写一些需要递归形式的代码的时候,通常很容易使用简单的递归函数。然而,优化递归代码通常很难,特别是在单线程环境下。

优化递归代码取决于递归函数返回了什么。比如说,优化一个返回调用自身两次以上的递归函数比优化只返回调用自身一次的函数要难得多。

作为初学者我们通常会忽视的就是其实有递归函数的替代方法。你可以使用_栈_数据结构。手动把函数调用结果 Push 入栈,然后在需要获取结果的时候把结果 Pop 出栈。

10)把既有代码弄得更糟

假设给你一个像这样的凌乱的屋子:

现在要求你在这个房间里面放置一件东西。由于房间现在已经是很混乱了,你很可能把东西随便一放,几秒钟就完事了。

当你面对的是混乱的代码的时候,千万别这么做。不要把代码弄得更乱!总是要让代码比你刚接手的时候干净那么一点。

以上房间问题中你应该做的是清理需要的部分,来给新的东西腾出合适的位置。比如说,如果这件东西是一件衣服,需要被放到衣柜里,那么你就需要清理出一条到衣柜的路出来。这就是完成这个任务的正确方式。

以下有一些错误的实践,通常会使得代码比以前更糟糕(不完备的列表):

  • 复制代码。如果你复制/粘贴一份代码之后只改了一行,你简直就是在产生重复代码并且把代码弄得更糟。放到以上凌乱房间的例子中就是,你拿进了一张更低基座的椅子而不是考虑使用一张可调节高度的椅子。总是要在心里想着抽象的概念,一旦可以就要使用它。
  • 没有使用配置文件。如果你要使用一个在其他环境下可能不一样的值,或者在其他时间可能不一样的值,这个值就应该放在配置文件中。如果你需要在你代码的不同地方都使用一个值,这个值也应该放置在配置文件中。当你要引入一个新的值到你的代码中的时候,只需要问问你自己:这个值应不应该放在配置文件中?答案很可能是“应该”。
  • 使用没必要的条件语句和临时变量。每一个 if 语句都是一个需要测试两次的逻辑分支。当你可以在不牺牲可读性的情况下避免条件语句的时候,你就应该这么做。这里主要的问题在于使用分支逻辑扩展一个函数还是引入另一个函数。每一次你觉得需要 if 语句或一个新的函数变量的时候,你应该问问自己:我是否在正确的层次修改代码,还是说我应该在更高的层次考虑一下这个问题。

关于不必要的 if 语句,看一下以下的代码:

1
2
3
4
5
6
7
function isOdd(number) {
if (number % 2 === 1) {
return true;
} else {
return false;
}
}

以上的 isOdd 函数有几个问题,但你能看出最明显的一个吗?

他使用了不必要的 if 语句,以下是一种等价的写法:

1
2
3
function isOdd(number) {
return (number % 2 === 1);
};

11)给显而易见的代码写注释

如今我已经学会了如何尽我所能去避免在写注释时面临的难题了。大多数的注释都可以使用更好命名的元素(译注:类、方法、变量)替换。

举个例子,不要写下面这样的代码:

1
2
3
4
5
6
7
8
9
10
// This function sums only odd numbers in an array
const sum = (val) => {
return val.reduce((a, b) => {
if (b % 2 === 1) { // If the current number is even
a+=b; // Add current number to accumulator
}

return a; // The accumulator
}, 0);
};

同样的代码可以不需要注释,像这样重写:

1
2
3
4
5
6
7
8
9
const sumOddValues = (array) => {
return array.reduce((accumulator, currentNumber) => {
if (isOdd(currentNumber)) {
return accumulator + currentNumber;
}

return accumulator;
}, 0);
};

仅仅是给函数起更好的的名字就可以使大多数注释变得没必要,在写注释前请记住这一点。

然而,有时候你可能被迫进入这样的情况,只有通过增加注释才能提高代码的清晰性。这时候你就应该组织你的注释来回答为什么是用这段代码而不是这段代码是干嘛的

如果你强烈地想要写一段注释来解释“这段代码是干什么的”,以增加代码清晰性,请不要写那些显而易见的。以下是一个例子,其中无用的注释只会徒增代码的干扰性。

1
2
3
4
5
6
7
8
9
10
11
// 创建一个变量并初始化为 0
let sum = 0;

// 遍历数组
array.forEach(
// 对于数组中的每一个数字
(number) => {
// 把当前数字加到变量 sum 中
sum += number;
}
);

别做上面那样的程序员。也不要接受这样的代码。不得不处理的情况下,删了那些注释。如果你碰巧雇用了写出上面那样注释的程序员,炒了他,马上炒。

12)没有写测试

我将简单地阐述这一点,如果你觉得你是个程序员专家并且这样的想法给你写代码不带测试的自信,在我的字典里你就是个新手。

如果你不在代码里写测试的话,那么你很可能在用某些手动的方式测试你的代码。如果你在构建一个网页应用的话,每次修改几行代码你就得在浏览器中刷新然后做一些交互来再次测试。我也是这么做的。手动测试代码没有错,但你应该在手动测试完之后,找到一种自动化测试的方法。如果你在你的应用中测试了一个交互功能,那么在你添加更多功能代码之前,你就应该先把这个交互功能的测试用代码自动化。

你是一个人,你就很难保证在每次修改完代码之后还能把之前所做的所有测试校验都再做一遍,那就让计算机帮你做吧。

如果可能的话,甚至在开始写代码实现需求之前,你就应该开始预估和设计需要测试校验的情况了。测试驱动开发 (Testing-driven development, TDD)可不是什么花俏的炒作,它是会实实在在会对你思考功能特性、寻找更好的设计方案产生积极影响的。

测试驱动开发(TDD)并非对每一个人和对每一个项目都奏效的,但如果你能够把它利用起来(哪怕只在项目的某一部分),你都完全应该这样去做。

13)觉得代码运行起来了就是能正确运行

看一下这个实现“把所有奇数相加”功能的函数 sumOddValues,有什么问题吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const sumOddValues = (array) => {
return array.reduce((accumulator, currentNumber) => {
if (currentNumber % 2 === 1) {
return accumulator + currentNumber;
}

return accumulator;
});
};


console.assert(
sumOddValues([1, 2, 3, 4, 5]) === 9
);

测试断言通过了,生活真美好啊,真的,真的对了吗?

问题在于以上的代码是不完备的,在少数情况下它能正确处理,碰巧测试使用的断言刚好就是这些情况中的一种,但除此之外还有很多问题,让我们列举其中的几个:

- 问题一: 没有处理空输入。如果这个函数被调用的时候没有传递任何参数呢?这种情况下就会产生一个错误,暴露了这个函数的内部实现。

1
TypeError: Cannot read property 'reduce' of undefined.

这通常是糟糕代码的一个标志,理由如下:

  • 函数的使用者不应该看到函数的具体实现。
  • 出错的信息对用户没有任何帮助,函数不起作用就是不起作用。但是,如果函数的出错信息能对函数的使用方法描述得更清晰具体一点,函数的使用者就可能知道了是他们使用姿势不当。比如你可以选择让这个函数抛出自定义的异常,像下面这样:
1
TypeError: Cannot execute function for empty list.

除了抛出一个异常,你也可以重新设计这个函数,忽略掉空的输入,然后返回 0。不管怎么样,在这种情况下你都应该做些处理。

- 问题二: 没有处理异常输入。如果调用函数的时候没有传递一个数组,而是传入了一个字符串,一个整数,或者一个对象,此时会发生什么?

现在这个函数就会抛出这样的错误了:

1
2
3
sumOddValues(42);

TypeError: array.reduce is not a function //(译注:array.reduce 不是一个函数)

好吧,真是不幸,因为 array.reduce 绝对是一个函数啊!

由于我们给函数的参数命名为 array,任何调用这个函数的参数(即以上例子中的 42)在函数内部都会打上 array 的标签,这个错误其实就是说 42.reduce 不是一个函数。

你亲眼看到了这样的错误多么令人费解,不是吗?也许更有帮助的错误是这样的:

1
TypeError: 42 is not an array, dude. // (译注:42 可不是数组啊,我的大胸弟。)

问题一和问题二有时候被称为边界情况,有一些基本的边界情况要考虑,但是也经常有一些不那么明显的边界情况,也需要考虑进来。比如说,如果我们传递了负数作为参数呢?

1
sumOddValues([1, 2, 3, 4, 5, -13]) // => 还是 9

呃,-13 也是一个奇数。这是你预期的行为吗?是不是应该抛出异常?负数是不是也应该被求和加起来?还是说像它现在的行为这样,仅仅忽略掉负数?现在你可能会意识到这个函数的名称本来应该被叫做 sumPositiveOddNumbers (对正奇数进行求和)。

这种情况下做决策很容易,但更重要的一点是,如果你不写测试用例记录你这次决策的原因,这个函数将来的维护者可能会不知道你是有意忽略掉负数还是这里有 bug,并对此毫无头绪。

这不是 bug ,这是特性。

— 忘了写测试代码的某某

- 问题三: 并非所有的有效情况都被测试了。抛开边界情况不说,这个函数还有一种合法的、非常简单的情况没有被正确处理:

1
sumOddValues([2, 1, 3, 4, 5]) // => 11

以上的 2 也被加到求和结果里面去了,但这本不应该。

答案很简单,reduce 接受另一个参数,作为求和器的初始值。如果这个参数没有传递的话(如上代码),reduce 函数就会用集合的第一个值作为求和器的初始值。这就是以上例子中第一个偶数也会被求和加起来的原因。

尽管你可能在你一开始写代码的时候就马上意识到这个问题了,但是暴露出这个问题的测试用例还是应该首先被包含到测试集中,跟其他很多基本测试用例一起,如“传递全是偶数的数组”,“传递包含 0 的数组“,还有”传递空数组“。

如果你看到了很少量的测试用例,还没有处理大多数甚至根本不处理边界情况,那就是新手代码的另一个标志。

14)没有质疑既有代码

除非你是个一直单飞的超级码农,否则毫无疑问,你在生活中肯定会遇到一些很傻逼的代码。初学者通常无法辨别这些代码的好坏,并且通常会觉得这些代码是好代码,因为这些代码似乎正常运行,还在代码仓库里面待了很长时间。

更糟的是,如果糟糕的代码用了一些糟糕的实践,初学者很可能就在代码仓库里的其他地方应用了这些糟糕的实践,因为他们把这些当作好代码给学习过来了。

有一些代码看起来很糟糕,但可能是有一些特殊的情况迫使开发者写出这样的代码,这就是应该编写详细注释的地方了,可以在这里告诉初学者这些特殊的情况以及代码这样写的原因。

作为一个初学者,总是应该假定那些你读不懂的、且没有文档注释的代码很可能就是糟糕的代码。质疑之,询问之,使用 git blame 揪出罪魁祸首!

如果代码作者已经离开很久了,或者他自己也记不起来了,那就好好研究这些代码,尽力弄懂相关的一切。只有当你完全理解了这份代码,你才能够建立起这份代码好坏的认知,在那之前不要做任何假设。

15)迷恋最佳实践

我觉得“最佳实践”其实是害人的,它暗示着你不需要深入研究它,这就是有史以来最佳实践,不用质疑!

没有最佳实践这回事,也许有目前来说针对这门语言的好的实践。

一些以前被认为是编程中最佳实践的,现在却被贴上了最差实践的标签。

如果你投入足够多实践,你总是可以找到更佳实践。别被最佳实践困扰了,把能做的做到最好。

不要因为你在某些地方读到过,说可以这么做你就去这么做,也不要因为你看过别人这么做你也去做,也不要因为别人说这是最佳实践你就这么做,包括本文中给出的所有建议!质疑一切,挑战权威,了解所有可能的选择,作出明智的决定。

16)迷恋性能

过早优化是万恶之源

— Donald Knuth (1974)

尽管自 Donald Knuth 写下上面的言论以来,计算机编程已经发生了翻天覆地的变化,我觉得这句话在今天仍然是很有价值的建议。

记住这一点的法则就是:如果你无法测量出被怀疑有性能问题的代码,那就不要试图去优化它。

如果你在运行代码之前就在优化它了,那很可能你就是在过早优化代码了,也很可能你正在费时费力做的优化是完全没必要的。

当然了,有一些很明显的优化是你必须在引入新代码前就要考虑的。比如说在 Node.js 中,不要让事件泛滥成灾或阻塞调用栈,这是至关重要的。这是你应该始终牢记的早期优化的一个例子。

在未经测量的既有代码中进行的任何不明显的优化都是有害的,也应该尽量避免。你可能觉得完成之后是一种性能收益,然而结果却可能是新的、意料之外的 bug 的源头。

不要浪费时间去优化未经测量的性能问题。

17)没有以最终用户体验为目标

给应用新增一个功能最简单的方法是什么?从你自己的视角来看这个功能,或者看新功能如何融入到目前的用户界面中,对吧?如果新功能是要从用户那里获取一些输入,那就在你已有的表单上添加,如果新功能是要在页面上添加一个链接,那就在你已有的菜单列表里面添加。

别做那样的开发者。 要做专业的开发者,站在最终用户的角度看问题。专业的开发者要考虑这个特定功能的用户需要什么、怎样使用,要想方设法使得这个功能容易让用户发现和使用,而不是想方设法在应用中用最便捷添加这个功能,毫不考虑这个功能的可发现性和可用性。

18)没有为任务挑选合适的工具

每一人都有一个最喜爱工具的列表,在他们的编程相关的活动中起到辅助的作用。一些工具很优秀,也有一些工具很辣鸡,但是大部分工具都很擅长处理某一特定的任务,对除此之外的任务就不那么在行了。

锤子是把钉子钉进墙壁里的绝妙工具,但是用来拧螺丝就是最差的工具了。不要因为你很喜爱那个锤子你就用它来拧螺丝。不要因为这是个很流行的锤子、在亚马逊上用户评分有 5.0 分就用它来拧螺丝。

依赖一个工具的流行度而不是它对一个问题的适用性来选择工具是真正新手的标志。

关于这点有一个问题是,你可能不知道适用某个特定任务的“更好的”的工具。在你目前的认知里,这个工具可能就是你所知道的最好的。但是,如果跟其他可选工具做比较的话,它就不是首选了。你需要使自己了解这些可选的工具,对这些新工具保持开放的心态,以后你可能会使用到它们。

一些程序员不愿意使用新的工具。他们对于目前正在使用的工具很满意,也不想学习新的工具。我理解这种做法,但这种做法很明显是错的。

你可以使用最原始的工具建造房子,然后享受甜蜜时光。你也可以花费一些时间和金钱去了解先进的工具、更快地建造更好的房子。工具在不断地改进中,你要乐意去学习它们、使用它们。

19)不理解代码问题会导致数据问题

程序的一个重要方面通常是管理某些形式的数据。程序就是一个添加新数据、删除旧数据、修改已有数据的接口。

即使是程序中最小的 bug 也会导致它所管理的数据去到一种不可预测的状态。尤其是当所有数据校验都完全在这个有 bug 的程序中进行时。

当涉及到“代码-数据”关系时,初学者可能无法马上理解其中的联系。他们可能觉得在生产环境中继续使用这段有 bug 的代码没什么大不了的,因为失效的功能 X 也不是超级重要。问题在于这段有 bug 的代码会持续产生数据完整性问题,这在一开始可不那么明显。

更糟的是,如果部署了 bugfix 的代码而没有修复相应 bug 导致的细微数据问题,也会让更多的数据问题累积,最终“积重难返”。

你怎么做才能使自己免受这些问题的困扰?你可以简单地使用多层数据完整性验证。不要依赖于单一的用户接口。在前端、后台、网络层、数据层都进行数据验证。如果做不到这样,那至少要在数据库层做约束。

要熟悉数据库的约束,在你往数据库里添加表、列的时候尽可能的用上这些约束:

  • NOT NULL 非空约束表示空值 NULL 无法被保存到该列上。如果你的应用假定了这个列的值是存在的,那就应该在数据库里把这个列定义为非空。
  • UNIQUE 唯一约束表示在整个数据表中,该列上的所有值都不能出现重复。举个例子,这在用户表中的用户名字段和邮件字段是极好的使用场景。
  • CHECK 约束是一个自定义的表达式,想要被数据库接受的数据都必须使该表达式计算结果为 true。举个例子,如果有一个常规的百分比列,它的值介于 0 到 100 之间,就可以使用 CHECK 约束来确保这一点。
  • PRIMARY KEY 主键约束表示这个列的值必须同时是非空和唯一的。你目前可能就已经在用了。数据库的每一个表都应该有一个主键以唯一标识一条记录。
  • FOREIGN KEY 外键约束表示该列的值必须和另一个表的一个列(通常是主键)的值匹配。

新手关于数据完整性的另一个问题是缺乏事务的观念。如果改变同个数据源的多个操作彼此依赖,他们就需要在一个事务里运行,以便在其中一个操作失败时能够回滚。

20)重复造轮子

This is a tricky point. In programming, some wheels are simply worth reinventing. Programming is not a well-defined domain. So many things change so fast and new requirements are introduced faster than any team can handle.
这是很棘手的一点。在编程领域,有些轮子确实值得重新造一遍。编程不是一个明确定义的领域。如此多的事物、变化如此之快、新需求的引进也如此之快,没有一个团队能够完美处理这些。

比如说,如果你需要一个轮子,根据一天的不同时间以不同的速度旋转,也许我们就不是去考虑改造那些我们所知的、所爱的轮子了,而要考虑重新造一个。但是,除非你确实需要一个非常规设计的轮子,否则都不要重新造一个新的轮子,就用那个该死的轮子吧。

有时候在众多可选的牌子里选择一个所需的轮子是很具挑战性的。在购买之前要先调研和使用!关于软件“轮子”很酷的一点是,大部分的轮子都是免费的、开放的,你可以看到它们的内部设计。你能够轻而易举地通过判断它们的内部设计的质量来判断软件轮子的好坏。如果可能,使用开源的轮子。开源的软件包可以很容易的调试和修复。也可以很容易的替换掉。此外,你还可以足不出户地支持它们。

不过,如果你需要一个轮子,千万不要去买一整辆车,然后把你正在维护的那辆车放到新买的车顶部。不要仅仅为了使用一两个函数就引入一整个代码库,在 JavaScript 中的典型例子就是 lodash 代码库。如果你要随即打乱一个数组,只要引入 shuffle 方法就好了。不要引入整个的 loadash 代码库,很可怕。

21)对代码复审的错误态度

程序员新手的一个标志就是他们经常把代码复审看作是批评。他们不喜欢代码复审、不感激代码复审、甚至恐惧代码复审。

这是错误的。如果你也这么觉得,你需要马上就改变这种态度。把每一次代码复审当作是学习的机会,欢迎他们、感激他们、从中学习,最重要的,当你从你的代码复审人员那里学习到东西的时候,要感谢他们。

在编程道路上,你永远都是个学习者。承认这一点。大多数的代码复审都能够让你学到以前不知道的一些东西。把它们当作学习的资源。

有时候代码审核人员也会犯错,那就轮到你来教他们了。但是如果无法仅仅从你的代码中就明显看出问题,也许在那种情况下你的代码就需要相应的修改了。如果你无论如何都需要给你的复审人员上一课的话,那就记得,对程序员来讲,教会别人是最有利的活动之一。

22)没有使用版本控制系统

新手往往低估了一个好的版本控制系统的威力,我这里所说的好的版本控制系统其实就是指 Git

源码控制并不仅仅是你把你的更改推送给别人,让他们在此之上接着开发,除此之外还有更重要的。源码控制是和清晰的历史相关的。源码会被质疑,代码的历史进程能够辅助回答这些困难的质疑。这就是为什么我们如此关注提交信息的原因。它们还是又一种交流实现的通道,使用提交信息能够帮助将来的维护者弄清代码为什么会发展到目前的状态。

应该经常提交和尽早提交。为了一致性,在提交信息主题行(译注:Git 提交信息的第一行)使用(译注:英文)现在分词。提交信息要尽可能详细但时刻注意也要尽可能是总结性的。如果你需要多几行来写提交信息,那很可能就是你的提交包含太多了,Rebase!

不要在提交信息中包含任何无关的东西。比如说,不要列出你添加、修改、删除了哪些文件,这些在提交对象的本身就包含了,并且可以轻易地使用一些 Git 命令参数就显示出来,在提交信息中包含这些显然就是混淆视听。有些团队喜欢为每一个更改的文件写一个总结信息,在我看来那是“提交信息太多”的标志。

源码控制也是和可发现性相关的。如果你看到一个函数,并且开始质疑它的必要性和设计,你可以找到引入这个函数的提交记录,看到这个函数的上下文。提交记录甚至可以帮助你认清哪些代码带来了 bug。Git 甚至提供了在各个提交之间进行二分查找,帮助定位到引入 bug 的提交的工具(bisect 命令)。

源码控制也可以被利用在一些神奇的地方,甚至在更改还没进入官方提交记录的时候。诸如暂存区变更、选择性打补丁、重置、暂存、修改、应用、比较、撤回等等工具为你的工作流提供了丰富的工具。了解它们、学习它们、使用它们、感激它们。

在我的字典里,Git 功能你懂得越少,你就越像一个新手。

23)过度使用共享状态

再次声明,这不是关于函数式编程和其他范式的讨论,那是另一篇文章的主题了。

事实就是,共享状态是出现问题的来源之一,如果可能的话,应该避免使用共享状态,如果避免不了,就应该最低限度地使用共享状态。

在我还是初级程序员的时候,我经常没有意识到,我定义的每一个变量都代表了一种共享状态,它所持有的数据能够被同一作用域内的所有元素修改。一个共享状态的作用域越大,它的跨度就越长。尽量让新的状态包含在小的作用域内,并且保证它们不会逸出。

关于共享状态的大问题是,在同一个 event loop(对于基于 event-loop 的环境) 中,当有多个资源需要同时修改这个状态的时候,就会出错。就会发生竞态条件。

重点来了:一个新手可能会尝试使用定时器来解决这个共享变量的竞态条件问题,特别是当他们必须处理一个数据锁的问题时。这是危险的标志,别这么做,注意它,在代码复审中指出它,永远也不要接受这样的代码。

24)面对 Erros 时的错误态度

Error 是好东西。这意味着你在进步,意味着你可以通过简单的后续修改就获得更多的进步。

专业程序员喜爱 Error。新手则痛恨 Error。

如果看到这些惊艳的红色错误信息让你很困扰,你就需要改变这种态度了。你需要把它们看作助手。你需要处理它们,需要利用它们来进步。

有一些 Error 需要被改进为异常(Exception)。异常是用户定义的错误,这些错误是你在计划之内的。有些 Error 则不需要管,它们就应该使程序奔溃并退出。

25)没有休息

你是一个人,你的头脑就需要休息。你的身体也需要休息。你经常很在状态以致于忘了休息。我把这视作新手的另一个标志。这不是你可以妥协的事情。在你的工作流中集成一些东西来迫使你中途休息。中途短暂休息很多次,离开你的椅子,走一小段路,以此来想清楚接下来应该做什么,稍后双眼清晰地回来继续写代码。

这真是篇长文章。你应该休息一下了。

感谢阅读