本书赞誉

正是这本书,开阔了我的视野,让我意识到自己不仅仅是庞大机器上的一枚齿轮,有朝一日也能藉由修炼成为匠师。它是我生命中最重要的一本书。

哪有什么简单的答案。没有最好的解决方案,无论是工具、语言还是操作系统;只在特定的环境下才有所谓更合适的系统。

作为务实的程序员,你们会共有许多如下特征:

  • 早期的采纳者/快速的适配者。
  • 你热衷于收集各种细微的事实,坚信它们会影响自己多年后的决策。

提示1 关注你的技艺

提示2 思考!思考你的工作

你能更积极地投入喜欢的工作,对越来越多的学科有掌控感,对不断进步产生愉悦感。从长期来看,时间投资将得到回报,因为你和你的团队将变得更高效,能编写出更容易维护的代码,并且在会议上花的时间更少。

伟大的草坪需要每天的点滴护理,伟大的程序员也是如此。

我活着不是为了满足你的期望,正如你也不是因为我的期望而活着。
——李小龙

提示3 你有权选择

如果你的技术过时了,安排时间(你自己的时间)学习一些看起来有趣的新东西。这是一种自我投资,只有为此而加班才是合理的。

在你的职业发展、学习教育,以及你的项目、每天的工作等各方面对你自己负责,对你的行为负责,这是务实哲学的基石之一。

提示4 提供选择,别找借口

给出选择,而不是找借口。不要说搞不定;解释一下要做些什么才能挽回这个局面。

当你意识到自己在说“我不知道”时,一定要接着说“——但是我会去搞清楚”。用这样的方式来表达你不知道是非常好的,因为接着你就可以像一个专家一样承担起责任。

提示5 不要放任破窗

不要搁置“破窗”(糟糕的设计、错误的决定、低劣的代码)不去修理。每发现一个就赶紧修一个。如果没有足够的时间完全修好,那么就把它钉起来。也许你可以注释掉那些糟糕的代码,显示一行“尚未实现”的信息,或用假数据先替代一下。采取行动,预防进一步的损害发生,表明一切尽在你的掌握中。

提示6 做推动变革的催化剂

大多数软件灾难都始于微不足道的小事,项目的拖延也是一天天累积而成的。系统一个特性接一个特性地偏离规范,一个接一个的补丁加到代码上,最终原始代码无影无踪。往往就是一件件小事的累积破坏了团队和士气。

提示7 牢记全景

永远留意着大局,持续不断地审视你身边发生的事情,而不要只专注于你个人在做的事情。

提示8 将质量要求视为需求问题

投资知识,收益最佳。
——本杰明·富兰克林

管理知识组合和管理金融投资组合非常的类似:

  1. 正规投资者有定期投资的习惯。

  2. 多样化是长线成功的关键。

  3. 聪明的投资者会平衡保守型和高风险高回报型投资的组合。

  4. 投资者用低买高卖来获得最大的回报。

  5. 应定期审查和重新平衡投资组合。

提示9 对知识组合做定期投资

  • 每年学习一门新语言

  • 每月读一本技术书

  • 还要读非技术书

  • 上课

  • 加入本地的用户组和交流群

  • 尝试不同的环境

  • 与时俱进

  • 学习的机会

  • 批判性思维

提示10 批判性地分析你读到和听到的东西

提示11 英语就是另一门编程语言

  • 了解听众。

  • 明白自己想说什么。

  • 选择时机。

  • 挑选风格。

  • 让它看起来不错。

  • 让听众参与。

  • 做倾听者。

  • 回应别人。

提示12 说什么和怎么说同样重要

越是有效的交流,影响力就越大。

提示13 把文档嵌进去,而不要栓在表面

注释源码是一个绝佳的机会,可以用来记录那些在其他地方无法记录的项目细节:工程上的权衡,为什么要做决定,放弃了哪些替代方案,等等。

接下来的两部分:DRY—邪恶的重复和正交性,有紧密关联。前一个提醒你不要在系统中复制知识,后一个阐述了不要把同一块知识切分到多个系统组件中。

提示14 优秀的设计比糟糕的设计更容易变更

为什么解耦很好?因为通过隔离关注焦点,可让每一部分都容易变更—此谓ETC。

ETC是一种价值观念,不是一条规则。

ETC里有一个隐含的前提。

第一件事,假设不确定什么形式的改变会发生,你也总是可以回到终极的“容易变更”的道路上:试着让你写的东西可替换。

第二件事,把它当作培养直觉的一种方式。在工程日志中记下你面临的处境:你有哪些选择,以及关于改变的一些猜测。

提示15 DRY—不要重复自己

唯一的方法是遵循下面这条被称为DRY的原则:在一个系统中,每一处知识都必须单一、明确、权威地表达。

DRY 针对的是你对知识和意图的复制。它强调的是,在两个地方表达的东西其实是相同的,只是表达方式有可能完全不同。

提示16 让复用变得更容易

你要努力的方向,应该是孕育出一个更容易找到和复用已有事物的环境,而不是自己重新编写。

提示17 消除不相关事物之间的影响

但凡编写正交的系统,就能获得两个主要的收益:提高生产力及降低风险。

有几种技术可以用来保持正交性:

  • 保持代码解耦

  • 避免全局数据

  • 避免相似的函数

  • 养成不断质疑代码的习惯

  • 测试

  • 由于系统组件之间的交互是形式化的,且交互有限,因此可以在单个模块级别上执行更多的系统测试。

  • 编写单元测试本身就是一个有趣的正交性测试

  • 修Bug也是评估整个系统的正交性的好时机

提示18 不设最终决定

提示19 放弃追逐时尚

要让你的代码具备“摇滚”精神:顺境时摇摆滚动,逆境时直面困难。

12 曳光弹

曳光弹之所以有用,是因为其工作环境和约束与真实子弹的相同。曳光弹能快速抵达目标,所以枪手可以得到即时的反馈。

提示20 使用曳光弹找到目标

曳光弹式开发和项目不会结束这种理念是一致的:总有东西需要改,总有新功能需要加。这是一个逐步递增的方法。

提示21 用原型学习

由于原型需要跳过细节,专注于它所考虑的系统的特定方面,所以你可能想用高阶脚本语言来实现原型。

脚本语言也可以很好地充当“粘合剂”,将低阶代码块组合成新的搭配。

提示22 靠近问题域编程

领域语言语言之界限,即是一个人世界之界限。

提示23 通过估算来避免意外

吃掉大象:

  • 检查需求分析风险

  • 设计、实现、集成

  • 和用户一起验证

一次次地迭代下去,提炼出的东西会变得更好,对进度的信心也会随之增长。这种评估工作通常在每个迭代周期的末尾团队进行回顾时完成。

提示24 根据代码不断迭代进度表

把提炼进度表作为每次迭代的一部分,你就可以估算出能力范围内最精确的进度安排。

提示25 将知识用纯文本保存

我们把需求以知识的形式收集起来,然后在设计、实现、测试和文档中表达这些知识。

纯文本赋予了我们操作知识的能力

  • 为防备老化而加保险

  • 利用杠杆效应让已有工具发挥最大优势

  • 易于测试

提示26 发挥Shell命令的威力

在Shell中,你可以调用所有能用的工具,或通过管道用各种方式把工具组合起来—恐怕开发者自己做梦都不会想到,自己当初开发的工具会被这么使用。

提示27 游刃有余地使用编辑器

怎么才算游刃有余。这里有一个挑战列表:

  • 当编辑文本时,以字符、单词、行、段落为单位移动光标及进行选择。

  • 当编辑代码时,在各种语法单元(配对的分隔符、函数、模块……)之间移动。

  • 做完修改后,重新缩进代码。

  • 用单个指令完成代码块的注释或取消注释。

  • Undo并Redo变更。

  • 把编辑窗口切割成多个面板,然后在它们之间跳转。

  • 跳转到特定的行号。

  • 对选出的多行进行排序。

  • 搜索普通字符串,或用正则表达式搜索,然后重复上一次的搜索。

  • 基于框选或某个模式匹配的结果,临时创建多个光标,并行地在多个光标处编辑文本。

  • 显示当前项目的编译错误。

  • 跑一下当前项目的测试。能不能不用鼠标/触控板完成上面所有的任务?

首先,编辑时要自省。每次发现自己又在重复做某件事情的时候,要习惯性地想到“或许有更好的方法”,然后找到这个方法。

藏起鼠标/触摸板,一整个星期只用键盘。如果发现有大量的事情,离开点击你就干不了了,那么正好学习一下该怎么干。

提示28 永远使用版本控制

把版本控制视为项目中枢。

提示29 去解决问题,而不是责备

Bug是你的错还是别人的错并不重要。无论是谁的错,问题仍然要你来面对。

调试心态。

最容易欺骗的人就是自己。

提示30 不要恐慌

人们很容易陷入恐慌,尤其是当最后期限逼近,或是在老板或客户站在背后紧张凝视之下,拼命找出问题原因的时候。

如果你在看到Bug或Bug报告时的第一反应是“这不可能”,那你就大错特错了。不要在“但那不可能发生”的思路上浪费哪怕一个神经元,因为很明显它会发生,而且已经发生了。

永远要去发掘问题的根本原因,而不仅仅停留在问题的表面现象。

从哪里开始。

请确保正在处理的代码可以干净构建——没有警告。

当错误报告来自第三方时,Bug报告的准确性会进一步降低——实际上,你可能需要观察提交错误报告的用户,通过其操作来获得足够详细的信息。

调试策略。

复制Bug。

提示31 修代码前先让代码在测试中失败

提示32 读一下那些该死的出错信息

通常有一种比逐个检查每个栈帧更快的方法来发现问题:使用二分法。但是在讨论二分法之前,让我们先看看另外两个常见的Bug场景。

输入值的敏感度。

获取数据集的副本,并用数据测试本地运行的应用程序副本,确保其在本地也能崩溃。

版本间回退。

二分法。

不断分割数据,直到得到能显露问题的最小集。

输出日志及(或)跟踪信息。

通过使用文本处理工具或Shell命令来处理日志文件,就可以很容易地识别出,有问题的打开操作发生在哪里。

排除法。

提示33 “select”没出问题

有时改变的东西超出了你的控制范围:更新操作系统、编译器、数据库或其他第三方软件的版本,可能会破坏以前正确的代码,因而出现新的Bug。

不要因为“觉得”一个程序或一段代码没问题,就在它牵涉一个Bug时,对它视而不见。你需要证明它没问题,用出Bug时的上下文、同样的数据、当时的边界条件来证明它没问题。

提示34 不要假设,要证明

如果Bug是损坏的数据造成的结果,而数据引爆程序前经过几层逐级传播,可以试试给函数加上更完备的参数检查,以便更早地将问题剥离出来。

如果修复这个Bug花了很长时间,问问自己为什么。你能做些什么来让下次修复这个Bug更容易呢?也许可以构建更好的测试钩子,或是编写一个日志文件分析器。

如果Bug是因为某人的错误假设造成的,那么就与整个团队讨论这个问题:如果一个人误解了,那么很可能很多人都误解了。

调试工作的清单。

提示35 学习一门文本处理语言

提示36 你无法写出完美的软件

我们需要防御性驾驶。在麻烦发生之前就做好准备,预料到意料之外的事情,永远不要把自己置于无法自拔的境地。

我们被教导要防御式编程——有任何疑问,都要去验证我们得到的一切信息;使用断言来检测错误数据,不信任任何可能来自潜在攻击者或有恶意者的数据;做一致性检查,对数据库的列设置约束——做完这些,我们通常就会自我感觉良好。

务实的程序员则更进一步,他们连自己也不相信。既然没人能写出完美的代码,那么也包括自己在内。

务实的程序员会为自己的错误建立防御机制。

最重要的一点是,只要我们总是坚持走小步,按照不要冲出前灯范围中描述的那样,就不会从悬崖边掉下去。

“当所有人都真的在给你找麻烦的时候,偏执就是一个好主意。”

提示37 通过契约进行设计

请记住,如果你订的契约是可以接受任何东西,并且承诺要回报整个世界,那么你就有很多代码要写!

类的不变式与函数式语言。

DBC与测试驱动开发。

实现DBC。

在编写代码之前,简单地列出输入域的范围、边界条件是什么、例程承诺要交付什么——或者更重要的是,没有承诺要交付什么——这些对编写更好的软件来说,是一个巨大的飞跃。

断言。

因为契约最大的用途,就体现在你的代码和它所使用的库之间的边界处,这里通常会检测到最多的问题。

DBC与尽早崩溃。

谁的责任。

语义不变式。

出错时要偏向消费者。

提示38 尽早崩溃

崩溃,不要制造垃圾。

一旦代码发现本来不可能发生的事情已发生,程序就不再可靠。

一个死掉的程序,通常比一个瘫痪的程序,造成的损害要小得多。

提示39 使用断言去预防不可能的事情

论何时,你发现自己在想“当然这是不可能发生的”时,添加代码来检查这一点。最简单的方法是使用断言。

提示40 有始有终

无论在什么时候,我们写代码都要管理资源:内存、事务、线程、网络连接、文件、计时器——所有可用的数量有限的东西。大多数情况下,资源使用遵循一个可预测的模式:分配资源,使用它,然后释放它。

当有疑问时,缩小范围总是有好处的。

提示41 在局部行动

嵌套的分配。

释放资源的顺序与分配资源的顺序相反。

在代码的不同位置,如果都会分配同一组资源,就始终以相同的顺序分配它们。

长期的平衡。

对象与异常。

保持平衡与异常。

当你无法保持资源的平衡时。

检查平衡。

因为务实的程序员不相信任何人,包括自己。

提示42 小步前进——由始至终

总是采取经过深思熟虑的小步骤,同时检查反馈,并在推进前不断调整。把反馈的频率当作速度限制,永远不要进行“太大”的步骤或任务。

越是必须预测未来会怎样,就越有可能犯错。与其浪费精力为不确定的未来做设计,还不如将代码设计成可替换的。

提示43 避免占卜

很多时候,明天看起来会和今天差不多,但不要指望一定会这样。

当我们试着单独挑出一个事物的时候,总会发现它与宇宙中其他一切都有关联。

提示44 解耦代码让改变更容易

提示45 只管命令不要询问

这个原则说的是,不应该根据对象的内部状态做出决策,然后更新该对象。这样做完全破坏了封装的优势,并且在这样做时,也会把实现相关的知识扩散到整个代码中。

提示46 不要链式调用方法

提示47 避免全局数据

全局数据包括单件。

全局数据包括外部资源。

提示48 如果全局唯一非常重要,那么将它包装到API中

继承增加了耦合。

一切都是为了变更。

让代码害羞一点:让它只处理直接知道的事情,这将有助于保持应用程序解耦,使其更易于变更。

如果你不能将正在做的事情描述为一个流程,那表示你不知道自己正在做什么。

提示49 编程讲的是代码,而程序谈的是数据

提示50 不要囤积状态,传递下去

提示51 不要付继承税

提示52 尽量用接口来表达多态

提示53 用委托提供服务:“有一个”胜过“是一个”

提示54 利用mixin共享功能

物归其所,事定期限。

提示55 使用外部配置参数化应用程序

提示56 通过分析工作流来提高并发性

共享状态是不正确的状态

提示57 共享状态是不正确的状态

提示58 随机故障通常是并发问题

角色是一个独立的虚拟处理单元,具有自己的本地(且私有的)状态。

进程通常代表一种更通用的虚拟处理机,它一般由操作系统实现,可以让并发处理更容易。进程也能(根据约定)被约束为以角色的形式运转,我们在这里说的就是这类进程。

提示59 用角色实现并发性时不必共享状态

提示60 使用黑板来协调工作流

提示61 倾听你内心的蜥蜴

首先,停止正在做的事情。给自己一点时间和空间,让大脑自我组织。

让想法自己从大脑的各个层面渗透出来:对此不用很刻意。最终这些想法可能会上升到有意识的水平,这样你就能抓住一个“啊哈!”的时刻。

学会在编码时听从直觉是一项需要培养的重要技能。

不要假设,要证明。

找到恰好能用的答案和找到正确的答案不是一回事。

提示62 不要依赖巧合编程

如果希望花费更少的时间来编写代码,就要在开发周期中尽可能早地捕获并修复错误,这样可以一开始就少犯错。

不要成为历史的奴隶。不要让现有的代码去支配未来的代码。如果不再合适,所有代码都可以替换。即使一个程序正在进展中,也不要让已经做完的事情限制下一步要做的事情——准备好重构。

所以下次碰到有什么事情看起来可行,但你不知道为什么,确保它不是一个巧合。

提示63 评估算法的级别

提示64 对估算做测试

最好的不会永远最好。

在投入宝贵的时间尝试改进算法之前,确保算法确实是瓶颈,总是最为可取。

提示65 尽早重构,经常重构

重构的核心是重新设计。

  1. 不要试图让重构和添加功能同时进行。

  2. 在开始重构之前,确保有良好的测试。

  3. 采取简短而慎重的步骤

提示66 测试与找Bug无关

测试获得的主要好处发生在你考虑测试及编写测试的时候,而不是在运行测试的时候。

提示67 测试是代码的第一个用户

测试驱动开发。

TDD的基本循环是:

  1. 决定要添加一小部分功能。

  2. 编写一个测试。

  3. 运行所有测试。

  4. 尽量少写代码,只需保证测试通过即可。

  5. 重构代码:看看是否有办法改进刚刚编写的代码(测试或函数)。

务必实践一下TDD。但真这样做时,不要忘记时不时停下来看看大局。

提示68 既非自上而下,也不自下而上,基于端对端构建

我们坚信,构建软件的唯一方法是增量式的。构建端到端功能的小块,一边工作一边了解问题。应用学到的知识持续充实代码,让客户参与每一个步骤并让他们指导这个过程。

测试对开发的驱动绝对能有帮助。但是,就像每次驱动汽车一样,除非心里有一个目的地,否则就可能会兜圈子。

提示69 为测试做设计

如果代码出过一次问题,就有再出问题的可能性。不要将创建出来的测试扔掉,把它添加到现有的单元测试库中。

我的个性坚持认为,当开始觉得舒适的时候,就应该去尝试别的东西。

我相信这个原因就是,(对我来说)测试的好处更主要来自于思考测试,以及思考测试会对代码造成怎样的影响。在长时间坚持这样做之后,我写不写测试都会这样思考。代码仍然是可测试的,只是无须真的写出测试而已。

提示70 要对软件做测试,否则只能留给用户去做

提示71 使用基于特性的测试来校验假设

当基于特性的测试失败时,找出传递给测试函数的参数,然后使用这些值创建一个单独的、常规的单元测试。

基于特性的测试让你从不变式和契约的角度来考虑代码;你会思考什么不能改变,什么必须是真实的。这种额外的洞察力会对代码产生神奇的影响,可以消除边界情况,并突显使数据处于不一致状态的函数。

安全性的基本原则

  1. 将攻击面的面积最小化

  2. 最小特权原则

  3. 安全的默认值

  4. 敏感数据要加密

  5. 维护安全更新

提示72 保持代码简洁,让攻击面最小

提示73 尽早打上安全补丁

不要禁用浏览器中的粘贴功能。破坏浏览器和密码管理器的功能,并不能使系统更安全。实际上,它会促使用户创建更简单、更短、更容易破解的密码。

当涉及加密时,第一条也是最重要的一条规则是,永远不要自己做。

名不正,则言不顺;言不顺,则事不成。

提示74 好好取名;需要时更名

所谓完美境界,亦非加无可加,而是减无可减……

提示75 无人确切知道自己想要什么

提示76 程序员帮助人们理解他们想要什么

提示77 需求是从反馈循环中学到的

每次迭代都以客户的直接反馈结束。这样可使我们走上正轨,并确保如果走错了方向,损失的时间是最少的。

提示78 和用户一起工作以便从用户角度思考

提示79 策略即元数

针对更普遍的情况做实现,至于系统需要支持的那种特定类型的东西,只是通用实现在加入策略信息后的示例。

提示80 使用项目术语表

提示81 不要跳出框框思考——找到框框

简单地说,注意力分散的人在解决复杂问题时比有意识的人做得更好。

打造代码,而非打造自我。这与谁最聪明无关;我们都有许多闪光的瞬间,也有糟糕的时刻。

  • 批评要针对代码,而不针对人。“让我们看看这一块”听起来比“你搞错了”好得多。

  • 倾听他人的观点并试着理解。观点不同不是错误。

  • 频繁进行回顾,为下一次做好准备。

提示82 不要一个人埋头钻进代码中

提示83 敏捷不是一个名词;敏捷有关你如何做事

所以下面是我们以敏捷方式工作的秘诀:

  1. 弄清楚你在哪里。

  2. 朝想去的方向迈出有意义的最小一步。

  3. 评估在哪里终结,把弄坏的东西修好。

由“魔法三连”:版本控制、测试和自动化组成的务实的入门套件。

提示84 维持小而稳定的团队

质量是一个团队问题。即使是最勤奋的开发者,只要身处一个什么都不在乎的团队中,也会发现自己很难保持修复琐碎问题所需的热情。如果团队不鼓励开发者在这些修复工作上花费时间,那么问题就会进一步恶化。

对项目范围扩大、时间缩短、额外特性、新的环境——任何在最初的理解中没有的东西,都要留心。

“只要有空闲时间”就去做,意味着这件事永远不会发生。

提示85 排上日程以待其成

良好的沟通是避免这些问题的关键。这里所说的“良好”,指的是即时、无摩擦力。

保持清醒,留意DRY 。

提示86 组织全功能的团队

记住团队是由个人组成的。赋予每个成员能力,让他们以自己的方式发光发热。要提供完善的架构来支持他们,并确保项目交付的价值。

提示87 做能起作用的事,别赶时髦

怎样才能知道“什么能起作用”?有一个最基本的实用技巧可以依靠,那就是:
试一试。

请注意,能够即期交付并不意味着你必须每天每分钟都交付。只有当这样做在业务上有意义时,才有必要在用户需要时即期交付。

提示88 在用户需要时交付

过度投资于任何一种特定的方法,会让你对其他方法视而不见。当你习惯于一种方法时,很快就看不到其他的出路了。你已经僵化,变得不再能快速适应。

提示89 使用版本控制来驱动构建、测试和发布

提示90 尽早测试,经常测试,自动测试

提示91 直到所有的测试都已运行,编码才算完成

构建可能包括几种主要的软件测试类型:单元测试、集成测试、确认和验证,以及性能测试。

提示92 使用破坏者检测你的测试

提示93 测试状态覆盖率,而非代码覆盖率

提示94 每个Bug只找一次

提示95 不要使用手动程序

一切都要依赖于自动化。

用户真正要的不是代码,他们只是遇到某个业务问题,需要在目标和预算范围内解决。他们的信念是,通过与你的团队合作,能够做到这一点。

“需求”实际上只是对用技术可以完成哪些工作的猜测——它实际上是一个业余的实现计划,只是伪装成需求文档。如果你能证明有方法会使项目更接近目标,那么就不要害怕,大胆提出改变需求的建议。

提示96 取悦用户,而不要只是交付代码

或许你的头衔只是“软件开发者”或“软件工程师”的某种变体,而事实上这个头衔应该是“解决问题的人”。这就是我们所做的,也是一个务实的程序员的本质。

提示97 在作品上签名

过去的工匠很自豪地为他们的作品签名。你也应该这样。

保持匿名会滋生粗心、错误、懒惰和糟糕的代码,特别是在大型项目中——很容易把自己看成只是大齿轮上的一个小齿,在无休止的工作汇报中制造蹩脚的借口,而不是写出好的代码。

我们这些开发者,拥有令人难以置信的特权——我们真正在建设未来,这是一股巨大的力量。伴随着这种力量而来的,是一种非同寻常的责任。

提示98 先勿伤害

有一些创造性的想法,开始打道德行为界限的擦边球,如果你参与了这类项目,就和出资者一样负有责任。

提示99 不要助纣为虐