41-10x程序员工作法

开篇词 | 程序员解决的问题，大多不是程序问题

你好！我是郑晔，一个程序员。

很多人都说，程序员很辛苦，与这个角色联系在一起的词儿，通常是忙碌、加班、熬夜等。

作为程序员，我们将其看作一个值得全情投入的职业，希望能够把精力放在设计算法、改进设计、优化系统这些具有创造性与成就感的本职工作上。

但现实情况却是，许多人因为一些“意外”，陷入了无休止的忙碌，比如：

你辛辛苦苦写的代码还没上线，产品经理就告诉你需求变了；

你拼命加班只因错估了工作量，自己造的“孽”，含着泪也要搞定；

你累死累活做出来的东西和要求不符，只能从头再来；

你大面积地修改代码只是因为设计糟糕，无法适应新的需求变化；

……

诸如此类，不胜枚举。我们很辛苦，但耗费我们大量时间和精力去应付的工作，并不是技术工作，反而是这些看似很“不值当”的事儿。

为什么会这样？

软件行业里有一本名著叫《人月神话》，其中提到两个非常重要的概念：本质复杂度（Essential Complexity）和偶然复杂度（Accident Complexity）。

简单来说，本质复杂度就是解决一个问题时，无论怎么做都必须要做的事，而偶然复杂度是因为选用的做事方法不当，而导致要多做的事。

比如你要做一个网站，网站的内容是你无论如何都要写的，这就是“本质复杂度”。而如果今天你还在用汇编写一个网站，效率是不可能高起来的，因为你选错了工具。这类选错方法或工具而引发的问题就是“偶然复杂度”。

作为一个在软件行业奋斗了近二十年的程序员，我深刻意识到一个遗憾的事实：大部分程序员忙碌解决的问题，都不是程序问题，而是由偶然复杂度导致的问题。

换句话说，只要选择了正确的做事方法，减少偶然复杂度带来的工作量，软件开发是可以有条不紊进行的。

如何减少偶然复杂度引发的问题，让软件开发工作有序、高效地进行，这正是我希望通过这个专栏帮你解决的问题。

许多人工作做事主要依靠直觉，在这个科学越发昌明的时代，我们清楚地看到，人类的直觉常常是错的，就像古人凭直觉认为大地是平的一样。

软件开发也不例外，如果你不曾在做软件这件事上有过学习和思考，形成一套高效的工作方法，只是凭直觉行事，在真实世界中往往会举步维艰。

幸运的是，总会有不同的人在不同的方向上探索不同的做法，一旦通过真实世界的验证，就会沉淀出可供行业直接应用的最佳实践（Best Practice）。

在软件行业中，这样能够提升工作效率的最佳实践已经有很多，但是，学习掌握这些最佳实践是有难度的，其根源就在于，很难找到这些实践彼此间的内在联系。

直觉大多是错误的，最佳实践又多而琐碎，所以在这个专栏中，我会尝试给你提供一个思考框架，帮你在遇到问题时梳理自己真正要做的事情。围绕着这个框架，我还会给你一些原则。

这些原则，是我从软件行业的诸多软件开发最佳实践中总结出来的，也是我如今在工作中所坚持的。这些原则就是一条主线，将各种最佳实践贯穿起来。

这些原则不多，总结起来就四个：

以终为始；

任务分解；

沟通反馈；

自动化。

也许看到这四个原则的名字，你会不以为然，这些说法你在很多地方都看到过，但我想与你分享的内容可能与你想的并不完全一致。

比如：你以为的“终”可能不是终，因为你只是站在自己的角度；你以为自己做了任务分解，在我看来，可能还不够，因为我希望你能够做到微操作；你以为的沟通反馈就是说话聊天，我想告诉你很多技术实践的存在也是为了沟通反馈；你以为自动化就是写代码，我会告诉你，有时候不写代码而解决问题，可能才是一个好方案。

在我看来，想要将精力聚焦在本质复杂度上，提高工作效率，摆脱直觉的束缚，只要掌握上面的四个原则就可以了。

或许你此时会问，这些原则很难吧？其实并不难，在探讨这个专栏的内容时，我的编辑作为软件开发的局外人，经常发出感叹：“这事真的就这么简单吗？这不就是正常做事应该有的逻辑吗？”

是的，就是这样简单，但大多数人没有这样做，因为这些原则在实际工作中很可能是反直觉的。只要打破思维误区，你的整个人都会变得不一样。

下面是整个专栏的目录，我希望能帮助你回答，或者厘清一些开发过程中，曾经遇到，又未曾深入的问题。

当我们详谈这些原则时，我会给你讲述一些最佳实践，让你看到这些原则是如何应用于不同的实践中的。希望我对这些实践的理解成为你的知识地图，让你拥有继续探索的方向。

我做这个专栏的原则是“授人以鱼，不如授人以渔”。我希望你很好地理解这些原则，掌握高效工作的方法。至于最佳实践，你可以自行决定，是直接采纳还是曲线救国更为合适。

介绍一下我自己，我是郑晔，目前在火币网担任首席架构师，写过代码、带过团队、做过咨询，创过业，还维护着一个拿过 Oracle Duke 选择奖的开源项目 Moco，至今仍然在编程一线写着代码。

很长时间里，我一直对如何做好软件充满了好奇，了解过各种技术以及开发方法。做咨询的经历让我有机会见识到不同公司面临的问题；带团队的时候，我也看到很多小兄弟因为不会工作，虽然很努力却收效甚微；而我自己菜鸟时期的笨拙依然是历历在目。

在我看来，所有做软件的人能力都很强，这些问题都只是因为不会工作造成的，但更可怕的是，许多人深陷泥潭而不自知。

在这些年的工作里，我一遍又一遍给别人讲如何工作，逐渐总结出一套自己的工作原则，如今呈现在你面前的就是我这些年思考的总结。

我不指望所有人都能从这个专栏受益，我只想把这个专栏写给那些愿意成长的人。我只是来做一次信息分享，分享一些思考，分享一些做法，希望可以将你从常见的思维误区中带出来。

也许在这个专栏的最后，你发现自己并不认同我的原则，却能够用自己的原则来与我探讨，那么，恭喜你，因为那是最美妙的事情！

01 | 10x程序员是如何思考的？

在开篇词中我们提到，程序员在工作中遇到的很多问题，大多不是程序问题，辛苦而低效的工作，多数是由偶然复杂度导致的。那这个由于偶然复杂度造成的差距会有多大呢？

1975 年，弗雷德里克·布鲁克斯（Frederick Brooks）出版了软件行业的名著《人月神话》，他给出了一个统计结果，优秀程序员的开发效率是普通程序员的 10 倍。40 多年过去了，这个数字得到了行业的普遍认同。

成为 10x 程序员是很多程序员的追求。但工作产出并不只是由写代码的效率决定的，一些不恰当工作方法很大程度上影响着你的产出。

在接下来的这段时间里，我希望通过这个专栏和你一起探讨，作为一个程序员，该如何更高效地工作，怎样才能把时间和精力尽可能地放在处理本质复杂度的事情上，减少在偶然复杂度上的消耗。

作为整个课程第一讲，我就从我常用的一个思考框架开始。

一个思考框架

我曾经组织过针对应届毕业生的培训，第一堂课我往往亲自操刀，其中有一个头脑风暴的环节“畅想未来”，我会让大家思考三个问题：

我现在是个什么水平？

我想达到一个什么水平？

我将怎样到达那个目标？

大家会围绕着这三个问题，从各种角度展开讨论。这是一个有趣的练习，你会发现大家“最擅长”的是回答第一个问题：我现在处于什么水平？和有经验的人相比，他们大多自认为比较“菜”。但对于后两个问题的讨论，却可以切实看出人和人之间处理问题的能力差异。

有人通过之前的资料搜集，已经对自己的未来有了一个打算。比如想成为一个研发大牛，或者想做一个开源软件等，也就是说，对于第二个问题，他有明确的答案。

而有的人则是一脸茫然，他很可能根本没有考虑过这个问题。而从题目本身来看，目标相对清晰的同学，才会进入到第三个问题，而茫然的同学，则完全无从下手。

那么我为什么会问这几个问题呢？我是想让大家跳出现有的思考模式，摆脱仅凭直觉“闷头做事”的习惯方式，把低着的头抬起来，看一眼未来，给自己找一个方向。

否则，如果你对未来没有定位，是茫然的，尽管你也知道要努力，但劲儿该往哪里使呢？如果使劲的方向不对，那么你越使劲儿，可能会在错误的路上跑得越远。南辕北辙的道理大家都懂，但具体到自己的工作和发展上，真正能体会并实践的却是少数。

其实，这三个问题来自一个思考框架。在给其他公司团队做咨询时，我也经常会运用到它，原来的问题是：

Where are we?（我们现在在哪？）

Where are we going?（我们要到哪儿去？）

How can we get there?（我们如何到达那里？）

这三个问题实际上是帮我们确定：

现状；

目标；

实现路径。

如果一个人能够清晰地回答出这三个问题，通常意味着他对要做的事有着清晰的认识。这个框架虽然看似简单，但却非常有效，它已经成为我工具箱里一件非常称手的思考工具。

在我的职业生涯里，与很多人讨论不同的事时，我都会用到这个思考框架的不同变体，而在这个专栏里，我也会用它来帮助回答“怎样高效工作、怎样做好软件”这件事。

四个思考原则

在实际的工作中，这个思考框架会帮助我更好地了解自己的工作。比如，当一个产品经理给我交代一个要开发的功能特性时，我通常会问他这样一些问题：

为什么要做这个特性，它会给用户带来怎样的价值？

什么样的用户会用到这个特性，他们在什么场景下使用，他们又会怎样使用它？

达成这个目的是否有其它手段？是不是一定要开发一个系统？

这个特性上线之后，怎么衡量它的有效性？

如果产品经理能够回答好这些问题，说明他基本上已经把这个工作想得比较清楚了，这个时候，我才会放心地去了解后续的细节。

我们用思考框架对照一下，为什么我会问这些问题。一般来说，一个新特性要开发时，现状我是知道的。所以，我更关心目标，这里“为什么要做这个特性？”就是在问目标，“给用户带来怎样的价值”是在确定这个目标的有效性。

接下来，我会关注实现路径，用户会怎么用，是否有其他的替代手段，我需要了解产品经理的设计是经过思考的，还是“拍着脑袋”给出的。衡量有效性，则是要保证我的工作不会被浪费。

通过这个例子，我给你展示了怎么用这个思考框架提出问题。但我估计你更想了解的是，我怎么会想到问这些问题。给出思考框架是为了让你明白为什么要提出问题，而具体问题要怎么问，就可以遵循下面这四项原则：

以终为始；

任务分解；

沟通反馈；

自动化。

这是我从思考框架延伸出来的。在这个专栏里，我会围绕这四项原则和你详细讨论。

解释一下，以终为始就是在工作的一开始就确定好自己的目标。我们需要看到的是真正的目标，而不是把别人交代给我们的工作当作目标。你可以看出这个原则是在帮助我们回答思考框架中，Where are we going?（我们要到哪儿去？）这个问题。

任务分解是将大目标拆分成一个一个可行的执行任务，工作分解得越细致，我们便越能更好地掌控工作，它是帮助我们回答思维框架中，How can we get there?（我们如何到达那里？）的问题。

如果说前两个原则是要在动手之前做的分析，那后面两个原则就是在通往目标的道路上，为我们保驾护航，因为在实际工作中，我们少不了与人和机器打交道。

沟通反馈是为了疏通与其他人交互的渠道。一方面，我们保证信息能够传达出去，减少因为理解偏差造成的工作疏漏；另一方面，也要保证我们能够准确接收外部信息，以免因为自我感觉良好，阻碍了进步。

自动化就是将繁琐的工作通过自动化的方式交给机器执行，这是我们程序员本职工作的一部分，我们擅长的是为其他人打造自动化的服务，但自己的工作却应用得不够，这也是我们工作中最值得优化的部分。

这四个原则互相配合，形成了一个对事情的衡量标准。总体上可以保证我的工作是有效的，在明确目标和完成目标的过程中，都可以尽量减少偶然复杂度。

怎么把这四个原则用在工作中呢？我们回过头来看一下前面的场景，产品经理把要做的功能特性摆在我面前。站在以终为始的角度，我需要了解真正的目标是什么，所以，我会关心为什么要做这个特性。为了保证目标是有效的，我会关心它给用户带来的价值。

有了任务分解的视角，我需要将一个大的目标进行拆解，如果我要达成这个目标，整体解决方案是远远不够的，我需要把任务分解成一个一个小的部分。所以，我会关心一个一个具体的使用场景。

一方面，我会了解到更多的细节，另一方面，当时间紧迫的时候，我会和产品经理来谈谈究竟优先实现哪个场景。

为什么要学会沟通反馈？因为我需要明确，自己是否真正理解了产品经理提交的需求。所以，我要不断地问问题，确保自己的理解和产品经理交代的内容一致。

另外，我也需要保证我的产品做出来确实能够达到目标。所以，我会关心它上线后的衡量手段。因为我知道，这个行业里有太多代码上线后，从来没有运行过。

自动化的角度很有意思，我们做的方案通常是一个自动化方案，但我们需要了解这个方案没有自动化之前是怎么做的。如果不自动化，用户会怎么用。所以，我会关心是不是还有其它替换方案，比如，买一个现成的服务。因为很多需求的提出，只是因为我们有了一个开发团队而已。

好，现在你已经对这四个原则在工作中的应用有了一个直观的认识。但你也会发现，我问的这些问题似乎已经“超纲”了，超过了一个普通程序员应该关注的范围。但这就是真实世界，它不像考试一样，有一个标准答案。

我们不是一个人孤独地在工作，而是与其他人在协作，想要做到高效工作，我们就要“抬起头”来，跳出写代码这件事本身。所以，我在开篇词里说，程序员解决的问题，大多不是程序问题。

可能你对这些原则的了解还没过瘾，没关系，这篇文章只是让大家清晰地了解思考框架和原则的背后逻辑。接下来，我会结合行业里的最佳实践，给你进一步讲解这些原则和具体应用。

总结时刻

大多数人工作低效是由于工作中偶然复杂度太多造成的，只要能够更多地将注意力放到本质复杂度上，减少偶然复杂度造成的消耗，我们“真实”的工作效率自然会得到大幅度提升。

而想要减少偶然复杂度的消耗，就要了解一些高效的工作方式和行业的最佳实践，而这一切是可以用统一的框架进行思考的。

运用这个思考框架，我们需要问自己一些问题：

Where are we?（我们现在在哪？）

Where are we going?（我们要到哪儿去？）

How can we get there?（我们如何到达那里？）

为了把这个框架应用在我们程序员的工作中，我给了你四个思考原则：

以终为始，确定好真实目标；

任务分解，找到实施路径；

沟通反馈，解决与人打交道出现的问题；

自动化，解决与机器打交道出现的问题。

如果今天的内容你只能记住一件事，那请记住：面对问题时，用思考框架问问自己，现状、目标和路径。

最后，我想请你思考一下，如果把这个思考框架运用在你的职业发展规划上，你会如何回答这三个问题呢？

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

02 | 以终为始：如何让你的努力不白费？

今天内容的开始，我希望你可以先来思考一个问题：如果让你设计一个登录功能，你会怎么做？

我曾在公司内部做过这样一个练习，我扮演客户，让大家帮我设计一个登录功能。同事们一听就高兴了，登录不就是用户名加密码嘛，我熟啊，我还可以设计出验证码、找回密码、第三方登录等等功能。

更有个别动作快的同事，甚至已经开始设计数据库表，考虑用 Redis 做缓存了。整个过程下来，大家彼此讨论得热火朝天，唯一没人理会的就是我这个“客户”。

讨论结束，扮演客户的我告诉大家，作为一个“土豪”，我打算做一个打车软件，用户可以通过手机号接收验证码的方式进行登录。你可以想见，同事们一副“被套路了”的表情。是的，他们设计那套用户名密码登录完全是文不对题。

虽然这是一个简单的练习，但反映的却是我们日常面对的真实工作场景：许多人都是刚刚听到别人要求做的一个功能，就开始脑补接下来的一切。导致的结果，就是付出的努力毫无意义。

那么问题出在哪呢？因为我们欠缺了“以终为始”的思维习惯。

一种反直觉的思维方式

以终为始，就是在做事之前，先想想结果是什么样子的。

说起来很简单，但做到并不容易。因为我们习以为常的思维模式是线性而顺序的，第一步做完，做第二步；第二步做完，做第三步。

这也情有可原。我们人类都是从远古时代演化而来，在那个食不果腹的时代里，倒着思考的用途并不大，人们甚至不确定自己能否见到明天的太阳。几十万年的进化留给我们很多短视的行为和思考习惯，因为这样的做法最为节省能量，把目光放长远是需要额外消耗能量的。

“以终为始”是一种反直觉的思维方式，是大多数人不具备的。所以，日常生活中，我们看到很多有趣的现象。

比如，大学毕业时，有很多人想考研，如果你问他为什么要考研，得到的理由通常是为了找个好工作。但考研真的能帮他找个好工作吗？不一定，因为找工作和考研根本就不是同一棵技能树。

如果真的是想找个好工作，那你就应该了解工作的要求是什么，怎样才能掌握工作要求的技能。

从后面这个角度出发，你会发现考研只是通往工作诸多道路中的一条，其他的路径也是可以到达的。比如，你应该找个实习的地方锻炼一下职业技能。这就是“以终为始”思考问题的方式。

回到前面“设计登录功能”的例子，对比“以终为始”的思维，你也许会替我的同事抱不平，他们或许也有“以终为始”的思路，只不过，他们的“终”和我这个客户的“终”不一样罢了。这就要说到做软件，本质上是在构建一个“集体想象”。

想象的共同体

如果你读过尤瓦尔·赫拉利的《人类简史》或《未来简史》，有一个说法你一定不陌生：想象的共同体。作者认为，人类历史发展的一个重要因素是“集体想象”，无论是国家、宗教，还是法律、习俗，都是人们达成的“集体想象”。人类就是认同了这些“集体想象”的一个共同体。

我们这些做软件的人其实就是一个想象的共同体，这个“集体想象”就是我们要做的软件，任何想象都需要一个载体将其展现出来，我们编写软件的过程就是将这个“集体想象”落实的过程。

既然是“集体想象”，那么在载体将想象呈现出来之前，我们的想象很难统一起来，都或多或少存在差异。

所以，任何事物都要经过两次创造：一次是在头脑中的创造，也就是智力上的或者第一次创造（Mental/First Creation），然后才是付诸实践，也就是实际的构建或第二次创造（Physical/Second Creation）。

我们在工作中遇到的很多问题，其实就是在于第一次创造没有做好，就进入到第二次创造。所以，我们在工作中会遇到很多“惊喜”，准确地说，是惊吓。

相比于第一次创造，第二次创造是一件成本很高的事。我们知道，软件开发最费时费力，一旦投入大量精力做出来，却发现与理解偏差甚大，所有人都会欲哭无泪。

所以，在动手做事之前，我们要在第一次创造上多下一些功夫，将相关各方的“集体想象”统一起来。以建筑为例，就是先在图纸上构思各种细节。对应到做软件，我们也可以做很多事，比如：

要给用户看产品的样子，可以用原型工具把它做出来，而不是非得把完整功能开发出来；

要呈现服务接口的样子，可以用模拟服务器搭出一个服务，而不用等后端全部开发完毕；

要让程序员知道要开发产品的细节，可以在任务上描述出软件各种场景给出的各种行为。

再回到前面“设计一个登录功能”的例子上，我的同事们在构建的其实是他们自己的想象，而不是我们共同的想象。这其中最大的一个区别就在于，没有人会为他们自己的想象买单的。

所以说，他们看到的“终”不是真正的终，只是一个自我的“终”，至于看到什么样的“终”，这取决于每个人的见识。

对做软件的人来说，我们应该把“终”定位成做一个对用户有价值的软件，能够为别人带来价值，自己的价值才能体现出来。

至此，你对“以终为始”已经有了一个初步的认识，有了这种思维方式，我们可以在工作中怎样运用它呢？

规划和发现

软件行业有很多英雄传说，一个人或者一个团队连续奋战一段时间，写好了一个软件，在上线前夜发现了一个问题，然后冒着“不成功便成仁”的风险，通宵达旦解决了问题，一战成名。

这种故事听起来让人热血沸腾，但仔细想想，为什么总在最后一刻发现问题？除了时间压力确实大的情况以外，大多数情况，他们还是一开始没有想好就动手了。

在团队内部，我一直坚持“以终为始”，让大家在执行任务之前，先倒着想想再动手规划，这样规划出来的工作更能瞄准真正的目标。举一个之前做产品的例子，当年在创业的时候，我们打算做一个物联网开发平台，但具体应该做成什么样子呢？

有了“以终为始”的思维，我们考虑的是别人会怎么用我们的平台。我们设计的方式是，用户到我们的网站，阅读相关文档，然后参考文档一步一步照着做。

这其中的一个关键点是：文档，特别是《起步走》的文档，这是用户接触我们这个平台的第一步，决定了他对我们产品的第一印象。

所以，我们决定从写《起步走》这个文档开始，这个文档描绘了用户怎样一步一步使用我们的开发平台，完成第一个“Hello World”级别的应用。请注意，这个时候，我们一行代码都没有写。

写好了这个《起步走》文档，团队的所有人对于我们的平台要做成什么样子，已经有了一个比较初步的认识。更重要的是，我们可以拿着这个文档，去和外部的人讨论这个尚未出世的平台。

人类是一个擅长脑补的群体，一旦有人看到了这个文档，他就已经可以构想出这个平台已经存在的样子，进而给出各种各样的反馈：“我认为这个地方可以这样做”“我觉得那个地方可以改改”。

所有这些反馈都是真实的，因为他们已经“看到了”一个真实的东西。正是这些真实的反馈，让我们逐渐地锁定了目标。之后，我们才开始动手写代码。

“以终为始”的方式，不仅仅可以帮我们规划工作，还可以帮我们发现工作中的问题。

有一次，我的团队在开发一个大功能，要将现有的系统改造成支持多租户的系统。也就是说，别的商家可以到我们的平台上发起申请，拥有和我们现有平台一样的能力。

功能来了，各个团队将任务分解，然后就各忙各的去了。但我有着习惯性的不安，总担心丢点什么，于是催着项目经理梳理一下上线流程。

是的，上线流程，虽然我们的代码还没开发完，但是本着“以终为始”的态度，我们就假设各个部分已经开发好了，来想一想上线应该怎么做。

果不其然，一梳理上线流程，我们便发现了问题：怎么识别不同的租户呢？有人给出的方案是设置一个 HTTP 头。但谁来设置这个 HTTP 头呢？没人仔细想过。于是，一个潜在的问题就这样被发现了，至少不用在未来为它加班了。至于解决方案，作为程序员，我们有的是办法。

事实上，在今天的软件开发实践中，已经有很多采用了“以终为始”原则的实践。

比如测试驱动开发。测试是什么？就是你这段代码的“终”，只有通过测试了，我们才有资格说代码完成了。当然，测试驱动开发想做好，并不是先写测试这么简单的。

比如持续集成，我们是要交付一个可运行的软件，倒着来想，最好的做法就是让软件一直处于可运行的状态，那就是持续地做集成。

概括地说，践行“以终为始”就是在做事之前，先考虑结果，根据结果来确定要做的事情。

这是“以终为始”这个内容版块的开篇，后面我会给你介绍这个原则在不同场景下的应用，也会引入一些现在行业内的最佳实践进行解析。相信会对你的实际工作有帮助。

总结时刻

有一段时间，网上流传着一个帖子，亚马逊 CTO 介绍亚马逊是如何开发一项产品的，简单来说，他们采用向后工作的方法，开发一项产品的顺序为：

写新闻稿；

写 FAQ（常见问题解答）；

写用户文档；

写代码。

今天我带你了解了“以终为始”的做事思路，回过头再来看这个帖子，相信你不难理解为什么亚马逊要这么做事情了。

人们习惯采用顺序思考的思维方式，几十万年的进化将这种思考模式刻在了我们的基因里。要成为更好的自己，我们要克服自身的不足，而这个做法很简单，那就是“以终为始”，做事倒着想，先考虑结果。

人类是一个想象的共同体，做软件的团队更是如此，而我们写出来的软件是我们将“集体想象”落地的载体。

任何事物都要经过两次创造：一次是在头脑中的创造，也就是智力上的或者第一次创造（Mental/First Creation），然后才是付诸实践，也就是实际的或第二次创造（Physical/Second Creation）。我们应该在第一次创造上多下功夫，统一集体想象，让目标更明确。

“以终为始”的思维可以帮助我们更好地规划我们手头任务，也可以帮助我们发现过程中的问题。

如果今天的内容你只能记住一件事，那请记住：遇到事情，倒着想。

最后，我想请你思考一下，在实际的工作或生活中，你有运用“以终为始”的思维方式吗？帮助你解决过哪些问题？欢迎在留言区写下你的想法。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

03 | DoD的价值：你完成了工作，为什么他们还不满意？

开始今天的讨论之前，我们先来看一个小故事。小李是一个程序员，有一天，项目经理老张来到他身边，和他商量一个功能特性的进度：

老张：这有一个任务需要完成，你看一下。
小李：这个不难，两天就能做完，两天以后就能上线。

两天以后，老张又来到小李的身边验收工作：

老张：怎么样，做完了吗？今天能上线吗？
小李：我的代码写完了。
老张：测试人员测过了吗？
小李：还没有。
老张：那今天能测完吗？
小李：那我就不知道了。
老张：什么？我可是答应了业务的人，今天一定要上线的！

很明显，老张有些愤怒，而小李也有些委屈。于是，老张、小李和测试人员一起度过了一个不眠之夜。

听完这个故事，你有什么感想呢？先不急，我们继续看后面的故事。

又过了几天，老张又来找小李，给小李安排一个很简单的功能。在小李看来，一天就能搞定，而按照老张给出的时间表，小李有两天时间处理这个功能。小李心中暗喜：看来我可以“偷得浮生一日闲”了。

两天以后，老张又来检查工作。

老张：这个功能开发完了吗？
小李：写完了，你看我给你演示一下。

小李熟练地演示了这个新写好的功能，这次老张很满意：

老张：做得不错。单元测试都写了吧？
小李：啊？还要写单元测试吗？
老张：要不为啥给你两天的时间？

怎么会这样？小李心里很委屈，自己明明已经很好地完成了工作，老张是不是故意在找自己的麻烦呢？

好，故事讲完了。是不是有些似曾相识的感觉呢？为什么小李辛辛苦苦地工作，老张却总能挑出毛病来呢？老张是不是来挑刺的呢？其实，老张才没那么闲，小李的委屈主要是因为他和老张对于“完成”有着不一样的理解。换句话说，他们之间存在一个理解的鸿沟。

理解的鸿沟

在这个模块里，我们讨论的主题是“以终为始”。那我们第一个问题就是，“终”到底是什么？在前面这个例子里，“终”就是“完成”，可是，小李认为他的活已经做完了，老张却认为他没做完。

怎么会这样？二人之所以有分歧，归根结底，就在于二人对“完成”的定义理解的不同。

在第一个故事里，作为项目经理，老张认为“完成”应该是“上线运行”，而程序员小李则认为“完成”是“功能代码编写完毕”。这中间存在的理解偏差，包括了测试人员的测试工作，可能还包括了运维人员的上线工作。

在第二个故事里，老张给了小李两天时间。小李认为这两天都是编写功能代码的，而老张想的是，小李应该自己写好功能代码和单元测试，可能还包括了功能测试，这中间的差异是测试代码的工作量。

因为双方的理解不一致，所以无论怎样努力，小李都不可能达成项目经理老张的要求，正所谓“南辕北辙”。

那该怎么办呢？小李会说，我又不是老张肚子里的蛔虫，怎么才能和他达成一致呢？答案很简单，既然双方的理解有差异，那就把这个差异弥合上，后面的问题便也不是问题了。

弥合差异的方式有很多，有一个最佳实践，它的名字叫 DoD（Definition of Done，完成的定义），从这个概念的名字便不难看出，它就是为了解决软件开发中常见的“完成”问题而生的。

完成的定义

DoD 这个概念本身并不复杂，它就是告诉我们怎样算是完成了，尽量减少因为理解偏差造成的各种浪费。具体怎么做呢？就是团队在开始工作前，先制定 DoD。以前面的场景为例，团队可以规定：

特性开发完成，表示开发人员经过了需求澄清、功能设计、编写代码、单元测试，通过了测试人员的验收，确保代码处于一个可部署的状态，相关文档已经编写完毕。

开发完成，表示开发人员编写好功能代码，编写好单元测试代码，编写好集成测试代码，测试可以通过，代码通过了代码风格检查、测试覆盖率检查。

大家都是聪明人，一旦 DoD 确定好了，谁该做什么事就一目了然了。这个时候，如果小李说“我已经开发完了”，却只是写好了功能代码，那就别怪老张手下无情了。

好了，你已经知道 DoD 是什么了，它简单到让人一目了然，相信你很快就能知道该怎样把它用到你的工作里。不过，我们不仅要知道怎么用，还要知道怎样让 DoD 更好地发挥作用。

DoD 是一个清单，清单是由一个个的检查项组成的，用来检查我们的工作完成情况。DoD 的检查项，就是我们开发产品所需的一系列有价值的活动。比如：编写代码、编写测试代码、通过测试人员验收等。什么样的活动是有价值的，也许每个团队的认识是不同的。但如果你的团队认为除了功能代码，其他都没价值，也许这是个信号，说明你的团队整体上是缺乏职业素养的，在这样的团队工作，前景并不乐观。

DoD 的检查项应该是实际可检查的。你说代码写好了，代码在哪里；你说测试覆盖率达标了，怎么看到；你说你功能做好了，演示一下。

DoD 是团队成员间彼此汇报的一种机制。别把“汇报”想复杂了，最简单的汇报就是说一句“这个功能做完了”。当我们有了 DoD，做事只有两种状态，即“做完”和“没做完”。在团队协作中，我们经常会听到有人说“这个事做完了 80%”，对不起，那叫没做完，根本没有 80% 做完的说法。

在前面的讨论中，我们所说的 DoD 只是从个人层面入手。在团队层面，我们也可以定义 DoD。

某个功能的 DoD，比如：这个功能特性已经开发完成，经过产品负责人的验收，处于一个可部署的状态。

一个迭代的 DoD，比如：这个迭代规划的所有功能已经完成。

一次发布的 DoD，比如：整个软件处于可发布的状态，上线计划已经明确。

站在 DoD 的肩膀上

至此，我们只是从软件开发团队内部协作的角度来谈 DoD。但实际上，它不仅局限在团队内部协作上，如果你可以放开思路，会发现 DoD 的思维在工作中用途非常广泛。比如，当我们需要和其他团队合作开发一个接口时，我们都知道第一步就是要把接口定义下来。

那么，怎样才算定义完成？很多团队认为落在字面上就够了。但是有了 DoD 的思维，我们定义接口，就会去明确定义可检查的检查项。那么在定义接口这件事上，什么才是“可检查”的呢？我们可以参照一个可运行的接口来进行评估。只要检查：

服务方提供的接口是不是和这个可运行的接口返回值是一样的；

调用方是否可以和这个可运行的接口配合使用。

谁错了，谁改去。你可能会问，应该参照哪些可运行的接口呢？这不难解决，现在模拟服务器的框架到处都是。如果你不介意的话，我的 Moco 就是这样一个开源项目，你可以看一下。

在协作中一旦确立好 DoD，我们甚至可以通过流程把它固化下来，从而更高效高质地完成工作。当然，我们在工作生活中难免会有一些临时的工作，它们没有复杂到需要一个流程，但是也可以用 DoD 思维来高效地解决。比如：

经常会有人过来，让我帮忙做些事。运用 DoD 的思维，我首先会问他我具体要做哪些事，确认好细节（相当于定义好“检查项”），然后我就知道，这个忙我能帮到什么程度。

我请别人帮忙的时候，也会很清楚告诉他，哪些事是需要他做的，尽量减少不必要的误解。

DoD 是一个的思维模式，是一种尽可能消除不确定性，达成共识的方式。我们本着“以终为始”的方式做事情，DoD 让我们能够在一开始就把“终”清晰地定义出来。

人与人协作中，经常会出现各种问题，根本原因就是，有太多因为理解差异造成的误解，进而浪费了大量的时间，而 DoD 就是一种将容易产生歧义的理念落到实处的方法。

总结时刻

好，我们来总结一下今天学到的内容。首先，你应该知道，人与人协作，总会有这样或那样的理解差异。开始协作之前，我们最好先同步一下彼此的理解，确保之后不会因为理解不一致，而让协作方措手不及。

怎样解决大家的理解偏差呢，我介绍了 DoD（完成的定义），它是行业中的一种最佳实践，能够在团队内部很好地同步大家对“完成”的理解。好的 DoD 是一个可以检查的清单，可以确保你不遗漏任何事情。

如果深入领会 DoD，你会发现 DoD 可以灵活应用在不同的协作场景中。比如应用于个人工作、团队工作，甚至跨团队工作。当然，你也可以将它灵活地运用于各种生活场景，弥合人与人理解之间的差异，更好地协作与沟通。

如果今天的内容你只能记住一件事，那请记住：在做任何事之前，先定义完成的标准。

最后，我想请你回想一下，你在工作或生活中，是否发生过因为双方理解差异导致的问题或不快呢？有了 DoD 的概念以后，你是不是有了一些新的想法呢？欢迎在留言区留言。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

04 | 接到需求任务，你要先做哪件事？

我们书接上文，继续讲程序员小李的故事。这次小李接到一个新的需求，让他开发一个单点登录的服务，经过几天的奋战，他顺利地写完了所有的代码。正好产品经理小王路过他身边，顺便问了他一下。

小王：单点登录做得咋样了？
小李：做完了，我给你演示一下。

小李演示了一遍自己做的功能，小王看上去很满意。

小王：不错。不过，怎么没有支持验证码？
小李：为什么要做这个？
小王：这不就是登录的一部分吗？
小李：哪里规定要做验证码了？
小王：现在做登录哪有不用验证码的？

我想你已经嗅到了双方谈话的火药味，这个时候如果双方都不能很好地控制自己的情绪，那接下来一场体力的较量可能就一触即发了。

为什么双方会有这么大的分歧呢？其中一个重要的原因是，开始实现这个需求之前，任务双方都没有清晰地定义好边界，没能把需求描述清楚。

需求描述的问题

在软件开发中，程序员做什么一般都由需求来定义。我们都知道，需求是软件开发的一个重要组成部分，但你可能并没有仔细想过，不同的需求描述方式，可能会影响我们程序员对需求的理解。

因为信息的传递是会衰减的，你不可能把你理解的信息 100% 传递给另外一个人，而这中间，如何传递，也就是如何描述将直接决定衰减的比例。

很多公司的软件开发模式是基于功能列表的，这个列表“规定”了程序员要做的功能，各个组从产品经理那里领来开发列表，然后“照单抓药”开始写代码。但是，通常这种功能列表只是一些简单的描述，你并不能看到全局。

很多团队的一个状态就是，程序员们都知道要开发的功能是什么，但这个功能是谁在什么样的场景下使用的，很多人却回答不上来。如果你去问他为什么要开发这个功能，他通常会说：这是功能列表里规定的。

这种功能列表式的需求描述方式，将一个完整的需求敲成了碎片。 只有所有功能全部开发完成，对接在一起的时候，才是“破镜重圆”的时刻。

也就是说，不到最后一刻，大多数人并没有一个完整的图景，这就相当于看不到完整的“终”。顺着这个思路做下去，你会在最后关头遇到许多意料之外的问题，其结果必然是手忙脚乱。

根据这种基于功能列表的需求描述，每个组在安排工作的时候，都会按照自己的理解进行功能排列。

所以，当你的组完成了一个功能时，这个功能却可能上不了线，因为你还要依赖于其他组的工作，而这个组不巧，却刚好把相关的功能开发排在了后面。

这还只是两个组之间有依赖的情况，如果需要多个组协同，可以想象，状况会多么糟糕。

所以，当我们对产品经理说“时间不足，砍掉一些需求吧。”得到的答案肯定是，“对不起，做不到，因为需求已破碎，没办法调整。”

因此，一些新的需求描述方式也就应运而生，这其中，用户故事（User Story）是我最喜欢的一种方式。它是站在用户的角度来描述了一个用户希望得到的功能，关注用户在系统中完成一个动作需要经过怎样的路径。既然它是“故事”，它就需要是一个完整的场景，可以讲述出来。

“用户故事”有什么用？

我们先来以用户密码登录为例，看看用户故事长什么样？一个完整的用户故事大致包含以下几个部分：

标题，简要地说明这个用户故事的主要内容，比如：注册用户使用用户名密码登录。

概述，简要地介绍这个用户故事的主要内容，一般会用这样的格式：
As a （Role）, I want to （Activity）, so that （Business Value）.
意思就是：作为一个什么角色，要做什么样的事，以便达成一种怎样的效果。其中最重要的是，告诉别人为什么要做这件事，虽然只有一句话，却往往是很多人欠缺的思考，只知做，不知为何做。
举个概述的例子：作为一个注册用户，我想要通过用户密码登录，以便我可以使用注册用户才能够使用的服务。

详述，详细地描述这个用户故事的完整流程，我们会把操作流程、用户界面等信息都放到这里。
比如：用户使用正确用户名和密码登录，就可以登录成功；如果密码不正确，则登录页面提示用户“用户名密码不正确”。基本上，看到这个部分，程序员就可以在心中描绘出这个用户故事的样子了。
超出范围的部分，比如：第三方登录不在范围内，这个部分主要是限定人们不要进一步发散。

验收标准，这个部分会描述一个正常使用的流程是怎样的，以及各种异常流程系统是如何给出响应的，这是程序员常常会欠缺的思考。它会把详述中很多叙述的部分变成一个具体的测试用例。比如，下面我给出的两个验收用例：
正常场景：给定一个注册用户张三，其用户名是 zhangsan，密码是 foobar，当张三使用 zhangsan 和 foobar 登录系统时，可以成功登录，登录成功后，跳转到用户中心。
异常场景：给定一个注册用户张三，其用户名是 zhangsan，密码是 foobar，当张三使用 zhangsan 和 wrong 登录系统时，登录失败，在登录页面上提示“用户名密码不正确”。

在前面的例子中，小张和小王之所以会对需求是否完成产生分歧，是因为大家对于需求完成的定义不同。对于这种情况，我们能怎么办呢？

这个模块的主题是“以终为始”，现在你看到了用户故事是如何描述需求的，你或许已经知道我要说什么了，没错，这里非常关键的一点就是“验收标准”。很多人学习用户故事，认为最重要的是记住“As…, I want to …, so that …”这样的需求描述方式。

在我看来，无论采用哪种需求描述方式，这部分也都是能说清楚的。那我们要从用户故事中学到什么呢？我认为就是用户故事的关键点：验收标准，它可以清晰地定义出需求边界。

验收标准非常重要的一环是异常流程的描述。大部分程序员都擅长解决正常流程，而异常流程则是最容易忽略的，也是产生扯皮的关键环节。既然容易扯皮，我们就在一开始把它定义清楚。怎么才算做完需求呢？验收标准说了算。

采用用户故事之后，我经常在写完了主要流程之后，再去看一下验收标准，为自己的开发查缺补漏。因为我知道，那是标准，达不成就不算任务完成。

当我们说自己开发完成，可以交给测试人员测试时，我们需要照着验收标准给测试人员演示一遍，证明我们的系统确实能够跑通。这之后，测试人员才会把系统接手过去，做更系统的测试。

验收标准给出了这个需求最基本的测试用例，它保证了开发人员完成需求最基本的质量。如果你了解 BDD（Behavior-Driven Development，也就是“行为驱动开发”），就可以按照验收标准中给出的内容编写验收测试用例了。

在实际工作中，许多产品经理把需求交给开发人员之前，很多细节是没想清楚的，那种功能列表式的需求常常只包含了正常路径，那些缺失的细节就是在后续的过程中，由开发人员补全的。用户故事就是一种固定的格式，让他们把这些应该想清楚的问题想清楚。

如果你的团队采用用户故事的格式进行需求描述固然好，如果不能，在功能列表中，补充验收标准也会极大程度地改善双方协作的效率。

你的角色

或许你会有这样的疑问，如果产品经理通过用户故事的方式，将需求实现细节都描绘得清清楚楚，那我们程序员的发挥空间在哪里？请注意，验收标准所给出实现细节应该是业务上的，程序员在这种问题上思考才是真正意义上的浪费时间，我们的发挥空间应该是在技术实现上。

然而，在现实情况中，很多团队做不到这种程度。

你会发现，我们在开发中之所以会“丢三落四”，很重要的一个原因是，在开发一个功能特性的时候，因为一些环节的缺失，我们不得已扮演了很多的角色，其中之一就是产品经理。你是一个专业的程序员，但大多数情况下，你却只是一个业余的产品经理，“丢三落四”就在所难免了。

或许你会说，我在一个小公司工作，公司没那么多人，没有专门的产品经理，只有我们几个“全世界都缺”的程序员，需求都是老板扔给我们的，谁来帮我们写验收标准呢？

没办法，答案只能是你自己。虽然你名义上是程序员，但当拿到一个需求的时候，你要做的事不是立即动手写代码，而是扮演产品经理的角色，分析需求，圈定任务范围。相信我，事前分析绝对比你拿一个写好的系统给老板，而他却告诉你这不是他想要的，好太多了。

另外我想提醒你注意的是，扮演不同角色的时候，我们的思考模式是不同的。还是以开发用户名密码登录为例，你想到的可能是：输入正确的用户名和密码可以正常登录，输入错误的用户名和密码不能登录，而且给出提示。

如果你只扮演开发人员的角色，想到这些就算不错了。但如果你扮演的是产品经理的角色，会从产品的角度进行思考，也就会看到不同的内容，比如：

登录是否需要验证码

是否需要第三方登录

用户名和密码的长度在系统内是否有限制

密码是否需要满足一定的规则

……

我知道，如果让你来填写，这个列表会更长。可能这并不是我们都需要完成的功能，但站在分析的角度，这都是我们要考虑的问题，一个登录功能，绝不仅仅是用户名和密码校验那么简单的。我们能想到这些，仅仅是因为我们正在扮演一个不同的角色。

所以，如果你要兼顾开发人员和产品经理两个角色，建议你先扮演好产品经理的角色，多花点时间把验收标准制定好，再回到开发人员的角色上去写代码。毕竟，最好维护的代码是没有写出来的代码。

总结时刻

需求，是软件开发中的一个关键环节，一旦需求理解出现问题，势必会造成大量的浪费。传统的功能列表只是简单罗列了要实现的功能，丢失了大量的上下文，会导致团队成员对于需求“只见树木不见森林”。

而在比较大的团队中，更是会将一个功能分拆到多个小团队中，每个人看到的只是功能碎片。于是，后来产生了其他的需求描述方式，比如用例和用户故事。

在实际的开发过程中，大量的分歧来自于对“需求完成”的定义。当我们把“以终为始”的原则应用在需求领域中，就会注意到，用户故事有一个非常重要的组成部分是验收标准。

验收标准不仅仅描述出了正常流程，也会关注到异常流程的处理，它也是我们验收测试用例的起点。一旦事先定义好验收标准，大量的扯皮工作就随之烟消云散了。

理解了验收标准的作用，即便我们不使用用户故事来定义需求，依然可以把用户故事中的关键点应用到自己的实践中，在功能列表的每个功能定义中，增加验收标准。

如果今天的内容你只能记住一件事，那请记住：在做任何需求或任务之前，先定好验收标准。

最后，我想请你回想一下，在实际工作中，你是如何澄清你的需求，或者因为需求不清晰给你造成了哪些困扰？欢迎在留言区写下你的想法。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

05 | 持续集成：集成本身就是写代码的一个环节

上一讲我们探讨了需求的“完成”，你现在知道如何去界定一个需求是否算做完了，这要看它是不是能够满足验收标准，如果没有验收标准，就要先制定验收标准。这一点，对于每一个程序员来说都至关重要。

在今天这一讲中，我们假设需求的验收标准已经制定清楚，接下来作为一个优秀的程序员，你就要撸起袖子准备开始写代码了。

不过在这里，我要问你一个问题：“是不是写完代码，工作就算完成了呢？”你或许会疑惑，难道不是这样吗？那我再问你：“代码是技术团队的交付物吗？”

你是不是发现什么不对劲了。没有人需要这堆文本，人们真正需要的是一个可运行的软件。写代码是程序员的职责，但我们更有义务交付一个可运行的软件。

交付一个可运行的软件，通常不是靠程序员个体奋战就能完成的，它是开发团队协作的结果。我们大多数人都工作在一个团队中，那我们写的代码是不是能够自然而然地就和其他人的代码配合到一起呢？显然没那么简单。

如果想将每个程序员编写的代码很好地组合在一起，我们就必须做一件事：集成。

但是集成这件事情，该谁做，该怎么做呢？我不知道你有没有思考过这个问题。在开始这个话题之前，我先给你讲个故事。

集成之“灾”

2009 年，我在一个大公司做咨询。对接合作的部门里有很多个小组，正在共同研发一个项目。他们工作流程是，先开发一个月，等到开发阶段告一段落，大项目经理再把各个小组最精锐成员调到一起开始集成。对他们来说，集成是一件大事，难度很大，所以要聚集精英来做。

这个项目是用 C 语言编写的，所以，集成的第一步就是编译链接。大家把各个小组写好的程序模块编译到一起，哪个模块有问题，哪个小组的精英就出手解决它。

如果第一天，所有模块能够编译链接到一起，大家就要谢天谢地了。之后才进入到一个正式“联调”的过程。

“联调”的目标，是把一个最基本的流程跑通，这样，集成才算完成。而对他们这个项目来说，“联调”阶段更像是场“灾难”。

为什么？你想想，一个大部门有若干个团队，每个团队都在为同一个项目进行代码开发，周期为一个月。这一个月期间，所有团队的程序模块汇总在一起，体量会非常庞大。那么这些内容中，出现错误需要改动的可能性也就非常大，需要改动的量也就非常大。因此他们集成“联调”所需要的时间也会非常长。

即便他们调动各组精英，完成一次项目集成的时间至少也需要 2～3 天，改动量稍大，可能就要一周了。虽然我不知道你所处公司的现状是什么样的，但大概率地说，你在职业生涯中，会遇到过类似的场景。那怎么去解决这个问题呢？

迈向持续集成

聪明的你作为旁观者一定会想，在这个故事里，为什么他们要在开发一个月后才做集成呢？为什么不能在开发一周后，甚至是更短的时间内就集成一次？

这是一个行业中常见的痛点，所以，就会有人不断地尝试改进，最先取得的突破是“每日构建”。

1996 年，Steve McConnel 出版了一本著作《Rapid Development》，国内译作《快速软件开发》。在这本书中，作者首次提出了解决集成问题的优秀实践：Daily Build，每日构建。通过这个名字，我们便不难看出它的集成策略，即每天集成一次。

这在当时的人看来，已经是“惊为天人”了。就像上面提到的例子一样，当时的人普遍存在一种错误认知：集成不是一件容易的事，需要精英参与，需要很长时间，如果每天都进行集成，这是想都不敢想的事情。

实际上，每日构建背后的逻辑很简单：既然一段时间累积下来的改动量太过巨大，那一天的时间，累积的改动量就小多了，集成的难度也会随之降低。

你会看到，对比最后做集成和每日构建，这两种不同的做法都是在处理改动量和集成时间的关系。只不过，一个是朝着“长”的方向在努力，一个则瞄准“短”的方向。最后的事实证明，“长”的成了恶性循环，“短”的成了最佳实践。

既然，我们认同了只要增加集成的频率，就可以保证在每次集成时有较少的改动量，从而降低集成难度。

那问题来了？究竟要在开发后多久才进行一次集成呢？是半天、两个小时、还是一个小时呢？倘若这个想法推演到极致，是否就变成了只要有代码提交，就去做集成？

没错，正是基于这样的想法，有人尝试着让开发和集成同时进行，诞生了一个关于集成的全新实践：持续集成。

持续集成一个关键的思维破局是，将原来分成两个阶段的开发与集成合二为一了，也就是一边开发一边集成。

持续集成这个想法固然好，但是不是需要有专人负责盯着大家的工作，只要有人提交了代码，这个负责人就要去集成呢？显然，这在真实工作中是行不通的。

既然是程序员的想法，程序员解决问题的方案自然就是自动化这个过程。于是，有人编写了一个脚本，定期去源码服务器上拉代码，出现程序更新时，就自动完成构建。

后来，人们发现这段脚本与任何具体项目都是无关的。于是，把它进一步整理并发布出来，逐步迭代发展成为今天广为人知的持续集成服务器。

在 2000 年时，“软件行业最会总结的人” Martin Fowler 发布了一篇重量级文章“Continuous Integration”。

之后一年，由 Martin Fowler 所在的 ThoughtWorks 公司发布了市面上第一款持续集成服务器 CruiseControl。CruiseControl 可谓是持续集成服务器的鼻祖，后来市面上的服务器基本都是在它的基础上改良而来的。

Martin Fowler 的重磅文章和首款持续集成服务器的问世，让软件行业对持续集成进行了更为深入的探讨，人们对于持续集成的认知程度一路走高，持续集成服务器成为了开发团队在集成阶段最得心应手的工具。围绕着持续集成的一系列行为准则逐渐成型。

以至于发展到 2006 年，Martin Fowler 不得不重写了“Continuous Integration”这篇文章。之后人们更是以持续集成为基础，进一步拓展出持续交付的概念。

人类对工具是有偏爱的，持续集成服务器的发布，将持续集成从一项小众实践逐步发展成为今天行业的“事实”标准。

“地面上”的持续集成

然而，即便持续集成已经发展多年，至今整个行业在对它的应用上，却并未达到同步的状态。有趣的是，有一部分公司虽然还无法实现持续集成，但是因为持续集成服务器的出现，反而可以做到每日构建。

这不难理解，每日构建的概念虽然早早就提出来了，但在那个时期，行业里真正践行每日构建的公司并不多，其根本原因就在于，每日构建最初都是一些指导原则，缺乏工具的支持。而每日构建和持续集成最根本的区别在于构建时机，而这只是持续集成服务器的一个配置选项而已。

当然，行业内有一部分公司已经可以将持续集成运用得得心应手，而也有相当大的一部分人还在为集成而痛苦不堪，比如我前面提到的咨询项目。

这个项目是我在 2009 年时参与的。也就是说，此时距离 Martin Fowler 最初写下“Continuous Integration”已经过去了 9 年，甚至距离这篇文章的更新版发布也已经过去了 3 年，更不要说距离 McConnell 提出“每日构建”已经 13 年。

即便以当时的时间坐标系来看，这个项目的集成实践水平至少落后行业 10 年以上。没错，他们甚至连每日构建都还差很远。

时至今日，持续集成早就是成熟得不能再成熟的实践了。然而，据我所知，许多公司依然处于集成要依赖于“英雄”的蛮荒阶段。

虽然我们在同一个时代写代码做开发，但在技术实践层面，不同的团队却仿佛生活在不同的年代。这也是我们要学习的原因。

也许，目前国内对于持续集成的实践水平还处于较为原始的状态，这是个坏消息。但好消息是，我们可以通过更多的学习，对集成有足够的了解，从而一步到位地进入到最先进的状态中。

无需停留在以精英为核心的集成时代，也可以完全不理会每日构建，我希望你拥有这个时代的集成观，直接开始持续集成。

如果有了持续集成的集成观，我们该怎么看待开发这件事呢？开发和集成就不再是两个独立的过程，而是合二为一成为一体。

基于这样的理解，我们就不能再说代码写完了，就差集成了，因为这不叫开发的完成。一个好的做法是尽早把代码和已有代码集成到一起，而不应该等着所有代码都开发完了，再去做提交。

怎样尽早呢？你需要懂得任务分解，这是我们在之后的“任务分解”主题下会讲到的内容。

总结时刻

在软件开发中，编写代码是很重要的一环，但程序员的交付物并不应该是代码，而是一个可工作的软件。当我们在一个团队中工作的时候，把不同人的代码放在一起，使之成为一个可工作软件的过程就是集成。

在很长一段时间内，集成都是软件行业的难题，改动量和集成时间互相影响。幸运的是，不同的人在不同的方向尝试着改变，结果，同时加大改动量和集成时间的人陷入了泥潭，而调小这两个参数的人看到了曙光。

每日构建作为早期的一种“最佳实践”被提了出来，但因为它基本上都是原则，没有得到广泛的应用。当人们进一步“调小”参数后，诞生了一个更极致的实践：持续集成，也就是每次提交代码都进行集成。

真正让持续集成成为行业最佳实践的是，Martin Fowler 的文章以及持续集成服务器。持续集成的思维让我们认识到，开发和集成可以合二为一。我们应该把开发的完成定义为代码已经集成起来，而站在个体的角度，我们应该尽早提交自己的代码，早点开始集成。

如果今天的内容你只能记住一件事，那请记住：尽早提交代码去集成。

最后，我想请你分享一下，在实际工作中，你遇到过哪些由集成带来的困扰？欢迎在留言区写下你的想法。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

06 | 精益创业：产品经理不靠谱，你该怎么办？

前面谈到验收标准时，我们说的实际上是确定性需求，也就是说，我们已经知道了这个需求要怎么做，就差把它做出来了。而有时候，我们面对的需求却是不确定的，比如，产品经理有了一个新想法，那我们该如何应对呢？

今天，我们从 IT 行业一个极为经典的话题开始：程序员如何面对产品经理。我先给你讲一件发生在我身边的事。

有一次，我们一大群人在一个大会议室里做一个产品设计评审，来自产品团队和技术团队的很多人都参与到这个评审中。一个产品经理正对着自己的设计稿，给大家讲解一个新的产品特性。

这个公司准备将自己的服务变成了一个云服务，允许第三方厂商申请，这个产品经理给大家讲解的就是第三方厂商自行申报开通服务的流程。听完前面基本情况的介绍，我举手问了几个问题。

我：这个服务会有多少人用？
产品经理：这是给第三方厂商的人用的。
我：我问的是，这个服务会有多少人用。
产品经理：每个第三方厂商的申请人都会用。
我：好，那你有预期会有多少第三方厂商申请呢？
产品经理：呃，这个……我们没仔细想过。
我：那现在给第三方厂商开通服务的具体流程是什么。
产品经理：第三方厂商申请，然后，我们这边开通。
我：好，这个过程中，现在的难点在哪里？这个审批过程能让我们的工作简化下来吗？
产品经理：……
我：那我来告诉你，现在开通第三方厂商服务，最困难的部分是后续开通的部分，有需要配置服务信息的，有需要配置网络信息的。目前，这个部分还没有很好的自动化，前面审批的部分能够自动化，对整个环节优化的影响微乎其微。

我的问题问完了，开发团队的人似乎明白了什么，纷纷表示赞同我的观点。这个审批流程本身的产品设计并不是问题，但我们的时间和资源是有限的，关键在于，要不要在这个时间节点做这个事。准确地说，这是优先级的问题。

此刻，作为开发团队一员的你，或许会有种快感，把产品经理怼回去，简直大快人心。好吧，作为一个正经的专栏，我们并不打算激化产品经理和开发团队的矛盾，而是要探讨如何做事情才是合理的。

之所以我们能很好地回绝了产品经理不恰当的需求，是因为我们问了一些好问题，但更重要的是，我们为什么能问出这些问题。

产品经理是个新职业

在做进一步讨论之前，我们必须认清一个可悲的现状，IT 行业中大多数人的专业程度是不够的。

IT 行业是一个快速发展的行业，这个行业里有无数的机会，相对于其它行业来说，薪资水平也要高一些，这就驱使大量的人涌入到这个行业。

也因为这是一个快速发展的行业，很多职位都是新近才涌现出来的，比如，在 2010 年之前很少有专职的前端工程师，之前的工程师往往要前后端通吃。

产品经理便是随着创业浪潮才风起云涌的职位。既然这是个“新”职位，往往是没有什么行业标准可言的。所以，你会看到很多行业乱象：很多人想进入 IT 行业，一看程序员需要会写代码，觉得门槛高，那就从产品经理开始吧！这些人对产品经理岗位职责的理解是，告诉程序员做什么。

这和郭德纲口中外行人“如何认识相声”是一个道理，以为会说话就能说相声，殊不知，这是个门槛极高的行业。产品经理也一样，没有良好的逻辑性，怎么可能在这个行业中有好的发展。

如果你遇到的产品经理能给出一个自洽的逻辑，那么恭喜你，你遇到了还算不错的产品经理。多说一句，这个行业中专业度不够的程序员也有很多，人数比产品经理还多，道理很简单，因为程序员的数量比产品经理的数量多。

这么说并不是为了黑哪个职位，而是要告诉大家，我们必须要有自己的独立思考，多问几个为什么，尽可能减少掉到“坑”里之后再求救的次数。

回到前面的主题，我们该怎么与产品经理交流呢？答案还在这个部分的主题上，以终为始。我们是要做产品，那就需要倒着思考，这个产品会给谁用，在什么场景下怎么用呢？

这个问题在 IT 行业诞生之初并不是一个显学，因为最初的 IT 行业多是为企业服务的。企业开发的一个特点是，有人有特定的需求。在这种情况下，开发团队只要把需求分析清楚就可以动手做了，在这个阶段，团队中的一个关键角色是业务分析师。即便开发出来的软件并不那么好用，企业中强行推动，最终用户也就用了。

后来，面向个人的应用开始出现。在 PC 时代和早期的互联网时代，软件开发还基本围绕着专业用户的需求，大部分软件只要能解决问题，大家还是会想办法用起来的。

但是随着互联网深入人心，软件开始向各个领域蔓延。越来越多的人进入到 IT 行业，不同的人开始在各个方向上进行尝试。这时候，软件开发的主流由面向确定性问题，逐渐变成了面向不确定性问题。

IT 行业是这样一个有趣的行业，一旦一个问题变成通用问题，就有人尝试总结各种最佳实践，一旦最佳实践积累多了，就会形成一套新的方法论。敏捷开发的方法论就是如此诞生的，这次也不例外。

精益创业

最早成型的面向不确定性创造新事物的方法论是精益创业（Lean Startup），它是 Eric Ries 最早总结出来的。他在很多地方分享他的理念，不断提炼，最终在 2011 年写成一本同名的书：《精益创业》。

看到精益创业这个名字，大多数人会优先注意到“创业（Startup）”这个词。虽然这个名字里有“创业”二字，但它并不是指导人们创业挣大钱的方法论。正如前面所说，它要解决的是面向不确定性创造新事物。

只不过，创业领域是不确定性最强而且又需要创造新事物的一个领域，而只要是面向不确定性在解决问题，精益创业都是一个值得借鉴的方法论。比如，打造一个新的产品。

精益创业里的“精益”（Lean）是另外一个有趣的词。精益这个词来自精益生产，这是由丰田公司的大野耐一和新乡重夫发展出来的一套理论。

这个理论让人们开始理解价值创造与浪费之间的关系。创造价值是每个人都能理解的，但减少浪费却是很多人忽略的。所以，把这几个理念结合起来，精益创业就是在尽可能少浪费的前提下，面向不确定性创造新事物。

那精益创业到底说的是什么呢？其实很简单。我们不是要面向不确定性创造新事物吗？既然是不确定的，那你唯一能做的事情就是“试”。

怎么试呢？试就要有试的方法。精益创业的方法论里，提出“开发（build）- 测量（measure）- 认知（learn）”这样一个反馈循环。就是说，当你有了一个新的想法（idea）时，就把想法开发成产品（code）投入市场，然后，收集数据（data）获取反馈，看看前面的想法是不是靠谱。

得到的结果无非是两种：好想法继续加强，不靠谱的想法丢掉算了。不管是哪种结果，你都会产生新的想法，再进入到下一个循环里。在这个反馈循环中，你所获得的认知是最重要的，因为它是经过验证的。在精益创业中，这也是一个很重要的概念：经过验证的认知（Validated Learning）。

既然是试，既然是不确定这个想法的有效性，最好的办法就是以最低的成本试，达成同样一个目标，尽可能少做事。精益创业提出一个非常重要的概念，最小可行产品，也就是许多人口中的 MVP（Minimum Viable Product）。简言之，少花钱，多办事。

许多软件团队都会陷入一个非常典型的误区，不管什么需求都想做出来看看，殊不知，把软件完整地做出来是最大的浪费。

你为什么要学习精益创业？

或许你会问，我就是一个程序员，也不打算创业，学习精益创业对我来说有什么用呢？答案在于，精益创业提供给我们的是一个做产品的思考框架，我们能够接触到的大多数产品都可以放在这个框架内思考。

有了框架结构，我们的生活就简单了，当产品经理要做一个新产品或是产品的一个新特性，我们就可以用精益创业的这几个概念来检验一下产品经理是否想清楚了。

比如，你要做这个产品特性，你要验证的东西是什么呢？他要验证的目标是否有数据可以度量呢？要解决的这个问题是不是当前最重要的事情，是否还有其他更重要的问题呢？

如果上面的问题都得到肯定的答复，那么验证这个目标是否有更简单的解决方案，是不是一定要通过开发一个产品特性来实现呢？

有了这个基础，回到前面的案例中，我对产品经理提的问题，其实就是在确定这件事要不要做。事实上，他们当时是用一个表单工具在收集用户信息，也就是说，这件事有一个可用的替代方案。鉴于当时还有很多其它需求要完成。我建议把这个需求延后考虑。

总结时刻

程序员与产品经理的关系是 IT 行业一个经典的话题。许多程序员都会倾向于不问为什么就接受来自产品经理的需求，然后暗自憋气。

实际上，产品经理是一个新兴职业，即便在 IT 这个新兴行业来看，也算是新兴的。因为从前的 IT 行业更多的是面向确定性的问题，所以，需要更多的是分析。只有当面向不确定性工作时，产品经理才成为一个行业普遍存在的职业。所以，在当下，产品经理并不是一个有很好行业标准的职位。

比较早成型的面向不确定创造新事物的方法论是精益创业，它提出了“开发（build）- 测量（measure）- 认知（learn）”这样一个反馈循环和最小可行产品的概念。

当产品经理让我们做一个新的产品特性时，我们可以从精益创业这个实践上得到启发，向产品经理们问一些问题，帮助我们确定产品经理提出的需求确实是经过严格思考的。

如果今天的内容你只记住一件事，那请记住：默认所有需求都不做，直到弄清楚为什么要做这件事。

最后，我想请你回想一下，你和产品经理日常是怎样做交流的呢？欢迎在留言区写下你的想法。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

07 | 解决了很多技术问题，为什么你依然在“坑”里？

在前面的内容中，我给你介绍了几个体现“以终为始”原则的实践，包括怎样界定工作是否完成的 DoD、怎样判定需求是否完成的验收标准、还有怎样验证产品经理给出的产品特性是否合理的精益创业理念。

了解了这些内容，可能你会想：我为什么要关心这些啊？我是程序员啊！难道我不应该安安静静地写程序吗？为什么要操心其他人的工作做得好坏？如果我管了那么多事，我还是不是一个程序员，到底哪里才是我的“终”呢？

今天这一讲，我们就来聊聊这个让许多人困惑的问题。因为只有要跳出程序员的角色看问题，工作才会变得更加高效。

“独善其身”不是好事

在需要与人协作的今天，独善其身可不一定是好的做法。我先给你讲一个发生在我身边的故事。

有一次，我的团队要开发一个数据服务层，准备作为一个基础设施提供给核心业务系统。开发没多久，一个团队成员和我说，他的工作进展不顺利，卡在了一个重要问题上，他想不明白该如何在多个实例之间分配 ID。

我听完之后，有些疑惑，为什么要考虑这个和功能无关的问题呢？他解释说，因为我们的系统需要保证消息的连续性，所以他设计了消息 ID，这样下游系统可以就通过消息 ID 识别出是否有消息丢失。

这是没错的，但我奇怪的是，他为什么要在多个实例之间协调呢？他给出的理由是，这么做，是出于考虑应对将来有多实例并发场景的出现。然而事实是，我们当下的需求应对的是单实例的情况。

我了解情况之后，马上跟他说清楚这一点，让他先把第一步做出来。这个同事还是有些担心未来如何做扩展。我告诉他，别纠结，先把第一步做出来，等后面真的有需求，我们再考虑。同事欣然答应了。

其实，这个同事的技术能力非常强，如果我不拦着他，他或许真能实现出一个完美的技术方案，但正如他自己所纠结的那样，这个方案可能要花掉他很长时间。但这真的是我们想要的吗？以现阶段的目标来看，根本没有这样的需求。

我们一直在强调“以终为始”。所谓“终”，其实就是我们的做事目标。虽然大家工作在一起，朝着一个共同的大目标前进，但真的到了一个具体的问题上，每个人看到的目标却不尽相同。

我之所以能把同事从一个纠结的状态中拉出来，是因为我看到的是需求，而他看到的是一个要解决的技术问题。所以，我们俩在对目标的理解上是有根本差异的。

你也许会认为，我和同事之所有这样的差异，是角色上的差异，我在项目里承担的角色要重一些，而且我的工作时间比同事要长一些。但不知道你有没有想过，不同角色的差异到底在哪里呢？

角色的差异

作为一个在职场工作的人，每个人都有一颗渴望得到认可的心，希望自己在职业的阶梯上步步高升。假如今天就让你往上走一个台阶，比如，你原来在项目里打杂，现在成为项目的主力，或者，你已经对项目细节驾轻就熟，即将委任你为项目负责人。你是否能胜任呢？

你需要补充的东西是什么？换句话说，你和你职业台阶中的上一级那个人，差异到底是什么？

也许你会说，他比我来的时间长，或者说，他每天的主要工作就是开会。如果真的是这样，那是不是只要你凑足这个条件，就可以到达他的位置呢？显然不是。

不同角色工作上真正的差异是上下文的不同。

这是什么意思呢？以前面的问题为例，你在项目里打杂，你只能关注到一个具体的任务，而项目主力心目中是整个系统。虽然写的代码都一样，但你看到的是树木，人家看到的是森林，他更能从全局思考。

同样，项目负责人的工作，虽然包括在项目组内的协调，但还有一部分工作是跨项目组的，他需要考虑你们项目组与其他组的互动。所以，他工作的上下文是在各组之间，包括技术和产品等方面。

再上升一个层面，部门负责人要协调内部各个组，同时要考虑部门之间的协调。而公司负责人考虑的上下文甚至要跳脱公司内部，进入到行业层面。

你可能会问，好了，我知道不同角色的上下文有差异了，但这对我意味着什么呢？

我们先从工作角度看。回到前面我分享的那个故事，你可能注意到了，我并不是靠技术能力解决了问题，而是凭借对需求的理解把这个问题绕过去了。

之所以我能这样做，原因就在于我是在一个更大的上下文里工作。类似的故事在我的职业生涯中发生过无数次，许多令程序员愁眉不展的问题，换个角度可能都不是问题。

技术是一把利刃，程序员相信技术可以改变世界，但并不是所有问题都要用技术解决。有这样一种说法，手里有了锤子，眼里都是钉子。花大力气去解决一个可能并不是问题的问题，常常是很多程序员的盲区。

之所以称之为盲区，是因为很多人根本看不见它，而看不见的原因就在于上下文的缺失，也就是说，你只在程序员的维度看问题。

多问几个为什么，交流一下是不是可以换个做法，许多困惑可能就烟消云散了。而能想到问这样的问题，前提就是要跳出程序员角色思维，扩大自己工作的上下文。

虽然我不是项目主力，但不妨碍我去更深入地了解系统全貌；虽然我不是项目负责人，但不妨碍我去了解系统与其他组的接口；同样，虽然我不是项目经理，但我可以去了解一下项目经理是怎样管理项目的；虽然我不是产品经理，但了解一个产品的设计方法对我来说也是有帮助的。

当你对软件开发的全生命周期都有了认识之后，你看到的就不再是一个点了，而是一条线。与别人讨论问题的时候，你就会有更多的底气，与那些只在一个点上思考的人相比，你就拥有了降维攻击的能力。

现在你知道为什么你的工作总能让老板挑出毛病了吧！没错，工作的上下文不同，看到的维度差异很大。单一维度的思考，在多维度思考者的眼里几乎就是漏洞百出的。

当扩大了自己工作的上下文时，我们的目标就不再局限于一个单点，而是会站在更高的维度去思考，解决问题还有没有更简单的方案。许多在低一级难以解决的问题，放到更大的上下文里，根本就不是问题。

我的职业生涯中经常遇到这样的情况，在一个特定的产品设计下，我总觉得设计的技术方案有些不优雅的地方，而只要产品设计微调一下，技术方案一下子就会得到大幅度提升。在这种情况下，我会先去问产品经理，是否可以这样调整。只要不是至关重要的地方，产品经理通常会答应我的要求。

在更大的上下文工作

扩展自己工作的上下文，目光不再局限于自己的一亩三分地，还可以为自己的职业发展做好布局。在这个方面，我给你分享一个不太成功的案例，就是我自己的故事。

我是属于愚钝型的程序员，工作最初的几年，一直把自己限定在程序员的上下文里，最喜欢的事就是安安静静地写代码，把一个系统运作机理弄清楚会让我兴奋很长一段时间。

我的转变始于一次机缘巧合，当时有一个咨询项目，负责这个项目的同事家里有些事，需要一个人来顶班，公司就把我派去了。

到了咨询项目中，我自己习惯的节奏完全乱掉了，因为那不是让代码正常运作就可以解决的问题，更重要的是与人打交道。

有很长一段时间，我一直处于很煎熬的状态，感谢客户没有把我从这个项目赶出来，让我有了“浴火重生”的机会。

为了让自己从这种煎熬的状态中摆脱出来，我必须从代码中走出来，尽量扩大自己思考的边界。经过一段时间的调整，我发现与人打交道也没那么难，我也能更好地理解一个项目运作的逻辑，因为项目运作本质上就是不同人之间的协作。

突破了自己只愿意思考技术的限制，世界一下子宽阔了许多。所以，后来才有机会更多地走到客户现场，看到更多公司的项目运作。虽然我工作过的公司数量并不多，但我却见过很多公司是如何工作的。

再后来，我有机会参与一个新的分公司建设工作中，这让我有了从公司层面进行思考的角度。对于员工招聘和培养，形成了自己一套独立的思考。

这些思考在我创业的过程中，帮我建立了一支很不错的团队。而创业的过程中，我又有了更多机会，去面对其他公司的商务人员，从而建立起一个更大的上下文，把思考从公司内部向外拓展了一些。

回过头来看自己的生涯时，我发现，因为不愿意拓展自己的上下文，我其实错过了很多职业发展的机会。所幸我还有机会突破自己，让自己走出来，虽然走的速度不如理想中快，但至少一直在前进，而不是原地打转。这也是我告诫你一定要不断扩大自己工作上下文的原因。

机会总是垂青那些有准备的人，尤其在公司规模不大的时候，总有一些跳跃式的发展机会。

我见过有人几年之内从程序员做到公司中国区负责人，只是因为起初公司规模不大，而他特别热心公司的很多事情，跳出了固定角色的思维。所以，当公司不断发展，需要有人站出来的时候，虽然没有人是完全合格的，但正是他的热心，让他有了更多的维度，才有机会站到了前排。

当然，随着公司规模越来越大，这种幅度极大的跳跃是不大可能的。江湖上流传着一个华为的故事，一个新员工给任正非写了封万言书，大谈公司发展，任正非回复：“此人如果有精神病，建议送医院治疗，如果没病，建议辞退。”

因为一旦公司规模大了，你很难了解更大的上下文，很多关于公司的事情，你甚至需要从新闻里才知道。

本质上，一个人能在自己的工作范围内多看到两三级都是有可能的。在公司规模不大时，从基层到老板没有太多层级，跳跃就显得很明显，而公司一大，层级一多，从低到顶的跳跃就不太可能了，但跨越级别跳跃是可能的。

所以我希望你跳出程序员思维，这不仅仅是为了工作能够更高效，也是希望你有更好的发展机会。

总结时刻

程序员总喜欢用技术去解决一切问题，但很多令人寝食难安的问题其实根本不是问题。之所以找不出更简单的解决方案，很多时候原因在于程序员被自己的思考局限住了。

不同角色工作真正的差异在于上下文的差异。在一个局部上下文难以解决的问题，换到另外一个上下文甚至是可以不解决的。所以说无论单点有多努力也只是局部优化，很难达到最优的效果。

想把工作做好，就需要不断扩大自己工作的上下文，多了解一下别人的工作逻辑是什么样的，认识软件开发的全生命周期。

扩大自己的上下文，除了能对自己当前的工作效率提高有帮助，对自己的职业生涯也是有好处的。随着你看到的世界越来越宽广，得到的机会也就越来越多。

如果今天的内容你只记住一件事，那请记住：扩大自己工作的上下文，别把自己局限在一个“程序员”的角色上。

最后，我想请你分享一下，在你的工作中，有哪些因为你扩大了工作上下文而解决的问题呢？欢迎在留言区写下你的想法。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

08 | 为什么说做事之前要先进行推演？

经过前面的学习，想必你已经对“以终为始”这个原则有了自己的理解。你知道接到一个任务后，要做的不是立即埋头苦干，而是要学会思考，找出真正的目标。那目标明确之后，我们是不是就可以马上开始执行了呢？

先不着急给出你的答案，今天的内容从一个技术任务开始。

一个技术任务

你现在在一家发展还不错的公司工作。随着业务的不断发展，原来采用的关系型数据库越发无法满足快速的变化。于是，项目负责人派你去做个技术选型，把一部分业务迁移到更合适的存储方式上。

经过认真的调研和思考，你给负责人提出了自己的建议，“我们选择 MongoDB。”出于对你的信任，负责人无条件地同意了你的建议，你获得了很大的成就感。

在你的喜悦尚未消退时，负责人进一步对你委以重任，让你来出个替代计划。替代计划？你有些不相信自己的耳朵，嘴里嘟囔着：“把现在存到数据库的内容写到 MongoDB 不就成了，我就一个表一个表地替换。难道我还要把哪天替换哪个表列出来吗？”

刚刚还对你欣赏有加的负责人，脸色一下子沉了下来。“只有表改写吗？”他问你。你一脸懵地看着他，心里想，“不然呢？”

“上线计划呢？”负责人问。

“我还一行代码都没写呢？”你很无辜地看着负责人。

“我知道你没写代码，我们就假设代码已经写好了，看看上线是怎样一个过程。”

“不是发新版本就好了吗？”你还是不知道负责人到底想说什么。

“你能确定新版代码一定是对的吗？”

虽然你已经叱咤编程很多年，但作为老江湖，一听这话反而是有些怯的。“不能。”你痛快地承认了。

“一旦出错，我们就回滚到上一个版本不就成了。”常规的处理手段你还是有的。

“但数据已经写到了不同的存储里面，查询会受到影响，对不对？”负责人一针见血。

“如果这个阶段采用两个数据存储双写的方案，新代码即便出问题，旧存储的代码是正常，我们还有机会回滚。”你一下子就给出了一个解决方案，咱最不怕出问题了。

“对。”负责人认同了你的做法，一副没看错人的神情。“让你出上线方案，就是为了多想想细节。”

你终于明白了负责人的良苦用心，也就不再大意。很快，你就给出了一份更详尽的上线方案。

你把这个方案拿给负责人看，信心满满，觉得自己够小心，一步一步做，没有任何问题。但负责人看了看你的上线计划，眉头逐渐锁了起来，你知道负责人还是不满意，但不知道还差在哪里？

“原有的数据怎么办？”负责人又问了一个问题。你一下子意识到，确实是问题。“没有原有数据，一旦查询涉及到原有数据，查询的结果一定是错的。所以，还应该有一个原有数据的迁移任务。”你尴尬地笑了笑。

负责人微笑着看着你。“好吧，从我的角度看差不多了，你可以再仔细想想。然后，排一个开发任务出来吧！”

你当然不会辜负负责人的信任，很快排出了开发任务。

看着排出的任务，你忽然困惑了。最开始只是想写个读写新库的组件，怎么就多出这么些任务。此外，你还很纳闷为什么负责人总是能找到这么多问题。

一次个人回顾

你想起之前的工作里有过类似的场景，那个负责人也是让你独立安排任务。通常，你最初得到的也是一个简单的答案，从当时的心境上看，你是很有成就感的。

只是后来的故事就不那么美妙了，上线时常常出现各种问题，你和其他同事们手忙脚乱地处理各种异常。当时顶着巨大压力解决问题的场景，你依然记忆犹新。解决完问题离开公司时，天空已经泛起鱼肚白。

而似乎自从加入了现在的公司，这种手忙脚乱的场景少了很多。你开始仔细回想现在这个负责人在工作中的种种。从给大家机会的角度来看，这个负责人确实不错，他总会让一个人独立承担一项任务。只不过，他会要求大家先将任务分解的结果给他看。

拿到组里任何一个人的开发列表之后，他都会问一大堆问题，而且大多数情况下，他都会问到让人哑口无言。说句心里话，每次被他追问心里是挺不舒服的，就像今天这样。

本来在你看来挺简单的一件事，经过他的一系列追问，变成了一个长长的工作列表，要做的事一下子就变多了。毕竟谁不愿意少做点活呢！

不过，你不得不承认的一点是，加入这个公司后，做事更从容了。你知道无论做的事是什么，那些基本的部分是一样的，差别体现在事前忙，还是事后忙，而现在这家公司属于事前忙。于是，你开始把前一家公司上线时所忙碌的内容，和现在负责人每次问的问题放在一起做对比。

这样一梳理，你才发现，原来负责人问的问题，其实都是与上线相关的问题。包括这次的问题也是，上线出问题怎么办，线上数据怎么处理等等。

你突然意识到一个关键问题，其实负责人每次问的问题都是类似的，无论是你还是其他人，他都会关心上线过程是什么样，给出一个上线计划。即便我们还一行代码都没有，他依然会让我们假设如果一切就绪，应该怎样一步一步地做。

你终于明白了，之前的项目之所以手忙脚乱，因为那时候只想了功能实现，却从来没考虑过上线，而且问题基本上都是出在上线过程中的。你想到了上次参加一个社区活动，其中的一个大牛提到了一个说法：“最后一公里”。

想到这，你赶紧上网搜了一下“最后一公里”，这个说法指的是完成一件事，在最后也是最关键的步骤。你才意识到，“最后一公里”这个说法已经被应用在很多领域了，负责人就是站在“最后一公里”的角度来看要发生的事情。

嗯，你学会了一招，以后你也可以站在“最后一公里”去发现问题了，加上你已经具备的推演能力，给出一个更令人满意的任务列表似乎更容易一些。

把这个问题想清楚了，你重新整理了自己的思路，列出了一个自己的问题解决计划。

先从结果的角度入手，看看最终上线要考虑哪些因素。

推演出一个可以一步一步执行的上线方案，用前面考虑到的因素作为衡量指标。

根据推演出来的上线方案，总结要做的任务。

不过，更令你兴奋的是，你拥有了一个看问题的新角度，让自己可以再上一个台阶，向着资深软件工程师的级别又迈进了一步。

通往结果之路

好了，这个小故事告一段落。作为我们专栏的用户，你可能已经知道了这个故事要表达的内容依旧是“以终为始”。关于“以终为始”，我们前面讲的内容一直是看到结果，结果是重要的。然而，通向结果的路径才是更重要的。

这个世界不乏有理想的人，大多数人都能看到一个宏大的未来，但这个世界上，真正取得与这些理想相配成绩的人却少之又少，大部分人都是泯然众生的。

宏大理想是一个目标，而走向目标是需要一步一个脚印地向前走的。唐僧的目标是求取真经，但他依然用了十几年时间才来到大雷音寺。唐僧西天取经有一个极大的优势，他达成目标的路径是清晰的，从长安出发，向着西天一路前行就好。

对比我们的工作，多数情况下，即便目标清晰，路径却是模糊的。所以，不同的人有不同的处理方式。有些人是走到哪算哪，然后再看；有些人则是先推演一下路径，看看能走到什么程度。

在我们做软件的过程中，这两种路径所带来的差异，已经在前面的小故事里体现出来了。一种是前期其乐融融，后期手忙脚乱；一种是前面思前想后，后面四平八稳。我个人是推崇后一种做法的。

或许你已经发现了，这就是我们在“以终为始”主题的开篇中，提到的第一次创造或者智力上的创造。如果不记得了，不妨回顾一下《02 | 以终为始：如何让你的努力不白费？》。

实际上，早就有人在熟练运用这种思想了。在军事上，人们将其称为沙盘推演，或沙盘模拟。军队通过沙盘模拟军事双方的对战过程，发现战略战术上存在的问题。这一思想也被商界借鉴过来，用来培训各级管理者。

这个思想并不难理解，我们可以很容易地将它运用在工作中的很多方面。比如：

在做一个产品之前，先来推演一下这个产品如何推广，通过什么途径推广给什么样的人；

在做技术改进之前，先来考虑一下上线是怎样一个过程，为可能出现的问题准备预案；

在设计一个产品特性之前，先来考虑数据由谁提供，完整的流程是什么样的。

最后这个例子也是软件开发中常遇到的，为数不少的产品经理在设计产品时，只考虑到用户界面是怎样交互的，全然不理会数据从何而来，造成的结果是：累死累活做出来的东西，完全跑不通，因为没有数据源。

很多时候，我们欠缺的只是在开始动手之前做一遍推演，所以，我们常常要靠自己的小聪明忙不迭地应对可能发生的一切。

希望通过今天的分享，能让你打破手忙脚乱的工作循环，让自己的工作变得更加从容。

总结时刻

即便已经确定了自己的工作目标，我们依然要在具体动手之前，把实施步骤推演一番，完成一次头脑中的创造，也就是第一次创造或智力上的创造。这种思想在军事上称之为沙盘推演，在很多领域都有广泛地应用。

在软件开发过程中，我们就假设软件已经就绪，看就绪之后，要做哪些事情，比如，如何上线、如何推广等等，这样的推演过程会帮我们发现前期准备的不足之处，进一步丰富我们的工作计划。为了不让我们总在“最后一公里”摔跟头，前期的推演是不可或缺的，也是想让团队进入有条不紊状态的前提。

如果今天的内容你只记住一件事，那请记住：在动手做一件事之前，先推演一番。

最后，我想请你思考一下，如果把你在做的事情推演一番，你会发现哪些可以改进的地方呢？欢迎在留言区写下你的想法。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

09 | 你的工作可以用数字衡量吗？

今天的分享从日常工作开始。请你回想一下，你每天到岗之后做的第一件事是什么呢？然后你来猜猜我的答案是什么？你可能猜不到，我每天到公司之后，第一件正事是看数字。

我现在服务于一家做数字资产的公司，我们提供的是一个 24 小时运行的服务。从加入这家公司的第一天开始，公司的人就给我不断灌输一个重要理念——看数字。在我座位的正前方，摆着一个巨大的显示器，上面展示着各种不断变换的曲线、柱状图和数字，这些数字反映的是各种系统运行的指标。

我们就是每天看着这些指标，来发掘一些线上系统问题的，一旦某些指标出现自己不能理解的异常，就要着手调查。

你或许会纳闷，我们不是在探讨“以终为始”吗？怎么变成了一个关于监控的讨论呢？别急，我们确实还在讨论“以终为始”，因为数字是诠释“终”的最好方式。

我们前面讨论了各种“终”，但通常靠语言定义的“终”，或多或少都会存在一些模糊的地方，也就是容易产生误解的地方。而数字却是一个明明白白的“终”。比如，测试覆盖率要求 100%，即便你做到了 99.9%，不达标就是不达标，没什么好说的，说破天也是不达标。

再比如，之前内容我们讲到精益创业时，提到了一个重要的反馈循环：开发（build）- 测量（measure）- 认知（learn）。你会发现，在这个循环中，开发（build）是可控的，认知（learn）必须是得到反馈之后才能有的。所以，这里面最需要我们回答的问题是测量（measure）。而这里的测量，最好的检验标准当然就是数字。

或许你会说，数字我们都很熟，还用讲吗？不过在我看来，你还真的未必习惯于使用数字。

熟悉而陌生的数字

从进化的角度来看，人们做事更多是依赖于直觉的。数字，是人类在非洲大草原上奔跑了许久之后才创造出来的东西。著名科普著作《从一到无穷大》的开篇有这么一个故事：

两个匈牙利贵族决定做一次数数的游戏，看谁说出的数字大。
一个贵族说：“好，那你先说吧！”
另一个绞尽脑汁想了好几分钟，说了一个数字：“3”。
现在轮到第一个贵族苦思冥想了，他想了一刻钟，然后说：“好吧，你赢啦!”

这个故事听起来有些荒诞，但一些非洲探险家证实，在某些原始部族里，不存在比 3 大的数词。如果问他们有几个孩子，而这个数字大于 3 的话，他就会回答“许多个”。

虽然我们中华民族是一个重视教育的民族，现在也都承认数学是一门重要的基础知识。但我们还是习惯性地观其大略，因为在日常生活领域里，除了买东西发工资，需要对数字斤斤计较的场合并不多。

历史的车轮在不停地滚滚向前，当今社会所面临的复杂度已经远远超过凭直觉就能把事情做好的程度。

一些人说，自己靠直觉就能把事情做好，其实这是一种误解，因为那种所谓的直觉，通常是一种洞见（Insight），洞见很大程度上依赖于一个人在一个领域长期的沉淀和积累，而这其实是某种意义上的大数据。

我们都在说，人类马上就要进入智能时代了。之所以这么说，主要是现在人工智能技术不断地向前发展着。而人工智能作为一门在 50 年代就已经问世的技术，直到最近几年才得到大踏步的前进，主要归功于基础设施的发展。

在人工智能领域，基于统计的方法早就在学术界提了出来，但由于当时的技术条件所限，人们的数据采集和存储能力都有限，当时的“大”数据和今天的大数据完全不是一个量级的概念。

直到进入到互联网时代，随着处理数据量的增加，基础设施在不断地拓展，进而促使人们采集更多的数据，这个正向反馈也造就了今天的大数据。

原本因为缺乏足够数据支撑，难以施展拳脚的 AI 算法，在今天一下子有了足够的表演空间，从一个边缘角色成为了舞台中心的主角。

今天谈到人工智能，人们主要会谈三件事：算法、算力和数据。算法几乎是行业共有的，而算力在云计算普及的今天也不再是稀缺资源，所以，数据几乎成了兵家必争之“物”。于是，我们看到的现象是各个公司都在努力地搜集各种数据，让数据成为自己的竞争力。所以，在大方向上，数据采集是一个行业共识。

但是，作为这个世界上最了解数据价值的一批人，我们程序员只是在努力地把数据用于不断改善别人的生活，而对于自己日常工作的改善，则思考得少之又少。

我们更习惯的讨论方式依然是靠直觉。比如：增加了这个特性可能会让用户增长，做了这个调整应该会让系统的压力变小。

在一些简单的情形下，或者说大家信息对称、知识背景相差无几的情况下，这样的讨论是很容易得到认同的。而当事情复杂到一定程度时，简单地靠感觉是很难让人相信的。

所以，在我们的工作中，经常会发生的一个现象是，一个人说，我觉得这个有作用，另一个人说，我觉得那个没有。几个“觉得”下来，双方就开始进入了隔空对话的环节，谁也无法说服谁。

如果换成用数字的方式进行讨论，效果就会更好。有一次，为了改善用户体验，我们准备进行一次主页改版。产品团队希望在主页上加上大量的内容，而开发团队则认为太多的内容会导致主页加载变慢，进而造成用户体验下降。

正当这个对话即将进入“空对空”的讨论之时，我们找到了一个测量指标：主页加载速度。只要保证主页加载速度，产品团队就可以按照自己的理解来做调整。于是，一个即将不可挽回的讨论，变成了在一定约束条件下的讨论，双方谁也不再思维发散，讨论就能继续推进了。

如果你认同了数据本身的价值，那么再结合“以终为始”的理念，我们就应该在着手做一件事之前，先来想怎么去测量。无论是在讨论产品特性，还是功能开发，“信口雌黄”是容易的，落到数字上，人们就会多想一下，这是对彼此的约束。

从数字出发

前面的内容我们都是在说应该重视测量指标，重视数字。接下来，我就分享下几个我在实际工作中运用数字的案例，让你看看习惯用数字去思考问题之后，会拓宽哪些思考的维度。

首先是基于数字进行技术决策。有一次，我们打算做一个技术改进，给系统增加一些缓存，减轻数据库的压力。大家一起设计了两个技术方案。如果查询是特定的，我们就准备简单地在某些方法上加上缓存；如果查询是五花八门的，就准备用一个中间件，使用它的查询方案。

系统现在的情况到底是什么样的呢？我们发现并不能立刻回答这个问题。于是，大家决定在系统中增加一些统计指标，让数据给我们答案。然后根据数据反映出的情况，进行具体的决策。

其次是一个准备上线的案例。当时，我们是要做一个影响力比较大的系统升级，因为这是一个系统的关键模块，上下游系统都会受到影响。

谁也不能确定哪个模块会在上线过程中出问题。于是，设计了一个全新的数据面板，将相关的几个模块的核心指标都摆在上面。而我们要做的就是在上线的同时，观察这些指标的变化。

所幸的是，这次上线影响不大，几个指标一路平稳，而大家的信心就源自这些提前准备好的指标。

再次，看一个从数字中发现问题的例子。由于各种历史原因，我们的重点指标面板上，会有几个指标表示的是类似的东西。

比如，某个模块的处理能力，一个指标是站在这个模块内部度量的，而另一个指标则是由这个模块上下游系统度量的。在大多数情况下，它们的表现是一致的。结果有一天两者突然出现了很大的差异，内部度量表现依然良好，而外部度量则出现了很大的延迟。

于是，我们开始追问为什么。一路追寻下来，我们发现，是这个模块内部需要定期将内部状态持久化下来，而在那个时间段内，这个模块就会停止从上游读取数据。所以，在内部看一切正常，而外部看则延迟较大。随后，我们找到了方案，解决了这一问题。

最后再说一个行业中的例子，据我所知，行业里的某些公司已经开始做所谓的 AIOps，也就是通过人工智能的方式，从数据中，发现更多运维的问题。无论哪种做法，都是为了从数字中发现问题，让系统更稳定。

我的一个同事有个观点非常值得玩味，他说，从数字上看，好的系统应该是“死水一潭”。

我是赞同这个观点的，因为出现波动尤其是大幅度波动，又不能给出一个合理解释的话，就说明系统存在着隐患。而让系统稳定，正是我们工作的一个重要组成部分。

回到这一讲的开头，我说每天工作中的一个重要组成部分就是看数字，其实就是在尝试着从数字的趋势中发现问题，如今团队已经习惯了“给个数字看看”这样的沟通方式，内部扯皮的机会也相应地减少了一些。

总结时刻

随着智能时代的来临，人类社会开始逐渐认识到数据的重要性。但我们这群 IT 人在通过数据为其他人服务的同时，却很少把数字化的思维带到自己的工作范围内。这也是工作中很多“空对空”对话的根源所在。

结合着“以终为始”的思考，如果我们可以在一开始，就设计好测量工作有效性的指标，那么就可以更有目的性地去工作了。

而如果我们习惯了用数字去思考，就可以在很多方面让数字帮助我们。我举了几个小例子，比如：基于数据进行技术决策、预先设定系统指标，以及发现系统中的问题等等。希望你也可以把数字思维带到你的日常工作中。

如果今天的内容你只记住一件事，那请记住：问一下自己，我的工作是不是可以用数字衡量。

最后，我想请你分享一下，你的工作中，有哪些应用数字解决问题的场景呢？欢迎在留言区写下你的想法。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

10 | 迭代0: 启动开发之前，你应该准备什么？

关于“以终为始”，我们已经从各个方面讲了很多。你或许会想，既然我们应该有“以终为始”的思维，那么在项目刚开始，就把该准备的东西准备好，项目进展是不是就能稍微顺畅一点儿呢？

是这样的，事实上这已经是一种常见的实践了。今天，我们就来谈谈在一开始就把项目准备好的实践：迭代 0。

为什么叫迭代 0 呢？在“敏捷”已经不是新鲜词汇的今天，软件团队对迭代的概念已经不陌生了，它就是一个完整的开发周期，各个团队在迭代上的差别主要是时间长度有所不同。

一般来说，第一个迭代周期就是迭代 1，然后是迭代 2、迭代 3，依次排列。从名字上你就不难发现，所谓迭代 0，就是在迭代 1 之前的一个迭代，所以，我们可以把它理解成开发的准备阶段。

既然迭代 0 是项目的准备阶段，我们就可以把需要提前准备好的各项内容，在这个阶段准备好。事先声明，这里给出的迭代 0，它的具体内容只是基本的清单。在了解了这些内容之后，你完全可以根据自己项目的实际情况，扩展或调整这个清单。

好，我们来看看我为你准备的迭代 0 清单都包含了哪些内容。

需求方面

1. 细化过的迭代 1 需求

一个项目最重要的是需求，而在迭代 0 里最重要的是，弄清楚第一步怎么走。当我们决定做一个项目时，需求往往是不愁的，哪些需求先做、哪些需求后做，这是我们必须做决策的。迭代 0 需要做的事，就是把悬在空中的内容落到地上。

在需求做好分解之后，我们就会有一大堆待开发的需求列表。注意，这个时候需求只是一个列表，还没有细化。因为你不太可能这个时候把所有的内容细化出来。如果你做过 Scrum 过程，你的 backlog 里放的就是这些东西。

然后，我们要根据优先级从中挑出迭代 1 要开发的需求，优先级是根据我们要完成的最小可行产品（minimum viable product，MVP）来确定的，这个最小可行产品又是由我们在这个迭代里要验证的内容决定的。一环扣一环，我们就得到了迭代 1 要做的需求列表。

确定好迭代 1 要做的需求之后，接下来就要把这些需求细化了，细化到可执行的程度。前面讲用户故事时，我们已经说过一个细化需求应该是什么样子的，这里的关键点就是要把验收标准定义清楚。

所以，我们要在迭代 0 里，根据优先级来确定迭代 1 要做的需求，然后进行细化。

2. 用户界面和用户交互

如果你的项目是一个有用户界面的产品，给出用户界面，自然也是要在迭代 0 完成的。另外，还有一个东西也应该在迭代 0 定义清楚，那就是用户交互。

我见过很多团队只给出用户界面，然后，让前端程序员或者 App 程序员根据界面去实现。程序员实现功能没问题，但定义交互并不是程序员这个角色的强项，它应该是需求的一部分。

如何让用户用着舒服，这是一门学问。我们在市面上看到很多难用的网站或 App，基本上都是程序员按照自己习惯设计出来的。

现如今，我们可以很容易地在市面上找到画原型的工具，某些工具用得好的话，甚至可以达到以假乱真的地步。如果能再进一步的话，甚至可以用一些模拟服务器的工具，把整个交互的界面都做出来。作为 Moco 这个模拟服务器的开发者，我很清楚，一个原型可以达到怎样的高度。

所以，一个有用户界面的项目需要在迭代 0 中给出用户界面和用户交互。

技术方面

1. 基本技术准备

技术方面，需要在项目一开始就准备好的事比较多。其中有一些是你很容易想到的，比如：在进入迭代 1 开始写代码之前，我们需要确定技术选型，确定基本的技术架构等等。也许你还能想到，数据库表结构也是这个阶段应该准备的。

确实，这些东西都应该是在一个项目初期准备的，也是大家容易想到的。接下来，我来补充一些大家可能会遗漏的。

持续集成

关于持续集成，通常的第一反应是搭建一个持续集成服务器。没错，但还不够。这里的重点其实是构建脚本。因为持续集成服务器运行的就是构建脚本。

那要把什么东西放在构建脚本里呢？最容易想到的是编译打包这样的过程。感谢现在的构建工具，它们一般还会默认地把测试也放到基本的构建过程中。

但仅有这些还是不够，我们还会考虑把更多的内容放进去，比如：构建 IDE 工程、代码风格检查、常见的 bug 模式检查、测试覆盖率等等。

持续集成还有一个很重要的方面，那就是持续集成的展示。为什么展示很重要？当你的持续集成失败时，你怎么发现呢？

一个简单的解决方案是：摆个大显示器，用一个 CI Monitor 软件，把持续集成的状态展示在上面。更有甚者，会用一个实体的灯，这样感官刺激更强一些。

在“以终为始”这个模块中，我们提到集成的部分时，只讲了要做持续集成，后面我们还会再次讲到持续集成，和你说说持续集成想做好，应该做成什么样子。

测试

测试是个很有趣的东西，程序员对它又爱又恨。一般来说，运行测试已经成为现在很多构建工具的默认选项，如果你采用的工具没有这个能力，建议你自己将它加入构建脚本。

让你为一个项目补测试，那是一件非常痛苦的事，如果在一开始就把测试作为规范加入进去的话，那么在边开发边写测试的情况下，相对来说，写测试痛苦度就低多了，团队成员也就容易遵守这样的开发规范。

把测试当作规范确定下来的办法就是把测试覆盖率加入构建脚本。

大多数团队提起测试，尤其是开发者测试，多半想到的都是单元测试和集成测试。把整个系统贯穿在一起的“端到端测试”却基本上交给其他人来做，也有不少团队是交给测试团队专门开发的特定程序来做。

在今天的软件开发中，有一些更适合描述这类测试的方法，比如 BDD，再比如 Specification by Example。你可以简单地把它们理解成一种描述系统行为的方式。还有一点做得好的地方是，有一些软件框架很好地支持了这种开发方法，比如 Cucumber。如果你有这种测试，不妨也将它加入构建脚本。

2. 发布准备

数据库迁移

如果你做的是服务器端开发，多半离不开与数据库打交道。只要是和数据库打交道，强烈建议你把数据库变更管理起来。

管理数据库变更的方式曾是很多团队面临的困扰。好在现在已经有了很多工具支持，比如，我最近喜欢的工具是 flyway，它可以把每一次数据库变更都当作一个文件。这样一来，我们就可以把数据库变更放到版本控制工具里面，方便进行管理。

管理变更有几种不同的做法，一种是每个变更是一个文件，一种是每一次发布是一个文件。各有各的好处，你可以根据需要，自行选择。

发布

技术团队擅长做功能开发，但上线部署或打包发布却是很多团队在前期最欠考量的内容，也是很多团队手忙脚乱的根源。

如果一开始就把部署或发布过程自动化，那么未来的生活就会轻松很多。如果你采用的是 Docker，就准备好第一个可以部署的 Dockerfile；如果是自己部署，就编写好 Shell 脚本。

其实你会发现，上面提到的所有内容即便不在迭代 0 做，在项目的各个阶段也会碰到。而且一般情况下，即便你在迭代 0 把这些工作做好了，后续依然要不断调整。但我依然建议你在迭代 0 把这些事做好，因为它会给你的项目定下一个基调，一个自动化的基调。

日常工作

最后，我们来看一下，如果在迭代 0 一切准备就绪，你在迭代 1 应该面对的日常工作是什么样的。

你从已经准备好的任务卡中选了一张，与产品经理确认了一些你不甚清楚的几个细节之后，准备实现它。你从代码仓库更新了最新的代码，然后，开始动手写代码。

这个任务要在数据库中添加一个字段，你打开开发工具，添加了一个数据库迁移文件，运行了一下数据库迁移工具，一切正常，新的字段已经出现在数据库中。

这个任务很简单，你很快实现完了代码，运行一下构建脚本，代码风格检查有个错误，你顺手修复了它。再运行，测试通过了，但测试覆盖率不够，你心里说，偷懒被发现了。不过，这是小事，补几个测试就好了。一切顺利！

你又更新了一下代码，有几个合并的问题。修复之后，再运行构建脚本，全过，提交代码。

你伸了一个懒腰，完成任务之后，你决定休息片刻。忽然，持续集成的大屏幕红了，你的提交搞砸了。你立刻看了一下代码，有一个新文件忘提交了，你吐了一下舌头赶紧把这个文件补上了。不一会儿，持续集成大屏幕又恢复了代表勃勃生机的绿色。

你休息好了，准备开始拿下一个任务。

这就是一个正常开发该有的样子，在迭代 0 时，将准备工作做好，后续你的一切工作就会变得井然有序，出现的简单问题会很快地被发现，所有人都在一种有条不紊的工作节奏中。

总结时刻

在这一讲中，我给你介绍了迭代 0 的概念，它是在正式开发迭代开始之前，进行一些基础准备的实践。我给了一份我自己的迭代 0 准备清单，这份清单包含了需求和技术两个大方面，你可以参照它设计你自己的迭代 0 清单。

根据我的经验，对比这个清单，大多数新项目都在一项或几项上准备得不够充分。即便你做的不是一个从头开始的项目，对照这个清单，也会发现项目在某些项上的欠缺，可以有针对性地做一些补充。

如果今天的内容你只记住一件事，那么请记住：设计你的迭代 0 清单，给自己的项目做体检。

最后，我想请你思考一下，如果让你来设计迭代 0 清单，它会包含哪些内容呢？欢迎在留言区写下你的想法。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

划重点 | 关于“以终为始”，你要记住的9句话

“以终为始”这个主题模块已经全部更新完毕，相信通过对各种实践的深入讲解，你已经对“以终为始”这个原则有了更为全面和透彻的理解。

为了帮助你更好地回顾和复习，我为每个主题模块增设了“划重点”的加餐内容。现在，我就带你一起梳理一下“以终为始”主题的核心要点。

重点复习

在这个模块中，我们学习到了一些行业最佳实践。

DoD，确定好完成的定义，减少团队内部的理解不一致。

用户故事，细化出有价值的需求。

持续集成，通过尽早集成，减少改动量，降低集成的难度。

精益创业，减少过度开发不确定性产品带来的浪费。

迭代 0，在项目开始之前，做好一些基础准备。

还学习到一些重要的思维转变。

任何事物都要经过两次创造：一次是在头脑中的创造，也就是智力上的或者第一次创造（Mental/First Creation），然后才是付诸实践，也就是实际的构建或第二次创造（Physical/Second Creation）。

在更大的上下文内发现自己的“终”。

通过推演，找到通往“终”的路径。

用可度量的“数字”定义自己的“终”。

实战指南

在每一篇文章的结尾，我们还将全篇内容浓缩为一句实战指南，希望你可以迅速上手，把“以终为始”的原则运用在实际工作之中，我们一起来回顾一下这些实战指南。

遇到事情，倒着想。
— 以终为始：如何让你的努力不白费？

在做任何事之前，先定义完成的标准。
— DoD 的价值：你完成了工作，为什么他们还不满意？

在做任何需求或任务之前，先定好验收标准。
— 接到需求任务，你要先做那件事？

尽早提交代码去集成。
— 持续集成：集成本身就是写代码的一个环节

默认所有需求都不做，直到弄清楚为什么要做这件事。
— 精益创业：产品经理不靠谱，你该怎么办？

扩大自己工作的上下文，别把自己局限在一个“程序员”的角色上。
— 解决了很多问题，为什么你依然在“坑”里？

在动手做一件事之前，先推演一番。
— 为什么说做事之前要先进行推演？

问一下自己，我的工作是不是可以用数字衡量。
— 你的工作可以用数字衡量吗？

设计你的迭代 0 清单，给自己的项目做体检。
— 启动开发之前，你应该准备什么？

额外收获

在这个部分的最后，针对大家在学习过程中的热门问题，我也进行了回答，希望你懂得：

作为程序员，你可以管理你的上级；

拿老板说事的产品经理，你可以到老板面前澄清；

喜欢无脑抄袭的产品经理，让他回去先想清楚到底抄的是什么；

分清楚需求和技术，产品经理和开发团队各自做好各自的事。
—答疑解惑 | 如何管理你的上司？

留言精选

同学们的留言很踊跃，也很有价值。精彩的留言本身就是对文章内容的补充与丰富，在此我挑出一些优秀的留言与你分享。

在讲高效工作的思考框架时，张维元 同学提到：

思考框架是道，原则是演化下的术，我们从 A → B，有无穷无尽的路径，最有效的唯有那条直线。本质上，各个维度、原则（不限于作者提到的四项原则）都是帮助我们更好地定位 A 在哪里，B 在哪里，那条直线在哪里。

对于以终为始的原则，WTF 同学提到：

“以终为始”，最常见的一个实践就是计划倒排了。先定时间，然后看功能是不是做不过来得砍掉一些，人力是不是不够需要补充一些，提前预知规避风险。

对于用户故事的验收标准，liu 同学提到：

程序员的核心职责是如何实现产品功能，怎么实现功能；前提是理解产品功能，需要实现哪些功能。有些项目经理，产品经理与程序员角色混淆。你同他谈功能，他同你谈技术实现，你同他谈技术，他同你谈产品（需要实现哪些功能）。

大家对沙盘推演的话题很感兴趣。其中，西西弗与卡夫卡 同学提到：

推演可以发现达成目标会涉及到哪些部门、哪些利益相关者，需要哪些资源，以及他们需要何时怎样的配合。

ZackZeng 同学也针对这个话题留言:

项目上线之前，一般都会有一个 launch plan, 数据库迁移这种项目，不去考虑上线回滚我认为是设计上的缺失。我们公司的 launch plan 一般是写成一步一步的 checklist, 在上线之前会做同伴审查。

Scott 同学也提到：

我觉得领导说先跑通再说和事前推演是不矛盾的，很多时候，我们需要一个 poc 来证明这个项目是可行的，这其实也是事前推演的一部分。上线要事无巨细的检查推演，和快速跑通 poc 不矛盾，当然现实世界是，大家就急着把 poc 当正式产品上线了，这是无数个悲剧故事的序章。

休息一下马上回来 同学对推演过程进行了很好地补充：

上线前，哪些机器什么配置，应该有一个预期，甚至提前准备好。

adang 同学也分享了他在工作中的感悟：

想清楚了才能写清楚，这是我在编程工作非常认可的一句话，并且我也认为它是区分合格与不合格开发工程师的重要区别。软件开发过程中，最常见的例子就是拿到需求后不管三七二十一，上来就开始撸代码，但最后往往返工不断，质量问题层出不穷，而且加班没完没了，这里面一个根本原因就是没有系统地想清楚，但很多人都觉得前期澄清需求、分析设计是浪费时间，只有编码才是真正的创造价值，这就是差距。

在讲到工作要尽量用数字衡量时，西西弗与卡夫卡 同学提到：

比如开发常常关注的是产品经理提的功能有没有实现，实际上也应该了解做出来的有多少人使用，每个页面有多少人使用。此外，看开发是否努力勤奋，不要光听他说，而是要看看他提交 git 有多频繁、提交的时间段、代码量有多少。代码质量可以用 bug 数 / 代码量来衡量。当然，这些量化未必科学，甚至会被误用，但总胜过凭印象拍脑袋的判断。

大彬 同学也提到：

上周我把一个方案进行推迟了，让同事去搜集某项指标的数据，没数据，一切方案都是空谈。AB 测试，留言量，阅读量，转发量一切数据都是下一步决策和改进的基础。

篇幅限制，就为大家分享这么多，感谢同学们的精彩留言。留言区还有很多同学提出了各种问题，其实都可以用任务分解的方式去解决。不着急，我们下一个主题的内容就是“任务分解”。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

答疑解惑 | 如何管理你的上级？

在这个模块里，我围绕着“以终为始”这个原则为你进行了详细地讲解，还给你介绍了应用“以终为始”原则的一些行业最佳实践。

同学们的留言特别踊跃，很多同学表示有收获的同时，也提出了大量的问题，大家比较关心怎样将这些实践在自己的实际工作中落地，部分问题我已经在留言中回复了。在今天的答疑环节中，我挑选了一些非常典型的问题来更详细地回答一下。

问题 1：领导要求的，无力反驳怎么办？

achenbj 同学提到

讲得很好，感觉落地还需努力。我们就是领导给了功能，跟你说下要做啥，就那么做就行。没有了。
—— 04 | 接到需求任务，你要先做哪件事？

Alexdown 同学提到

考虑到地位的不对等以及我的“人设”已经定型了，实施起来有点难度。
—— 02 | 以终为始：如何让你的努力不白费？

这类问题很经典，很多同学留言提到，我就不一一列举了。

在我的职业生涯中，无数次听到不同的人有过同样的抱怨。我最初也觉得这是一个无解的问题，直到后来我读到了一本书。

管理大师彼得·德鲁克有一本经典著作《卓有成效的管理者》，虽然标题上带着管理者几个字，但在我看来，这是一本告诉我们如何工作的书，每个人都可以读一下。

当年我读这本书时，其中的一个观点让我很受震撼：如何管理上级。

什么？上级也可以管理？这对于我们这些习惯了接受上级指挥的人来说，观念上的转变几乎是天翻地覆一般。

在很多人看来，自己累死累活，只是因为自己的笨蛋上级，没有很好地处理好他该处理好的事情，还把“锅”扣到了自己的头上。

不过，在德鲁克看来，上级也是人，一样有着长处和短处。我们应该发挥其长处，减少其短处带来的不良影响。管理上级，也就是要发挥上级的长处，不能唯命是从，应该从正确的事情入手，以上级能够接受的方式向其提出建议。

具体到我们的日常工作中该怎么管理上级呢？我给你一些小建议。

我们要敢于管理上级，对上级不合理的要求说“不”，这是一个思想上的转变。很多人受到传统官本位思想的影响，对上级的服从达到了不健康的程度。勇于改变，是有效管理上级的前提条件。如果不从思想上转变，我接下来的建议都是没有价值的。

那具体要从哪些方面着手呢？

第一，管理上级的预期。

上级问你：“一个产品特性，你多长时间能做完？两天？一天行不行？”你想了想，如果不写测试，确实能够省下不少时间，于是，你决定答应上级的要求。是的，大部分人就是这么妥协的。

妥协很容易，但再往回扳就不容易了。下次，他还会再进一步压缩：“半天能不能搞定？两小时行不行？”人的欲望是无限的，所以，就不要让上级有错误的预期。

如果是我，我会告诉上级，这个压缩会影响到什么。比如，要想做这个调整，你需要放弃的内容是什么；或者，我可以给出一个快速上线的临时方案，但接下来的几天，我需要调整，让代码回到一个正常的状态中。所以，你就不要给我安排新工作了。

这个过程，相当于我把自己看到的问题暴露给上级，让他选择。他有更多的上下文，他会平衡该做的事情。

第二，帮助上级丰富知识。

不是每个上级都是经验丰富的，知道所有事情。比如，有些成长得比较快的负责人，自己甚至都还没来得及了解软件开发全生命周期。在 IT 这个快速发展的行业里，这是非常可能出现的情况。所以，在某些局部，你比他了解得多是非常有可能的。

在那些他做得不够好的领域，他肯定有许多烦恼。比如，盲目给需求的产品经理，可能也会影响到他对需求的判断。

这个时候，你就不妨把自己知道的内容找个机会给他讲讲。一个简单的方式是，把我专栏的内容发给他，和他一起探讨怎么做是合理的。然后，大家一起协同，改进工作方式。因为你是在帮他解决问题，他会更愿意接受。

第三，说出你的想法。

如果你什么都不做，上级会按照他自己的理解安排工作。比如，小李擅长处理消息队列，那消息队列的活都给他。

如果你有自己的想法和打算，不妨提出来，主动承担一些职责。比如，你接下来打算多学点消息队列，那就大大方方地告诉上级，下次有相关的活，考虑一下自己，上级再安排工作的时候，他就会多想想。这其实就是我们熟悉的一个最简单的道理：会哭的孩子有奶吃。

如果经过你的种种努力，发现你的上级真的是完全没法影响，只能以令人无语的方式行事，那你需要仔细考虑一下与他合作的前景了。

不过，更可能出现的场景是，你还没去尝试改变就放弃了，将全部责任都归结于上级的问题。如果你是这种思考问题的逻辑，不论到哪个公司，结果都不会比现在更好。

问题 2：产品经理总拿老板说事，怎么办？

此方彼方 Francis 同学提到

很多时候产品要做这需求的理由就一个：老板要的！
——01 | 10x 程序员是如何思考的？

西西弗与卡夫卡 同学提到

有的产品经理会使出必杀技——这是老板的需求
——06 | 精益创业：产品经理不靠谱，你该怎么办？

用老板来“甩锅”，这在软件行业中特别常见。

实际上，老板要求的是方向，不是产品特性。大老板不会安排那么细的细节。所以，一个产品经理该做的事就是把老板给的方向，变成一个个可以实现的产品特性，他要分析其中的合理与不合理。

不合理的部分应该是他和老板去沟通的，而不是让开发团队来实现。

在真实世界中，更有可能的情形是，产品经理“拿着鸡毛当令箭”，老板说的是试一下，到他这里就变成了必须完成。他不敢对老板提问，就只能压迫下游了。

这种情况，你就不妨和产品经理一起去见老板。我们在《解决了很多技术问题，为什么你依然在”坑“里？》这篇文章中提到，要扩大自己工作的上下文，这种做法也可以帮助你解决问题，在自己上下文中解决不了的问题，就放到更大的上下文中去解决。

问题 3：别人能做的，我们也要做

Xunqf 同学提到

当你和产品经理理论的时候，他往往会拿出来一个现有的产品给你看:“人家怎么就能做到，人家能做到说明技术上是可行的，做吧。”时间久了你会发现他的需求全是抄的的别的 APP，然后就觉得别人能做到的我们也一定能做。
——《06 | 精益创业：产品经理不靠谱，你该怎么办？》

你会发现，在这个问题里，提到了两个与产品经理交流可能出现的典型问题：一个是竞争对手有的产品，我们也要有；另一个是人家能做到的，说明技术上可行，我们也能开发。

我带你来分别看下，这两种说法你该如何应对。

第一，竞争对手有的产品，我们也要有。

没有哪个企业是靠纯粹抄袭成功的。我知道，你想说腾讯。腾讯当年做 QQ，从形式上看，是和 ICQ 极其相似的，甚至名字都是极为相似的：OICQ。但腾讯却做了自己的微创新，它将信息保存到了服务器端，而 ICQ 是保存在客户端的。

正是有了这样看似微小的创新，让当时大部分家里没有电脑的普通用户，可以在网吧里不同的电脑上继续自己的网络社交，适应了时代发展的需要。腾讯“抄”得好的东西，都是有自己微创新的，包括如今的微信。

“抄”不是问题，问题是无脑地抄。

所以，如果你的产品经理只想无脑抄袭，本质上，他就是在偷懒，没干好他该干的活。竞争对手有这个特性，他为什么要做？他做这个特性与它其他特性是怎么配合的？我们做这个特性，在我们的产品里怎样发挥价值？作为产品经理，你必须给我讲清楚这些。

即便我们最终的结果是，做的与竞争对手一模一样，经过思考之后的“抄袭”也是一件价值更大的事。

第二：人家能做到，说明技术上是可行的。

关于这一点，我不得不说，产品经理说得对。别人能做到，说明技术上肯定是可行的。

不过，我们必须分清楚两件事：需求和技术。

要做什么是需求，怎么做是技术。与产品经理要确认的是，这个需求是不是合理，该不该做。技术上能否实现，这是开发团队要考虑的事情，并不是产品经理说事的理由。

还有一种情况是，需求确实合理，但技术实现的成本极高，所需花费的时间很长。在这种情况下，你和产品经理之间很难互相说服。

解决方案是，将问题升级，放到更大的上下文中，让上一层的领导来决定，此时此刻，在现有的资源约束下，是否要按照这种方式做。同时，你最好再提供一个可选的替换方案，这样领导才能更好做选择。

还有一些同学问了很好的问题。比如，程序员充当了太多角色，很困惑。这个问题我在专栏中已经回答了，在《 接到需求任务，你要先做哪件事？》中，我们说，每次扮演好一个角色。在《 解决了很多技术问题，为什么你依然在“坑”里？》中，我们提到程序员应该多了解不同的角色。

还有人问，计划赶不上变化快，怎么办？简单回答就是靠任务分解，因为这个话题涉及到“任务分解”这个模块的内容，等这个主题讲完之后，如果大家还有疑惑，我们再来详细讨论。

好，今天的答疑就到这里，请你回想一下，你在你工作中是否也遇到过类似的问题呢？你又是怎么解决的呢？欢迎在留言区写下你的想法。我会从中筛选出典型的问题，与大家进行互动交流。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

11 | 向埃隆·马斯克学习任务分解

这次我们从一个宏大的话题开始：银河系中存在多少与我们相近的文明。我想，即便这个专栏的读者主力是程序员这个平均智商极高的群体，在面对这样一个问题时，大多数人也不知道从何入手。

我来做一个科普，给大家介绍一下德雷克公式，这是美国天文学家法兰克·德雷克（Frank Drake）于 1960 年代提出的一个公式，用来推测“可能与我们接触的银河系内外星球高等文明的数量”。

下面，我要放出德雷克公式了，看不懂一点都不重要，反正我也不打算讲解其中的细节，我们一起来感受一下。

不知道你看了德雷克公式做何感想，但对于科学家们来说，德雷克公式最大的作用在于：它将一个原本毫无头绪的问题分解了，分成若干个可以尝试回答的问题。

随着观测手段的进步，我们对宇宙的了解越来越多，公式中大多数数值，都可以得到一个可以估算的答案。有了这些因子，人们就可以估算出银河系内可以与我们通信的文明数量。

虽然不同的估算结果会造成很大的差异，而且我们迄今为止也没能找到一个可以联系的外星文明，但这个公式给了我们一个方向，一个尝试解决问题的手段。

好吧，我并不打算将这个专栏变成一个科普专栏，之所以在这讲解德雷克公式，因为它体现了一个重要的思想：任务分解。

通过任务分解，一个原本复杂的问题，甚至看起来没有头绪的问题，逐渐有了一个通向答案的方向。而“任务分解”就是我们专栏第二模块的主题。

马斯克的任务分解

如果大家对德雷克公式有些陌生，我们再来看一个 IT 人怎样用任务分解的思路解决问题。

我们都知道埃隆·马斯克（Elon Musk），他既是电动汽车公司特斯拉（Tesla）的创始人，同时还创建了太空探索公司 SpaceX。SpaceX 有一个目标是，送 100 万人上火星。

美国政府曾经算过一笔账，把一个人送上火星，以现有技术是可实现的，需要花多少钱呢？答案是 100 亿美金。如果照此计算，实现马斯克的目标，送 100 万人上火星就要 1 万万亿。这是什么概念呢？这笔钱相当于美国 500 年的 GDP，实在太贵了，贵到连美国政府都无法负担。

马斯克怎么解决这个问题呢？他的目标变了，他准备把人均费用降到 50 万美元，也就是一个想移民的人，把地球房子卖了能够凑出的钱。原来需要 100 亿美金，现在要降到 50 万美金，需要降低 2 万倍。

当然，降低 2 万倍依然是一个听起来很遥远的目标。所以，我们关注的重点来了：马斯克的第二步是，把 2 万分解成 20×10×100。这是一道简单的数学题，也是马斯克三个重点的努力方向。

先看“20”：现在的火星飞船一次只能承载 5 个人，马斯克的打算是，把火箭造大一点，一次坐 100 人，这样，就等于把成本降低 20 倍。如果你关注新闻的话，会发现 SpaceX 确实在进行这方面的尝试，

再来看“10”：马斯克认为自己是私营公司，效率高，成本可以降到十分之一。他们也正在向这个方向努力，SpaceX 的成本目前已经降到了同行的五分之一。

最后的“100”是什么呢？就是回收可重复使用的火箭。如果这个目标能实现，发射火箭的成本就只是燃料成本了。这也就是我们频频看到的 SpaceX 试飞火箭新闻的原因。

这么算下来，你是不是觉得，马斯克的目标不像最开始听到的那样不靠谱了呢？正是通过将宏大目标进行任务分解，马斯克才能将一个看似不着边际的目标向前推进。

软件开发的任务分解

好了，和大家分享这两个例子只是为了热热身，说明人类解决问题的方案是差不多的。当一个复杂问题摆在面前时，我们解决问题的一个主要思路是分而治之。

一个大问题，我们都很难给出答案，但回答小问题却是我们擅长的。所以，当我们学会将问题分解，就相当于朝着问题的解决迈进了一大步。

我们最熟悉的分而治之的例子，应该是将这个理念用在算法上，比如归并排序。将待排序的元素分成大小基本相同的两个子集，然后，分别将两个子集排序，最后将两个排好序的子集合并到一起。

一说到技术，大家就觉得踏实了许多，原来无论是外星人搜寻，还是大名鼎鼎的马斯克太空探索计划，解决问题时用到的思路都是大同小异啊！确实是这样。

那么，用这种思路解决问题的难点是什么呢？给出一个可执行的分解。

在前面两个例子里面，最初听到要解决的问题时，估计你和我一样，是一脸懵的。但一旦知道了分解的结果，立即会有一种“柳暗花明又一村”的感觉。你会想，我要是想到了这个答案，我也能做一个 SpaceX 出来。

但说到归并排序的时候，你的心里可能会有一丝不屑，这是一个学生级别的问题，甚至不值得你为此费脑子思考。因为归并排序你已经知道了答案，所以，你会下意识地低估它。

任务分解就是这样一个有趣的思想，一旦分解的结果出来，到了可执行的步骤，接下来的工作，即便不是一马平川，也是比原来顺畅很多，因为问题的规模小了。

在日常工作中，我们会遇到很多问题，既不像前两个问题那样宏大，也不像归并排序那样小，但很多时候，我们却忘记了将任务分解这个理念运用其中，给工作带来很多麻烦。

举一个例子，有一个关于程序员的经典段子：这个工作已经做完了 80%，剩下的 20% 还要用和前面的一样时间。

为什么我们的估算差别如此之大，很重要的一个原因就在于没有很好地分解任务，所以，我们并不知道要做的事情到底有多少。

前面我们在“为什么说做事之前要先进行推演？”文章中，讲到沙盘推演，这也是一个很好的例子，推演的过程就是一个任务分解的过程。上手就做，多半的结果都是丢三落四。你会发现，真正把工作完全做好，你落掉的工作也都要做，无论早晚。

与很多实践相反，任务分解是一个知难行易的过程。知道怎么分解是困难的，一旦知道了，行动反而要相对来说容易一些。

在“任务分解”这个主题下，我还会给你介绍一些实践，让你知道，这些最佳实践的背后思想就是任务分解。如果你不了解这些实践，你也需要知道，在更多的场景下，先分解任务再去做事情是个好办法。

也许你会说，任务分解并不难于理解，我在解决问题的过程中也是先做任务分解的，但“依然过不好这一生。”这就要提到我前面所说难点中，很多人可能忽略的部分：可执行。

可执行对于每个人的含义是不同的，对于马斯克而言，他把 2 万分解成 20×10×100，剩下的事情对他来说就是可执行的，但如果你在 SpaceX 工作，你就必须回答每个部分究竟是怎样执行的。

同样，假设我们做一个 Web 页面，如果你是一个经验丰富的前端工程师，你甚至可能认为这个任务不需要分解，顶多就是再多一个获取网页资源的任务。

而我如果是一个新手，我就得把任务分解成：根据内容编写 HTML；根据页面原型编写页面样式；根据交互效果编写页面逻辑等几个步骤。

不同的可执行定义差别在于，你是否能清楚地知道这个问题该如何解决。

对于马斯克来说，他的解决方案可能是成立一个公司，找到这方面的专家帮助他实现。对你的日常工作来说，你要清楚具体每一步要做的事情，如果不能，说明任务还需要进一步分解。

比如，你要把一个信息存起来，假设你们用的是关系型数据库，对大多数人来说，这个任务分解就到了可执行的程度。但如果你的项目选用了一个新型的数据库，比如图数据库，你的任务分解里可能要包含学习这个数据库的模型，然后还要根据模型设计存储方案。

不过，在实际工作中，大多数人都高估了自己可执行粒度，低估任务分解的程度。换句话说，如果你没做过任务分解的练习，你分解出来的大部分任务，粒度都会偏大。

只有能把任务拆分得非常小，你才能对自己的执行能力有一个更清楚地认识，真正的高手都是有很强的分解能力。这个差别就相当于，同样观察一个物品，你用的是眼睛，而高手用的是显微镜。在你看来，高手全是微操作。关于这个话题，后面我们再来细聊。

一旦任务分解得很小，调整也会变得很容易。很多人都在说计划赶不上变化，而真正的原因就是计划的粒度太大，没法调整。

从当年的瀑布模型到今天的迭代模型，实际上，就是缩减一次交付的粒度。几周调整一次计划，也就不存在“计划赶不上变化”的情况了，因为我的计划也一直在变。

如今软件行业都在提倡拥抱变化，而任务分解是我们拥抱变化的前提。

总结时刻

我们从外星人探索和马斯克的火星探索入手，介绍了任务分解在人类社会诸多方面的应用，引出了分而治之这个人类面对复杂问题的基本解决方案。接着，我给你讲了这一思想在软件开发领域中的一个常见应用，分而治之的算法。

虽然我们很熟悉这一思想，但在日常工作中，我们却没有很好地应用它，这也使得大多数人的工作有很大改进空间。运用这一思想的难点在于，给出一个可执行的分解。

一方面，对复杂工作而言，给出一个分解是巨大的挑战；另一方面，面对日常工作，人们更容易忽略的是，分解的任务要可执行。每个人对可执行的理解不同，只要你清楚地知道接下来的工作该怎么做，任务分解就可以告一段落。

大多数人对于可执行的粒度认识是不足的，低估了任务分解的程度，做到好的分解你需要达到“微操作”的程度。有了分解得很小的任务，我们就可以很容易完成一个开发循环，也就让计划调整成为了可能。软件行业在倡导拥抱变化，而任务分解是拥抱变化的前提。

如果今天的内容你只记住一件事，那么请记住：动手做一个工作之前，请先对它进行任务分解。

最后，我想请你回想一下，你在实际工作中，有哪些依靠任务分解的方式解决的问题呢？欢迎在留言区写下你的想法。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

12 | 测试也是程序员的事吗？

在“任务分解”这个模块，我准备从一个让我真正深刻理解了任务分解的主题开始，这个主题就是“测试”。

这是一个让程序员又爱有恨的主题，爱测试，因为它能让项目的质量有保证；恨测试，因为测试不好写。而实际上，很多人之所以写不好测试，主要是因为他不懂任务分解。

在上一个模块，我们提到了一些最佳实践，但都是从“以终为始”这个角度进行讲解的。这次，我准备换个讲法，用五讲的篇幅，完整地讲一下“开发者测试”，让你和我一起，重新认识这个你可能忽视的主题。

准备好了吗？我们先从让很多人疑惑的话题开始：程序员该写测试吗？

谁要做测试？

你是一个程序员，你当然知道为什么要测试，因为是我们开发的软件，我们得尽可能地保证它是对的，毕竟最基本的职业素养是要有的。

但测试工作应该谁来做，这是一个很有趣的话题。很多人凭直觉想到的答案是，测试不就该是测试人员的事吗，这还用问？

测试人员应该做测试，这是没错的，但是测试只是测试人员的事吗？

事实上，作为程序员，你多半已经做了很多测试工作。比如，在提交代码之前，你肯定会把代码跑一遍，保证提交的基本功能是正确的，这就是最基本的测试。但通常，你并不把它当成测试，所以，你的直觉里面，测试是测试人员的事。

但我依然要强调，测试应该是程序员工作的一部分，为什么这么说呢？

我们不妨想想，测试人员能测的是什么？没错，他们只能站在系统外部做功能特性的测试。而一个软件是由它内部诸多模块组成的，测试人员只从外部保障正确性，所能达到的效果是有限的。

打个比方，你做一台机器，每个零部件都不保证正确性，却要让最后的结果正确，这实在是一个可笑的要求，但这却真实地发生在软件开发的过程中。

在软件开发中有一个重要的概念：软件变更成本，它会随着时间和开发阶段逐步增加。也就是说我们要尽可能早地发现问题，修正问题，这样所消耗掉的成本才是最低的。

上一个模块讲“以终为始”，就是在强调尽早发现问题。能从需求上解决的问题，就不要到开发阶段。同样，在开发阶段能解决的问题，就不要留到测试阶段。

你可以想一下，是你在代码中发现错误改代码容易，还是测试了报了 bug，你再定位找问题方便。

更理想的情况是，质量保证是贯穿在软件开发全过程中，从需求开始的每一个环节，都将“测试”纳入考量，每个角色交付自己的工作成果时，都多问一句，你怎么保证交付物的质量。

需求人员要确定验收标准，开发人员则要交出自己的开发者测试。这是一个来自于精益原则的重要思想：内建质量（Build Quality In）。

所以，对于每个程序员来说，只有在开发阶段把代码和测试都写好，才有资格说，自己交付的是高质量的代码。

自动化测试

不同于传统测试人员只通过手工的方式进行验证，程序员这个群体做测试有个天然的优势：会写代码，这个优势可以让我们把测试自动化。

早期测试代码，最简单的方式是另外写一个程序入口，我初入职场的时候，也曾经这么做过，毕竟这是一种符合直觉的做法。不过，既然程序员有写测试的需求，如此反复出现的东西，就会有更好的自动化方案。于是开始测试框架出现了。

最早的测试框架起源是 Smalltalk。这是一门早期的面向对象程序设计语言，它有很多拥趸，很多今天流行的编程概念就来自于 Smalltalk，测试框架便是其中之一。

真正让测试框架广泛流行起来，要归功于 Kent Beck 和 Erich Gamma。Kent Beck 是极限编程的创始人，在软件工程领域大名鼎鼎，而 Erich Gamma 则是著名的《设计模式》一书的作者，很多人熟悉的 Visual Studio Code 也有他的重大贡献。

有一次，二人一起从苏黎世飞往亚特兰大参加 OOPLSA（Object-Oriented Programming, Systems, Languages & Applications）大会，在航班上两个人结对编程写出了 JUnit。从这个名字你便不难看出，它的目标是打造一个单元测试框架。

顺便说一下，如果你知道 Kent Beck 是个狂热的 Smalltalk 粉丝，写过 SUnit 测试框架，就不难理解这两个人为什么能在一次航班上就完成这样的力作。

JUnit 之后，测试框架的概念逐渐开始流行起来。如今的“程序世界”，测试框架已经成为行业标配，每个程序设计语言都有自己的测试框架，甚至不止一种，一些语言甚至把它放到了标准库里，行业里也用 XUnit 统称这些测试框架。

这种测试框架最大的价值，是把自动化测试作为一种最佳实践引入到开发过程中，使得测试动作可以通过标准化的手段固定下来。

测试模型：蛋卷与金字塔

在前面的讨论里，我们把测试分为人工测试和自动化测试。即便我们只关注自动化测试，也可以按照不同的层次进行划分：将测试分成关注最小程序模块的单元测试、将多个模块组合在一起的集成测试，将整个系统组合在一起的系统测试。

有人喜欢把验收测试也放到这个分类里。为了简化讨论，我们暂时忽略验收测试。

随之而来的一个问题是，我们应该写多少不同层次的测试呢？理论上固然是越多越好了，但实际上，做任何事都是有成本的，所以，人们必须有所取舍。根据不同测试的配比，也就有了不同的测试模型。

有一种直觉的做法是，既然越高层的测试覆盖面越广，那就多写高层测试，比如系统测试。

当然，有些情景高层的测试不容易覆盖到的，所以，还要有一些底层的测试，比如单元测试。在这种情况下，底层的测试只是作为高层测试的补充，而主力就是高层测试。这样就会形成下面这样一种测试模型：冰淇淋蛋卷。

听说过冰淇淋蛋卷测试模型的人并不多，它是一种费时费力的模型，要准备高层测试实在是太麻烦了。

之所以要在这里提及它，是因为虽然这个概念很多人没听说过，但是有不少团队的测试实际采用的就是这样一种模型，这也是很多团队觉得测试很麻烦却不明就里的缘由。

接下来，要说说另一种测试模型，也是行业里的最佳实践：测试金字塔。

Mike Cohn 在自己的著作《Succeeding with Agile》提出了测试金字塔，但大多数人都是通过 Martin Fowler 的文章知道的这个概念。

从图中我们不难看出，它几乎是冰淇淋蛋卷的反转，测试金字塔的重点就是越底层的测试应该写得越多。

想要理解测试金字塔成为行业最佳实践的缘由，我们需要理解不同层次测试的差异。越是底层的测试，牵扯到相关内容越少，而高层测试则涉及面更广。

比如单元测试，它的关注点只有一个单元，而没有其它任何东西。所以，只要一个单元写好了，测试就是可以通过的；而集成测试则要把好几个单元组装到一起才能测试，测试通过的前提条件是，所有这些单元都写好了，这个周期就明显比单元测试要长；系统测试则要把整个系统的各个模块都连在一起，各种数据都准备好，才可能通过。

这个模块的主题是“任务分解”，我必须强调一点：小事反馈周期短，而大事反馈周期长。小事容易做好，而大事难度则大得多。所以，以这个标准来看，底层的测试才更容易写好。

另外，因为涉及到的模块过多，任何一个模块做了调整，都有可能破坏高层测试，所以，高层测试通常是相对比较脆弱的。

此外，在实际的工作中，有些高层测试会牵扯到外部系统，这样一来，复杂度又在不断地提升。

人们会本能地都会倾向于少做复杂的东西，所以，人们肯定不会倾向于多写高层测试，其结果必然是，高层测试的测试量不会太多，测试覆盖率无论如何都上不来。而且，一旦测试失败，因为牵扯的内容太多，定位起来也是非常麻烦的。

而反过来，将底层测试定义为测试主体，因为牵扯的内容少，更容易写，才有可能让团队得到更多的测试，而且一旦出现问题，也会更容易发现。

所以，虽然冰淇淋蛋卷更符合直觉，但测试金字塔才是行业的最佳实践。

当测试金字塔遇到持续集成

测试金字塔是一个重要实践的基础，它就是持续集成。当测试数量达到一定规模，测试运行的时间就会很长，我们可能无法在本地环境一次性运行所有测试。一般我们会选择在本地运行所有单元测试和集成测试，而把系统测试放在持续集成服务器上执行。

这个时候，底层测试的数量就成了关键，按照测试金字塔模型，底层测试数量会很多，测试可以覆盖主要的场景；而按照冰淇淋蛋卷模型，底层测试的数量则有限。

作为提交代码的防护网，测试数量多寡决定着得到反馈的早晚。所以，金字塔模型与持续集成天然就有着很好的配合。

需要特别注意的是，不是用单元测试框架写的测试就是单元测试。很多人用单元测试框架写的是集成测试或是系统测试。单元测试框架只是一个自动化测试的工具而已，并不是用来定义测试类型的。

在实际工作中，区分不同测试有很多种做法，比如，将不同的测试放到不同的目录下，或是给不同类型的测试一个统一的命名规范。

区分不同类型测试主要目的，主要是在不同的场景下，运行不同类型的测试。就像前面提到的做法是，在本地运行单元测试和集成测试，在持续集成服务器上运行系统测试。

总结时刻

测试是软件开发重要的组成部分，测试应该是软件开发团队中所有人的事，而不仅仅是测试人员的事。因为软件变更成本会随着时间和开发阶段逐步增加，能在早期解决的问题，就不要将它延后至下一个阶段。

在测试问题上，程序员有着天生的优势，会写代码，于是，程序员拥有了一个突出的强项，自动化测试。写测试应该是程序员工作完成的重要组成部分。

随着人们对于测试理解的加深，各种各样的测试都出现了，也开始有了测试的分类：单元测试、集成测试、系统测试等等。越在底层测试，成本越低，执行越快；越在高层测试，成本越高，执行越慢。

人的时间和精力是有限的，所以，人们开始思考不同的测试如何组合。在这个方面的最佳实践称之为测试金字塔，它强调的重点是，越底层的测试应该写得越多。只有按照测试金字塔的方式写测试，持续集成才能更好地发挥作用。

如果今天的内容你只能记住一件事，那请记住：多写单元测试。

最后，我想请你分享一下，你的团队在写测试上遇到哪些困难呢？欢迎在留言区写下你的想法。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

13 | 先写测试，就是测试驱动开发吗？

在上一讲中，我向你说明了为什么程序员应该写测试，今天我准备与你讨论一下程序员应该在什么阶段写测试。

或许你会说，写测试不就是先写代码，然后写测试吗？没错，这是一个符合直觉的答案。但是，这个行业里确实有人探索了一些不同的做法。接下来，我们就将进入不那么直觉的部分。

既然自动化测试是程序员应该做的事，那是不是可以做得更极致一些，在写代码之前就把测试先写好呢？

有人确实这么做了，于是，形成了一种先写测试，后写代码的实践，这个实践的名字是什么呢？它就是测试先行开发（Test First Development）。

我知道，当我问出这个问题的时候，一个名字已经在很多人的脑海里呼之欲出了，那就是测试驱动开发（Test Driven Development），也就是大名鼎鼎的 TDD，TDD 正是我们今天内容的重点。

在很多人看来，TDD 就是先写测试后写代码。在此我必须澄清一下，这个理解是错的。先写测试，后写代码的实践指的是测试先行开发，而非测试驱动开发。

下一个问题随之而来，测试驱动开发到底是什么呢？测试驱动开发和测试先行开发只差了一个词：驱动。只有理解了什么是驱动，才能理解了测试驱动开发。要理解驱动，先来看看这两种做法的差异。

测试驱动开发

学习 TDD 的第一步，是要记住 TDD 的节奏：“红 - 绿 - 重构”。

红，表示写了一个新的测试，测试还没有通过的状态；绿，表示写了功能代码，测试通过的状态；而重构，就是再完成基本功能之后，调整代码的过程。

这里说到的“红和绿”，源自单元测试框架，测试不过的时候展示为红色，通过则是绿色。这在单元测试框架形成之初便已经约定俗成，各个不同语言的后代也将它继承了下来。

我们前面说过，让单元测试框架流行起来的是 JUnit，他的作者之一是 Kent Beck。同样，也是 Kent Beck 将 TDD 从一个小众圈子带到了大众视野。

考虑到 Kent Beck 是单元测试框架和 TDD 共同的贡献者，你就不难理解为什么 TDD 的节奏叫“红 - 绿 - 重构”了。

测试先行开发和测试驱动开发在第一步和第二步是一样的，先写测试，然后写代码完成功能。二者的差别在于，测试驱动开发并没有就此打住，它还有一个更重要的环节：重构（refactoring）。

也就是说，在功能完成而且测试跑通之后，我们还会再次回到代码上，处理一下代码上写得不好的地方，或是新增代码与旧有代码的重复。因为我们第二步“绿”的关注点，只在于让测试通过。

测试先行开发和测试驱动开发的差异就在重构上。

很多人通过了测试就认为大功告成，其实，这是忽略了新增代码代码可能带来的“坏味道（Code Smell）”。

如果你真的理解重构，你就知道，它就是一个消除代码坏味道的过程。一旦你有了测试，你就可以大胆地重构了，因为任何修改错误，测试会替你捕获到。

在测试驱动开发中，重构与测试是相辅相成的：没有测试，你只能是提心吊胆地重构；没有重构，代码的混乱程度是逐步增加的，测试也会变得越来越不好写。

因为重构和测试的互相配合，它会驱动着你把代码写得越来越好。这是对“驱动”一词最粗浅的理解。

测试驱动设计

接下来，我们再来进一步理解“驱动”：由测试驱动代码的编写。

许多人抗拒测试有两个主要原因：第一，测试需要“额外”的工作量。这里我特意把额外加上引号，因为，你也许本能上认为，测试是额外的工作，但实际上，测试也应该是程序员工作的一部分，这在上一篇文章中我已经讲过。

第二，很多人会觉得代码太多不好测。之所以这些人认为代码不好测，其中暗含了一个假设：代码已经写好了，然后，再写测试来测它。

如果我们把思路反过来，我有一个测试，怎么写代码能通过它。一旦你先思考测试，设计思路就完全变了：我的代码怎么写才是能测试的，也就是说，我们要编写具有可测试性的代码。用这个角度，测试是不是就变得简单了呢？

这么说还是有些抽象，我们举个写代码中最常见的问题：static 方法。

很多人写代码的时候喜欢使用 static 方法，因为用着省事，随便在哪段代码里面，直接引用这个 static 方法就可以。可是，一旦当你写测试的时候，你就会发现一个问题，如果你的代码里直接调用一个 static 方法，这段代码几乎是没法测的。尤其是这个 static 方法里面有一些业务逻辑，根据不同业务场景返回各种值。为什么会这样？

我们想想，常见的测试手法应该是什么样的？如果我们在做的是单元测试，那测试的目标应该就是一个单元，在这个面向对象作为基础设施流行的时代，这个单元大多是一个类。测试一个类，尤其是一个业务类，一般会涉及到一些与之交互的类。

比如，常见的 REST 服务三层架构中，资源层要访问服务层，而在服务层要访问数据层。编写服务层代码时，因为要依赖数据层。所以，测试服务层通常的做法是，做一个假的数据层对象，这样即便数据层对象还没有编写，依然能够把服务层写完测好。

在之前的“蛮荒时代”，我们通常会写一个假的类，模拟被依赖那个类，因为它是假的，我们会让它返回固定的值，使用这样的类创建出来的对象，我们一般称之为 Stub 对象。

这种“造假”的方案之所以可行，一个关键点在于，这个假对象和原有对象应该有相同的接口，遵循同样的契约。从设计上讲，这叫符合 Liskov 替换法则。这不是我们今天讨论的重点，就不进一步展开了。

因为这种“造假”的方案实在很常见，所以，有人做了框架支持它，就是常用的 Mock 框架。使用 Mock 对象，我们可以模拟出被依赖对象的各种行为，返回不同的值，抛出异常等等。

它之所以没有用原来 Stub 这个名字，是因为这样的 Mock 对象往往有一个更强大的能力：验证这个 Mock 对象在方法调用过程中的使用情况，比如调用了几次。

我们回到 static 的讨论上，你会发现 Mock 对象的做法面对 static 时行不通了。因为它跳出了对象体系，static 方法是没法继承的，也就是说，没法用一系列面向对象的手法处理它。你没有办法使用 Mock 对象，也就不好设置对应的方法返回值。

要想让这个方法返回相应的值，你必须打开这个 static 方法，了解它的实现细节，精心地按照里面的路径，小心翼翼地设置对应的参数，才有可能让它给出一个你预期的结果。

更糟糕的是，因为这个方法是别人维护的，有一天他心血来潮修改了其中的实现，你小心翼翼设置的参数就崩溃了。而要重新进行设置的话，你只能把代码重读一遍。

如此一来，你的工作就退回到原始的状态。更重要的是，它并不是你应该关注的重点，这也不会增加你的 KPI。显然，你跑偏了。

讨论到这里你已经知道了 static 方法对测试而言，并不友好。所以，如果你要想让你的代码更可测，一个好的解决方案是尽量不写 static 方法。

这就是“从测试看待代码，而引起的代码设计转变”的一个典型例子。

关于 static 方法，我再补充几点。static 方法从本质上说，是一种全局方法，static 变量就是一种全局变量。我们都知道，全局方法也好，全局变量也罢，都是我们要在程序中努力消除的。一旦放任 static 的使用，就会出现和全局变量类似的效果，你的程序崩溃了，因为别人在另外的地方修改了代码，代码变得脆弱无比。

static 是一个方便但邪恶的东西。所以，要限制它的使用。除非你的 static 方法是不涉及任何状态而且行为简单，比如，判断字符串是否为空。否则，不要写 static 方法。你看出来了，这样的 static 方法更适合做库函数。所以，我们日常写应用时，能不用尽量不用。

前面关于 static 方法是否可以 Mock 的讨论有些绝对，市面上确实有某些框架是可以 Mock static 方法的，但我不建议使用这种特性，因为它不是一种普遍适用的解决方案，只是某些特定语言特定框架才有。

更重要的是，正如前面所说，它会在设计上将你引到一条不归路上。

如果你在自己的代码遇到第三方的 static 方法怎么办，很简单，将第三方代码包装一下，让你的业务代码面对的都是你自己的封装就好了。

以我对大多数人编程习惯的认知，上面这个说法是违反许多人编程直觉的，但如果你从代码是否可测的角度分析，你就会得到这样的结论。

先测试后写代码的方式，会让你看待代码的角度完全改变，甚至要调整你的设计，才能够更好地去测试。所以，很多懂 TDD 的人会把 TDD 解释为测试驱动设计（Test Driven Design）。

还有一个典型的场景，从测试考虑会改变的设计，那就是依赖注入（Dependency Injection）。

不过，因为 Spring 这类 DI 容器的流行，现在的代码大多都写成了符合依赖注入风格的代码。原始的做法是直接 new 一个对象，这是符合直觉的做法。但是，你也可以根据上面的思路，自己推演一下，从 new 一个对象到依赖注入的转变。

有了编写可测试代码的思路，即便你不做 TDD，依然对你改善软件设计有着至关重要的作用。所以，写代码之前，请先想想怎么测。

即便我做了调整，是不是所有的代码就都能测试了呢？不尽然。从我个人的经验上看，不能测试的代码往往是与第三方相关的代码，比如访问数据库的代码，或是访问第三方服务之类的。但不能测试的代码已经非常有限了。我们将它们隔离在一个小角落就好了。

至此，我们已经从理念上讲了怎样做好 TDD。有的人可能已经跃跃欲试了，但更多的人会用自己所谓的“经验”告诉你，TDD 并不是那么好做的。

怎么做好 TDD 呢？下一讲，我会给你继续讲解，而且，我们“任务分解大戏”这个时候才开始真正拉开大幕！

总结时刻

一些优秀的程序员不仅仅在写测试，还在探索写测试的实践。有人尝试着先写测试，于是，有了一种实践叫测试先行开发。还有人更进一步，一边写测试，一边调整代码，这叫做测试驱动开发，也就是 TDD。

从步骤上看，关键差别就在，TDD 在测试通过之后，要回到代码上，消除代码的坏味道。

测试驱动开发已经是行业中的优秀实践，学习测试驱动开发的第一步是，记住测试驱动开发的节奏：红——绿——重构。把测试放在前面，还带来了视角的转变，要编写可测的代码，为此，我们甚至需要调整设计，所以，有人也把 TDD 称为测试驱动设计。

如果今天的内容你只能记住一件事，那请记住：我们应该编写可测的代码。

最后，我想请你分享一下，你对测试驱动开发的理解是怎样的呢？学习过这篇内容之后，你又发现了哪些与你之前理解不尽相同的地方呢？欢迎在留言区写下你的想法。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

14 | 大师级程序员的工作秘笈

前面我和大家分享了 TDD 的来龙去脉，那些尚未将 TDD 烂熟于胸的同学会分为两个派别。一派是摩拳擦掌，准备动手实践一番；另一派是早就自我修炼过，但实践之路不通。所以，市面上经常会听到有人说，TDD 不实用。

但是 TDD 真的不实用吗？

和任何一门技能一样，TDD 也是需要练习的。更重要的是，你需要打通 TDD 的“任督二脉”，而这关键正是我们这个模块的主题：任务分解。而且，在今天的内容中，我还将带你领略大师级程序员的工作风范。让我们开始吧！

TDD 从何而来？

要学最原汁原味的 TDD ，莫过于从源头学起。

从前 TDD 只在小圈子里流行，真正让它在行业里广为人知的是 Kent Beck 那本知名的软件工程之作《解析极限编程》（Extreme Programming Explained）。这是一本重要的作品，它介绍了一种软件开发方法：极限编程。

当年他写作之时，许多人都在努力探寻瀑布开发方法之外的软件工程方法，除了极限编程，还有特征驱动开发、水晶开发方法等等，正是这些开发方法的探索，才有了后面敏捷方法的诞生。

极限编程对于行业最大的贡献在于，它引入了大量的实践，比如，前面提到过的持续集成、这里提到的 TDD，还有诸如结对编程、现场客户等等。

极限编程之所以叫“极限”，它背后的理念就是把好的实践推向极限。

前面提到持续集成时，我们已经介绍过这个理念，如果集成是好的，我们就尽早集成，推向极限每一次修改都集成，这就是持续集成。

如果开发者测试是好的，我们就尽早测试，推向极限就是先写测试，再根据测试调整代码，这就是测试驱动开发。

如果代码评审是好的，我们就多做评审，推向极限就是随时随地地代码评审，这就是结对编程。

如果客户交流是好的，我们就和客户多交流，推向极限就是客户与开发团队时时刻刻在一起，这就是现场客户。这种极限思维是一种很好的思考问题方式，推荐你也在工作中尝试使用一下。

虽然 TDD 只是《解析极限编程》介绍的诸多实践的一种，它却是与开发人员关系最为密切的一个实践。

随着 TDD 逐渐流行开来，人们对如何做 TDD 也越来越感兴趣，于是，Kent Beck 又专门为 TDD 写了一本书，叫《测试驱动开发》。

大师级程序员的秘笈

《测试驱动开发》这本书很有意思。如果你只是为了了解 TDD，这本书可能很无聊。Kent Beck 在第一部分只是在写一个功能，写完一段又写一段。

这本书我看过两遍，第一遍觉得平淡无奇，这种代码我也能写。第二遍看懂他的思路时，我几乎是震惊的感觉，因为它完全是在展示 Kent Beck 的工作方式。这也是我把 TDD 放到这个部分来讲的重要原因，Kent Beck 在做的就是任务分解。任务分解，也是这本书的真正价值所在。

当时，我已经工作了很多年，自以为自己在写代码上已经很专业了。看懂 Kent Beck 的思路，我才知道，与他相比，我还不够专业。

Kent Beck 是怎么做的呢？每当遇到一件要做的事，Kent Beck 总会先把它分解成几个小任务，记在一个清单上，然后，才是动手写测试、写代码、重构这样一个小循环。等一个循环完成了，他会划掉已经做完的任务，开始下一个。

一旦在解决问题的过程中遇到任何新的问题，他会把这个要解决的问题记录在清单上，保证问题不会丢失，然后，继续回到自己正在处理的任务上。当他把一个个任务完成的时候，问题就解决完了。

你或许会纳闷，这有什么特别的吗？你不妨回答这样一个问题，你多长时间能够提交一次代码？如果你的答案超过半天，对不起，你的做法步子一定是太大了。你之所以不能小步提交，一定是牵扯了太多相关的部分。

Kent Beck 的做法清晰而有节奏，每个任务完成之后，代码都是可以提交的。看上去很简单，但这是大多数程序员做不到的。

只有把任务分解到很小，才有可能做到小步提交。你能把任务分解到很小，其实是证明你已经想清楚了。而大多数程序员之所以开发效率低，很多时候是没想清楚就动手了。

我在 ThoughtWorks 工作时，每个人都会有个 Sponsor，类似于工厂里师傅带徒弟的关系。我当时的 Sponsor 是 ThoughtWorks 现任的 CEO 郭晓，他也是写代码出身的。有一次，他给我讲了他和 Wiki 的发明者 Ward Cunningham 一起结对编程的场景。

Ward 每天拿到一个需求，他并不急于写代码，而是和郭晓一起做任务分解，分解到每个任务都很清晰了，才开始动手做。接下来就简单了，一个任务一个任务完成就好了。

当时，郭晓虽然觉得工作节奏很紧张，但思路则是非常清晰的。有时，他也很奇怪，因为在开始工作之前，他会觉得那个问题非常难以解决。结果一路分解下来，每一步都是清晰的，也没遇到什么困难就完成了。

之所以这里要和你讲 Ward Cunningham 的故事，因为他就是当年和 Kent Beck 在同一个小圈子里一起探讨进步的人，所以，在解决问题的思路上，二人如出一辙。

为什么任务分解对于 TDD 如此重要呢？因为只有当任务拆解得足够小了，你才能知道怎么写测试。

很多人看了一些 TDD 的练习觉得很简单，但自己动起手来却不知道如何下手。中间就是缺了任务分解的环节。

任务分解是个好习惯，但想要掌握好它，大量的练习是必须的。我自己也着实花不少时间进行练习，每接到一个任务，我都会先做任务分解，想着怎么把它拆成一步一步可以完成的小任务，之后再动手解决。

微操作

随着我在任务分解上练习的增多，我越发理解任务分解的关键在于：小。

小到什么程度呢？有时甚至可以小到你可能认为这件事不值得成为一件独立的事。比如升级一个依赖的版本，做一次变量改名。

这样做的好处是什么呢？它保证了我可以随时停下来。

我曾在一本书里读到过关于著名高尔夫球手“老虎”伍兹的故事。高尔夫球手在打球的时候，可能会受到一些外界干扰。一般情况下还好，如果他已经开始挥杆，这时候受到了干扰，一般选手肯定是继续把杆挥下去，但通常的结果是打得不理想。

而伍兹遇到这种情况，他会停下来，重新做挥杆的动作，保证了每一杆动作的标准。

伍兹能停下来，固然是经过了大量的练习，但还有一个关键在于，对于别人而言，挥杆击球是一个动作，必须一气呵成。而对伍兹来说，这个动作是由若干小动作组成的，他只不过是刚好完成了某个小动作，而没有做下一个小动作而已。

换句话说，大家同样都是完成一个原子操作，只不过，伍兹的原子操作比其他人的原子操作小得多。

同样，我们写程序的时候，都不喜欢被打扰，因为一旦被打扰，接续上状态需要很长一段时间，毕竟，我们可不像操作系统那么容易进行上下文切换。

但如果任务足够小，完成一个任务，我们选择可以进入到下一个任务，也可以停下来。这样，即便被打扰，我们也可以很快收尾一个任务，不致于被影响太多。

其实，这种极其微小的原子操作在其他一些领域也有着自己的应用。有一种实践叫微习惯，以常见的健身为例，很多人难以坚持，主要是人们一想到健身，就会想到汗如雨下的健身场景，想想就放弃了。

但如果你一次只做一个俯卧撑呢？对大多数人来说，这就不是很难的一件事，那就先做一个。做完了一个如果你还想做，就接着做，不想做就不做了。

一个俯卧撑？你会说这也叫健身，一个俯卧撑确实是一个很小的动作，重要的是，一个俯卧撑是你可以坚持完成的，如果每天做 10 个，恐怕这都是大多数人做不到的。我们知道，养成一个习惯，最难的是坚持。如果你有了一个微习惯，坚持就不难了。

我曾经在 github 上连续提交代码 1000 天，这是什么概念？差不多三年的时间里，每天我都能够坚持写代码，提交代码，这还不算工作上写的代码。

对于大多数人来说，这是不可思议的。但我坚持做到了，不是因为我有多了不起，而是我养成了自己的微习惯。

这个连续提交的基础，就是我自己在练习任务分解时，不断地尝试把一件事拆细，这样，我每天都至少能保证完成一小步。当然，如果有时间了，我也会多写一点。正是通过这样的方法，我坚持了 1000 天，也熟练掌握了任务分解的技巧。

一个经过分解后的任务，需要关注的内容是有限的，我们就可以针对着这个任务，把方方面面的细节想得更加清晰。很多人写代码之所以漏洞百出，一个重要的原因就是因为任务粒度太大。

我们作为一个普通人，能考虑问题的规模是有限的，也就很难方方面面都考虑仔细。

微操作与分支模型

经过这种练习之后，任务分解也就成了我的本能，不再局限于写程序上。我遇到任何需要解决的问题，脑子里的第一反应一定是，它可以怎么一步一步地完成，确定好分解之后，解决问题就是一步一步做了。

如果不能很好地分解，那说明我还没想清楚，还需要更多信息，或者需要找到更好的解决方案。

一旦你懂得了把任务分解的重要性，甚至通过训练能达到微操作的水准，你就很容易理解一些因为步子太大带来的问题。举一个在开发中常见的问题，代码开发的分支策略。

关于分支策略，行业里有很多不同的做法。有的团队是大家都在一个分支上写代码，有的是每个人拉出一个分支，写完了代码再合并回去。你有没有想过为什么会出现这种差异呢？

行业中的最佳实践是，基于主分支的模型。大家都在同一个分支上进行开发，毕竟拉分支是一个麻烦事，虽然 git 的出现极大地降低了拉分支的成本。

但为什么还有人要拉出一个分支进行开发呢？多半的原因是他写的代码太多了，改动量太大，很难很快地合到开发的主分支上来。

那下一个问题就来了，为什么他会写那么多代码，没错，答案就是步子太大了。

如果你懂得任务分解，每一个分解出来的任务要改动的代码都不会太多，影响都在一个可控的范围内，代码都可以很快地合并到开发的主分支上，也就没有必要拉分支了。

在我的实际工作中，我带的团队基本上都会采用基于主分支的策略。只有在做一些实验的时候，才会拉出一个开发分支来，但它并不是常态。

总结时刻

TDD 在很多人眼中是不实用的，一来他们并不理解测试“驱动”开发的含义，但更重要的是，他们很少会做任务分解。而任务分解是做好 TDD 的关键点。只有把任务分解到可以测试的地步，才能够有针对性地写测试。

同样听到任务分解这个说法，不同的人理解依然是不一样的。我把任务分解的结果定义成微操作，它远比大多数人理解得小。我们能将任务分解到多小，就决定了我们原子操作的粒度是多大。软件开发中的许多问题正是由于粒度太大造成的，比如，分支策略。

如果今天的内容你只能记住一件事，那请记住：将任务拆小，越小越好。

最后，我想请你分享一下，你身边是否有一些由于任务分解得不够小带来的问题。欢迎在留言区写下你的想法。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

15 | 一起练习：手把手带你分解任务

前面在讨论 TDD 的时候，我们说任务分解是 TDD 的关键。但这种认识依然是一种感性上的认识。今天，我们就来用一个更加具体的例子，让你看看任务分解到底可以做到什么程度。

这个例子就是最简单的用户登录。需求很简单，用户通过用户名密码登录。

我相信，实现这个功能对大家来说并不困难，估计在我给出这个题目的时候，很多人脑子里已经开始写代码了。今天主要就是为了带着大家体验一下任务分解的过程，看看怎样将一个待实现的需求一步步拆细，变成一个个具体可执行的任务。

要完成这个需求，最基本的任务是用户通过输入用户名和密码登录。

用户名和密码登录这个任务很简单，但我们在第一部分讲过沙盘推演，只要推演一下便不难发现，这不是一个完整的需求。

用户名和密码是哪来的呢？它们可能是用户设置的，也可能是由系统管理员设置的。这里我们就把它们简单设定成由用户设定。另外，有用户登录，一般情况下，还会有一个退出的功能。好了，这才是一个简单而完整的需求。我们就不做进一步的需求扩展。

所以，我们要完成的需求列表是下面这样的。

假设我们就是拿到这个需求列表的程序员，要进行开发。我们先要分析一下要做的事情有哪些，也就是任务分解。到这里，你可以先暂停一会，尝试自己分解任务，之后，再来对比我后面给出分解的结果，看看差异有多少。

好，我们继续。

我们先来决定一下技术方案，就用最简单的方式实现，在数据库里建一张表保存用户信息。一旦牵扯到数据库表，就会涉及到数据库迁移，所以，有了下面的任务。

这时，需要确定这两个任务自己是否知道怎么做。设计表，一般熟悉 SQL 的人都知道怎么做。数据库迁移，可能要牵扯到技术选型，不同的数据库迁移工具，写法上略有差别，我们就把还不完全明确的内容加到任务清单里。

数据库的内容准备好了，接下来，就轮到编写代码的准备上了。我们准备用常见的 REST 服务对外提供访问。这里就采用最常规的三层技术架构，所以，一般要编写下面几项内容。

领域对象，这里就是用户。

数据访问层，在不同的项目里面叫法不一，有人从 J2EE 年代继承下来叫 DAO（数据访问对象，Data Access Obejct），有人跟着 Mybatis 叫 mapper，我现在更倾向于使用领域驱动设计的术语，叫 repository。

服务层，提供对外的应用服务，完成业务处理。

资源层，提供 API 接口，包括外部请求的合法性检查。

根据这个结构，就可以进一步拆解我们的开发任务了。

不知道你有没有注意到，我的任务清单上列任务的顺序，是按照一个需求完整实现的过程。

比如，第一部分就是一个完整的用户注册过程，先写 User，然后是 UserRepository 的 save 方法，接着是 UserService 的 register 方法，最后是 UserResource 的 register 方法。等这个需求开发完了，才是 login 和 logout。

很多人可能更习惯一个类一个类的写，我要说，最好按照一个需求、一个需求的过程走，这样，任务是可以随时停下来的。

比如，同样是只有一半的时间，我至少交付了一个完整的注册过程，而按照类写的方法，结果是一个需求都没完成。这只是两种不同的安排任务的顺序，我更支持按照需求的方式。

我们继续任务分解的讨论。任务分解到这里，需要看一下这几个任务有哪个不好实现。register 只是一个在数据库中存储对象的过程，没问题，但 login 和 logout 呢？

考虑到我们在做的是一个 REST 服务，这个服务可能是分布到多台机器上，请求到任何一台都能提供同样的服务，我们需要把登录信息共享出去。

这里我们就采用最常见的解决方案：用 Redis 共享数据。登录成功的话，就需要把用户的 Session 信息放到 Redis 里面，退出的话，就是删除 Session 信息。在我们的任务列表里，并没有出现 Session，所以，需要引入 Session 的概念。任务调整如下。

如果采用 Redis，我们还需要决定一下在 Redis 里存储对象的方式，我们可以用原生的 Java 序列化，但一般在开发中，我们会选择一个文本化的方式，这样维护起来更容易。这里选择常见的 JSON，所以，任务就又增加了两项。

至此，最基本的登录退出功能已经实现了，但我们需要问一个问题，这就够了吗？之所以要登录，通常是要限定用户访问一些资源，所以，我们还需要一些访问控制的能力。

简单的做法就是加入一个 filter，在请求到达真正的资源代码之前先做一层过滤，在这个 filter 里面，如果待访问的地址是需要登录访问的，我们就看看用户是否已经登录，现在一般的做法是用一个 Token，这个 Token 一般会从 HTTP 头里取出来。但这个 Token 是什么时候放进去的呢？答案显然是登录的时候。所以，我们继续调整任务列表。

至此，我们已经比较完整地实现了一个用户登录功能。当然，要在真实项目中应用，需求还是可以继续扩展的。比如：用户 Session 过期、用户名密码格式校验、密码加密保存以及刷新用户 Token 等等。

这里主要还是为了说明任务分解，相信如果需求继续扩展，根据上面的讨论，你是有能力进行后续分解的。

来看一下分解好的任务清单，你也可以拿出来自己的任务清单对比一下，看看差别有多大。

首先要说明的是，任务分解没有一个绝对的标准答案，分解的结果根据个人技术能力的不同，差异也会很大。

检验每个任务项是否拆分到位，就是看你是否知道它应该怎么做了。不过，即便你技术能力已经很强了，我依然建议你把任务分解到很细，观其大略人人行，细致入微见本事。

也许你会问我，我在写代码的时候，也会这么一项一项地把所有任务都写下来吗？实话说，我不会。因为任务分解我在之前已经训练过无数次，已经习惯怎么一步一步地把事情做完。换句话说，任务清单虽然我没写下来，但已经在我脑子里了。

不过，我会把想到的，但容易忽略的细节写下来，因为任务清单的主要作用是备忘录。一般情况下，主流程我们不会遗漏，但各种细节常常会遗漏，所以，想到了还是要记下来。

另外，对比我们在分解过程中的顺序，你会看到这个完整任务清单的顺序是调整过的，你可以按照这个列表中的内容一项一项地做，调整最基本的标准是，按照这些任务的依赖关系以及前面提到的“完整地实现一个需求”的原则。

最后，我要特别强调的一点，所有分解出来的任务，都是独立的。也就是说，每做完一个任务，代码都是可以提交的。只有这样，我们才可能做到真正意义上的小步提交。

如果今天的内容你只能记住一件事，那请记住：按照完整实现一个需求的顺序去安排分解出来的任务。

最后，我想请你分享一下，你的任务清单和我的任务清单有哪些差异呢？欢迎在留言区写下你的想法。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

16 | 为什么你的测试不够好？

你好！我是郑晔。今天是除夕，我在这里给大家拜年了，祝大家在新的一年里，开发越做越顺利！

关于测试，我们前面讲了很多，比如：开发者应该写测试；要写可测的代码；要想做好 TDD，先要做好任务分解，我还带你进行了实战操作，完整地分解了一个任务。

但有一个关于测试的重要话题，我们始终还没聊，那就是测试应该写成什么样。今天我就来说说怎么把测试写好。

你或许会说，这很简单啊，前面不都讲过了吗？不就是用测试框架写代码吗？其实，理论上来说，还真应该就是这么简单，但现实情况却往往相反。我看到过很多团队在测试上出现过各种各样的问题，比如：

测试不稳定，这次能过，下次过不了；

有时候是一个测试要测的东西很简单，测试周边的依赖很多，搭建环境就需要很长的时间；

这个测试要运行，必须等到另外一个测试运行结束；

……

如果你也在工作中遇到过类似的问题，那你理解的写测试和我理解的写测试可能不是一回事，那问题出在哪呢？

为什么你的测试不够好呢？

主要是因为这些测试不够简单。只有将复杂的测试拆分成简单的测试，测试才有可能做好。

简单的测试

测试为什么要简单呢？有一个很有趣的逻辑，不知道你想没想过，测试的作用是什么？显然，它是用来保证代码的正确性。随之而来的一个问题是，谁来保证测试的正确性？

许多人第一次面对这个问题，可能会一下子懵住，但脑子里很快便会出现一个答案：测试。但是，你见有人给测试写测试吗？肯定没有。因为一旦这么做，这个问题会随即上升，谁来保证那个测试的正确性呢？你总不能无限递归地给测试写测试吧。

既然无法用写程序的方式保证测试的正确性，我们只有一个办法：把测试写简单，简单到一目了然，不需要证明它的正确性。所以，如果你见到哪个测试写得很复杂，它一定不是一个好的测试。

既然说测试应该简单，我们就来看看一个简单的测试应该是什么样子。下面我给出一个简单的例子，你可以看一下。

@Test

void should_extract_HTTP_method_from_HTTP_request() {

  // 前置准备

  request = mock(HttpRequest.class);

  when(request.getMethod()).thenReturn(HttpMethod.GET);

  HttpMethodExtractor extractor = new HttpMethodExtractor();

  // 执行

  HttpMethod method = extractor.extract(request);

  // 断言

  assertThat(method, is(HttpMethod.GET);

  // 清理

}

这个测试来自我的开源项目 Moco，我稍做了一点调整，便于理解。这个测试很简单，从一个 HTTP 请求中提取出 HTTP 方法。

我把这段代码分成了四段，分别是前置准备、执行、断言和清理，这也是一般测试要具备的四段。

这几段的核心是中间的执行部分，它就是测试的目标，但实际上，它往往也是最短小的，一般就是一行代码调用。其他的部分都是围绕它展开的，在这里就是调用 HTTP 方法提取器提取 HTTP 方法。

前置准备，就是准备执行部分所需的依赖。比如，一个类所依赖的组件，或是调用方法所需要的参数。在这个测试里面，我们准备了一个 HTTP 请求，设置了它的方法是一个 GET 方法，这里面还用到了之前提到的 Mock 框架，因为完整地设置一个 HTTP 请求很麻烦，而且与这个测试也没什么关系。

断言是我们的预期，就是这段代码执行出来怎么算是对的。这里我们判断了提取出来的方法是否是 GET 方法。另外补充一点，断言并不仅仅是 assert，如果你用 Mock 框架的话，用以校验 mock 对象行为的 verify 也是一种断言。

清理是一个可能会有的部分，如果你的测试用到任何资源，都可以在这里释放掉。不过，如果你利用好现有的测试基础设施（比如，JUnit 的 Rule），遵循好测试规范的话，很多情况下，这个部分就会省掉了。

怎么样，看着很简单吧，是不是符合我前面所说的不证自明呢？

测试的坏味道

有了对测试结构的了解，我们再来说说常见的测试“坏味道”。

首先是执行部分。不知道你有没有注意到，前面我提到执行部分时用了一个说法，一行代码调用。是的，第一个“坏味道”就来自这里。

很多人总想在一个测试里做很多的事情，比如，出现了几个不同方法的调用。请问，你的代码到底是在测试谁呢？

这个测试一旦出错，就需要把所有相关的几个方法都查看一遍，这无疑是增加了工作的复杂度。

也许你会问，那我有好几个方法要测试，该怎么办呢？很简单，多写几个测试就好了。

另一个典型“坏味道”的高发区是在断言上，请记住，测试一定要有断言。没有断言的测试，是没有意义的，就像你说自己是世界冠军，总得比个赛吧！

我见过不少人写了不少测试，但测试运行几乎从来就不会错。出于好奇，我打开代码一看，没有断言。

没有断言当然就不会错了，写测试的同事还很委屈地说，测试不好写，而且，他已经验证了这段代码是对的。就像我前面讲过的，测试不好写，往往是设计的问题，应该调整的是设计，而不是在测试这里做妥协。

还有一种常见的“坏味道”：复杂。最典型的场景是，当你看到测试代码里出现各种判断和循环语句，基本上这个测试就有问题了。

举个例子，测试一个函数，你的断言写在一堆 if 语句中，美其名曰，根据条件执行。还是前面提到的那个观点，你怎么保证这个测试函数写的是对的？除非你用调试的手段，否则，你都无法判断你的条件分支是否执行到了。

你或许会疑问，我有一大堆不同的数据要测，不用循环不用判断，我怎么办呢？你真正应该做的是，多写几个测试，每个测试覆盖一种场景。

一段旅程（A-TRIP）

怎么样的测试算是好的测试呢？有人做了一个总结 A-TRIP，这是五个单词的缩写，分别是

Automatic，自动化；

Thorough，全面的；

Repeatable，可重复的；

Independent，独立的；

Professional，专业的。

下面，我们看看这几个单词分别代表什么意思。

Automatic，自动化。有了前面关于自动化测试的铺垫，这可能最好理解，就是把测试尽可能交给机器执行，人工参与的部分越少越好。

这也是我们在前面说，测试一定要有断言的原因，因为一个测试只有在有断言的情况下，机器才能自动地判断测试是否成功。

Thorough，全面，应该尽可能用测试覆盖各种场景。理解这一点有两个角度。一个是在写代码之前，要考虑各种场景：正常的、异常的、各种边界条件；另一个角度是，写完代码之后，我们要看测试是否覆盖了所有的代码和所有的分支，这就是各种测试覆盖率工具发挥作用的场景了。

当然，你想做到全面，并非易事，如果你的团队在补测试，一种办法是让测试覆盖率逐步提升。

Repeatable，可重复的。这里面有两个角度：某一个测试反复运行，结果应该是一样的，这说的是，每一个测试本身都不应该依赖于任何不在控制之下的环境。如果有，怎么办，想办法。

比如，如果有外部的依赖，就可以采用模拟服务的手段，我的 Moco 就是为了解决外部依赖而生的，它可以模拟外部的 HTTP 服务，让测试变得可控。

有的测试会依赖数据库，那就在执行完测试之后，将数据库环境恢复，像 Spring 的测试框架就提供了测试数据库回滚的能力。如果你的测试反复运行，不能产生相同的结果，要么是代码有问题，要么是测试有问题。

理解可重复性，还有一个角度，一堆测试反复运行，结果应该是一样的。这说明测试和测试之间没有任何依赖，这也是我们接下来要说的测试的另外一个特点。

Independent，独立的。测试和测试之间不应该有任何依赖，什么叫有依赖？比如，如果测试依赖于外部数据库或是第三方服务，测试 A 在运行时在数据库里写了一些值，测试 B 要用到数据库里的这些值，测试 B 必须在测试 A 之后运行，这就叫有依赖。

我们不能假设测试是按照编写顺序运行的。比如，有时为了加快测试运行速度，我们会将测试并行起来，在这种情况下，顺序是完全无法保证的。如果测试之间有依赖，就有可能出现各种问题。

减少外部依赖可以用 mock，实在要依赖，每个测试自己负责前置准备和后续清理。如果多个测试都有同样的准备和清理呢？那不就是 setup 和 teardown 发挥作用的地方吗？测试基础设施早就为我们做好了准备。

Professional，专业的。这一点是很多人观念中缺失的，测试代码，也是代码，也要按照代码的标准去维护。这就意味着你的测试代码也要写得清晰，比如：良好的命名，把函数写小，要重构，甚至要抽象出测试的基础库，在 Web 测试中常见的 PageObject 模式，就是这种理念的延伸。

看了这点，你或许会想，你说的东西有点道理，但我的代码那么复杂，测试路径非常多，我怎么能够让自己的测试做到满足这些要求呢？

我必须强调一个之前讲测试驱动开发强调过的观点：编写可测试的代码。很多人写不好测试，或者觉得测试难写，关键就在于，你始终是站在写代码的视角，而不是写测试的视角。如果你都不重视测试，不给测试留好空间，测试怎么能做好呢？

总结时刻

测试是一个说起来很简单，但很不容易写好的东西。在实际工作中，很多人都会遇到关于测试的各种各样问题。之所以出现问题，主要是因为这些测试写得太复杂了。测试一旦复杂了，我们就很难保证测试的正确性，何谈用测试保证代码的正确性。

我给你讲了测试的基本结构：前置准备、执行、断言和清理，还介绍了一些常见的测试“坏味道”：做了太多事的测试，没有断言的测试，还有一种看一眼就知道有问题的“坏味道”，测试里有判断语句。

怎么衡量测试是否做好了呢？有一个标准：A-TRIP，这是五个单词的缩写，分别是 Automatic（自动化）、Thorough（全面）、Repeatable（可重复的）、Independent（独立的）和 Professional（专业的）。

如果今天的内容你只能记住一件事，那请记住：要想写好测试，就要写简单的测试。

最后，我想请你分享一下，经过最近持续对测试的讲解，你对测试有了哪些与之前不同的理解呢？欢迎在留言区写下你的想法。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

17 | 程序员也可以“砍”需求吗？

我们前面讲的任务分解，主要是在讲开发任务的分解。今天我们换个角度，看看需求的分解。是的，需求也要分解。

有一次，我和一个做开发的同事聊天，他给我讲了他近期的烦恼。

同事：我们现在就是需求太多，开发的人太少，再这么干下去，哪天觉得自己抗不住了，我就拍拍屁股走人。
我：你没尝试着砍砍需求？
同事：怎么没尝试？产品的人都不同意。这批功能他们都说是关键功能。
我：你有没有尝试把需求拆开了再砍呢？
同事：还可以这样？

同事很惊讶，我一点都不意外。我们都是在说需求，但彼此对需求的理解却是大不相同。我先来问个问题，提到需求这个词，你会想到什么呢？

以我们用了好多次的登录为例，如果我问你这个需求是什么，大多数人的第一直觉还是用户名密码登录。

基本上，闯入你脑海的需求描述是主题（epic），在敏捷开发中，有人称之为主用户故事（master story）。

如果你对需求的管理粒度就是主题，那好多事情就没法谈了。比如，时间紧迫的时候，我想砍需求，你问产品经理，我不做登录行不行，你就等着被拒绝吧。

但是，如果你说时间比较紧，我能不能把登录验证码放到后面做，或是邮件地址验证的功能放到后面，这种建议产品经理是可以和你谈的。

这其中的差别就在于，后者将需求分解了。

大多数人可以理解需求是要分解的，但是，分解的程度不同，就是导致执行效果差异极大的根源。

以我的经验而言，绝大多数问题都是由于分解的粒度太大造成的，少有因为粒度太小而出问题的。所以，需求分解的一个原则是，粒度越小越好。

需求要分解

“主题”只是帮你记住大方向，真正用来进行需求管理，还是要靠进一步分解出来的需求。这里的讨论，我们会继续沿用前面专栏文章中已经介绍过的需求描述方式：用户故事，它将是我们这里讨论需求管理的基本单位。

如果你的团队用的是其他方式描述需求，你也可以找找是否有对应的管理方式。

上一个模块介绍“以终为始”，我们对用户故事的关注点主要在：用户故事一定要有验收标准，以确保一个需求的完整性。而在“任务分解”这个模块，我们看用户故事，则主要关注它作为需求分解的结果，也就是分拆出来要解决的一个个需求点。

在前面的讨论中，我们已经知道了用户故事的“长相”，但更重要的问题是，划分需求的方式有无数种，就像一块蛋糕，你可以横着切，也可以竖着切。如果你一刀不切，那就是拿着主题当用户故事。你也可以快刀飞起，把主题切碎。

每个人都会有自己喜欢的拆分方式，我相信知道拆分的重要性之后，你总会有办法的。这里，我主要想和你聊聊怎样评判拆分结果，毕竟我们要把它当作需求管理的基本单位。

只有细分的需求才能方便进行管理。什么样的需求才是一个好的细分需求呢？我们先来看看用户故事的衡量标准。

评价用户故事有一个“ INVEST 原则”，这是六个单词的缩写，分别是：

Independent，独立的。一个用户故事应该完成一个独立的功能，尽可能不依赖于其它用户故事，因为彼此依赖的用户故事会让管理优先级、预估工作量都变得更加困难。如果真的有依赖，一种好的做法是，将依赖部分拆出来，重新调整。

Negotiable，可协商的。有事大家商量是一起工作的前提，我们无法保证所有的细节都能 100% 落实到用户故事里，这个时候最好的办法是大家商量。它也是满足其它评判标准的前提，就像前面提到的，一个用户故事不独立，需要分解，这也需要大家一起商量的。

Valuable，有价值的。一个用户故事都应该有其自身价值，这一项应该最容易理解，没有价值的事不做。但正如我们一直在说的那样，做任何一个事情之前，先问问价值所在。

Estimatable，可估算的。我们会利用用户故事估算的结果安排后续的工作计划。不能估算的用户故事，要么是因为有很多不确定的因素，要么是因为需求还是太大，这样的故事还没有到一个能开发的状态，还需要产品经理进一步分析。

Small，小。步子大了，不行。不能在一定时间内完成的用户故事只应该有一个结果，拆分。小的用户故事才方便调度，才好安排工作。

Testable，可测试的。不能测试谁知道你做得对不对。这个是我们在前面已经强调过的内容，也就是验收标准，你得知道怎样才算是工作完成。

“INVEST 原则”的说法是为了方便记忆，我们这里着重讨论两个点。

第一个关注点是可协商。作为实现者，我们要问问题。只是被动接受的程序员，价值就少了一半，只要你开始发问，你就会发现很多写需求的人没有想清楚的地方。

在我的职业生涯中，我无数次将需求挡了回去，不是我不合作，而是我不想做一些糊涂的需求。我之所以能问出问题，一方面是出于常识，另一方面就是这里说的用户故事是否是有价值。用户故事，之所以是故事，就是要讲，要沟通。

还有一个更重要的关注点，也是这个模块的核心：小。无论是独立性也好，还是可估算的也罢，其前提都是小。只有当用户故事够小了，我们后续的腾挪空间才会大。

那接下来就是一个重要的问题，怎么才算小？这就牵扯到用户故事另一个重要方面：估算。

需求的估算

估算用户故事，首先要选择一个度量标准。度量用户故事大小的方式有很多种，有人用 T 恤大小的方式，也就是 S、M、L、XL、XXL。也有人用费波纳契数列，也就是 1、2、3、5、8 等等。有了度量标准之后，就可以开始估算了。

我们从分解出来的用户故事挑出一个最简单的，比如，某个信息的查询。这个最简单的用户故事，其作用就是当作基准。

比如，我们采用费波纳契数列，那这个最简单的用户故事就是基准点 1。其他的用户故事要与它一一比较，如果一个用户故事比它复杂，那可以按照复杂程度给个估计。

你或许会问，我怎么知道复杂程度是什么样的呢？这时候，我们前面讲过的任务分解就派上用场了，你得在大脑中快速地做一个任务分解，想想有哪些步骤要完成，然后才好做对比。

所以，你会发现，任务分解是基础中的基础，不学会分解，工作就只能依赖于感觉，很难成为一个靠谱的程序员。

估算的结果是相对的，不是绝对精确的，我们不必像做科研一样，只要给出一个相对估算就好。

同一个用户故事，不同的人估算出的结果可能会有差别。怎么样尽可能在团队中达成一致呢？这就需要团队中的很多人参与进来，如果团队规模不大，全员参与也可以。

如果多人进行估算，你就会发现一个有趣的现象，针对同一个用户故事，不同的人估算的结果差异很大。

如果差别不大，比如，你觉得 3 个点，我觉得 2 个点，我们协调一下就好。但如果差异很大，比如，你认为 2 个点，我认为 8 个点，那绝对是双方对任务的理解出现了巨大的差异，这个时候，我们就可以把刚才在脑中进行的任务分解“摆”到桌面上，看看差异在哪。

通常情况下，是双方对需求的理解出现了偏差，这时候负责用户故事编写的同事就要站出来，帮助大家澄清需求。所以，一般来说，估算的过程也是大家加深对需求理解的过程。

估算还有另外一个重要的作用：发现特别大的用户故事。一般而言，一个用户故事应该在一个迭代内完成。

比如，你预计大小为 1 点的用户故事要用 1 天完成，而你团队的迭代周期是两周，也就是 10 个工作日，那 13 点的任务是无论如何都完不成的。那该怎么办呢？很简单，把它拆分成多个小任务，这样一来，每个小任务都可以在一个迭代中完成了。

所以，一般来说，用户故事有可能经过两次拆分。一次是由负责业务需求的同事，比如，产品经理，根据业务做一次拆分。另外一次就是在估算阶段发现过大的用户故事，就再拆分一次。

当我们有了一个合适的用户故事列表，接下来，我们就可以安排我们的开发计划了。只要厘清用户故事之间的依赖关系，安排工作是每一个团队都擅长的事情。

我在这里想回到我们开头讨论的话题。我们常说，需求来自产品经理，但需求到底是什么，这是一个很宽泛的话题。到这里，我们已经有了一个更清晰更可管理的需求，用户故事。这时候我们再说需求调整，调整的就不再是一个大主题，而是一个个具体的用户故事了。

许多团队真正的困境在于，在开发过程中缺少需求分解的环节。在这种情况下，需求的管理基本单位就是一个主题，既然是基本单位，那就是一个不可分割的整体。团队就被生生绑死在一个巨大的需求上，没有回旋的余地。

如果团队可以将需求分解，需求的基本单位就会缩小，每个人看到的就不再是“铁板”一块，才能更方便地进行调整，才会有比较大的腾挪空间。

总结时刻

软件开发中，需求管理是非常重要的一环。在需求管理上常见的错误是，需求管理的粒度太大，很多团队几乎是在用一个大主题在管理需求，这就让需求调整的空间变得很小。

结合用户故事，我给你讲了一个好的需求管理基本单位是什么样子的，它要符合“INVEST 原则”。其中的一个关键点是“小”，只有小的需求才方便管理和调整。

什么样的需求才算小呢？我给你介绍了一种需求估算的方式，每个团队都可以根据自己的特点决定在自己的团队里，多大的需求算大。大需求怎么办？只要再进行分解就好了。

如果你对用户故事这个话题感兴趣，推荐阅读 Mike Cohn 的两本书《User Stories Applied》和《Agile Estimating and Planning》。

如果今天的内容你只能记住一件事，那请记住：想要管理好需求，先把需求拆小。

最后，我想请你分享一下，你的团队在需求管理上还遇到过哪些问题呢？欢迎在留言区写下你的想法。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

18 | 需求管理：太多人给你安排任务，怎么办？

上一讲我们讲了需求的分解，我以用户故事为例，给你讲了我们应该把大的需求拆分成小的需求，但是不是只要把需求拆开了就万事大吉了呢？显然不是。今天我们再来探讨另一个与需求强相关的话题：需求管理。

需求管理？许多程序员的第一直觉通常是，这要么是产品经理的事，要么是项目经理的事，跟我有什么关系？我知道很多人会这么想，可我想说的是，如果你不了解需求是怎么管理的，即便是进行了需求分解，最终的结果很有可能依然是你深陷泥潭苦苦挣扎而不自知。

为什么这么说呢？我给你讲一个发生在我身边的故事。

最无脑的需求管理法：老板说的

有一次，我们组织了一次各团队负责人的吐槽大会，让大家把遇到的问题在台面上“摆”一下。一个开发团队的负责人说：“我这边倒排期太严重了，每个产品经理到我这里都说上线日期已经定好了，我这边资源有限，实在是抗不住了。”

出于好奇，有人问：“这些任务都一样重要吗？”

这个负责人无奈地摇摇头，“他们都说自己的任务重要。”

“他们凭什么说自己的任务重要呢？”我也问了一个问题。

这个负责人说：“他们告诉我，是老板说的。”

这是不是一个很熟悉的场景？一堆任务压过来，只是因为这是老板的一句话。我们的老板都是这么不近人情吗？其实，大概率来看，并不是。

就凭一句“老板说的”，我们就可以判断出，产品经理缺乏对需求管理应有的理解。而研发团队也因为无脑地接受了需求，几乎将自己压垮。

这时候，CTO 发话了：“口头的东西不算数，如果他们说是老板说的，那就让老板发邮件确认。”

我很认可 CTO 的说法，但我并不放心那个开发团队的负责人，于是我问他：“你会让产品经理这么去做吗？”果然，他犹豫了。

“产品经理可能不会和老板这么说。那你去说好了。”我们又给他提了个建议。显然，他更犹豫了，毕竟要面对大老板。

针对这种情况，我们又给出了一个解决办法，“如果你担心产品经理不这么做，你可以直接发邮件给老板，同时抄送 CTO。”

“对，可以这么做”，CTO 把责任抗了过去。这个负责人心里一下子有底了。

是不是有种似曾相识的感觉？其实，这个故事只要再往下延伸一点，就到了我们程序员身边。

作为程序员，我们面临的场景往往是，一个需求不明就里地来了，你的周末假期全部泡汤，因为你的负责人会和你说，这是老板说的。

软件行业有个段子：做软件，最理想的交付日期是什么时候？答案是昨天，其次是尽快。所有提出业务需求的人都恨不得需求早就做好了。但事实总是那么不如人意，所以，他们只能寄希望于需求被尽快实现。

如果我们等着所有需求都开发好了再上线呢？这就是当年所谓瀑布模型做的事，放在二十年前，这种做法还有生存空间，但今天这种做法显然已经不合时宜了。

关于如何做软件，我们已经讨论了很多，关键点就在于这个世界有太多的不确定，我们只好把产品的“一部分”开发好，送上线。

这就引出了一个问题，到底是选择“哪部分”优先上线呢？我们必须在宏大的理想和骨感的现实中作出取舍。这也就牵扯出需求管理的本质，实际上是个优先级的问题。

需求的优先级

“来自老板”，这是判断优先级最简单的答案，也是推卸责任的一个答案。其潜台词是，压力大不怪我，要怪就怪老板去。“来自老板”不应该成为优先做事的指标。

首先，我们要明确一点，优先级这种事大家也是可以谈的，大多数能当老板的人都是可以讲道理的。但要和老板谈，我们得知道怎么讲道理。准备一些基础知识，才能与各级老板探讨怎么安排工作的优先级。

为什么要区分优先级？因为时间是有限的，有限的时间内你能完成工作的上限是一定的。

怎么充分利用好有限的时间，这其实是一个时间管理的问题。所以，我们完全可以借鉴时间管理领域的一些优秀实践，帮助我们更有效地明辨优先级。

谈到时间管理，一个有效的时间管理策略是艾森豪威尔矩阵（Eisenhower Matrix），这是由美国前总统艾森豪威尔开发出的一个工具。

这个工具到了史蒂芬·柯维（Stephen Richards Covey）手里得到了发扬光大，他那本著名的《高效能人士的七个习惯》书籍将其推广至世界各地。也许这个名字你不太熟悉，看一下下面这个图你就知道了。

它将事情按照重要和紧急两个维度进行划分，也就形成了四个部分：重要且紧急，重要不紧急，不重要且紧急，不重要不紧急。

用几个程序员生活中的例子帮你理解一下。让系统不能正常运行的线上故障，就属于重要且紧急事情，不赶紧解决，就影响公司的正常运营。团队对系统升级改造就属于重要不紧急：改造好，性能也好，可维护性也得到提升；不改造，一时半会也能用。一些临时任务都属于紧急不重要，而刷朋友圈则属于既不紧急也不重要。

按照时间管理的理念，重要且紧急的事情要立即做。重要但不紧急的事情应该是我们重点投入精力的地方。紧急但不重要的事情，可以委托别人做。不重要不紧急的事情，尽量少做。

这个矩阵带给我们思维上最大的改变是，让人意识到事情和事情不是等价的。如果不把精力放在重要的事情上，到最后可能都变成紧急的事情。

比如，我们放任系统不做升级改造，过多的技术债会让系统的问题越来越多，新需求实现的速度越来越慢，最后几个看起来不大的需求就足以让团队加班加点，天怒人怨。

把这个思路带回到我们现实的需求管理中，你会发现，其实团队面临的各种需求所采用的优先级排序方式，基本上都是按照紧急程度排列的，但它们是否真的重要呢？

如果你把这个问题抛给需求的提出者，我几乎可以肯定，他们给你的答案是，他们提出的需求就是重要的。一种可能是，他们也分不清重要和紧急的差别，正如有时候我们也糊涂一样。

对于这样的场景，我们要做的就是多问一些问题。我在“精益创业：产品经理不靠谱，你该怎么办？”文章中说过，默认所有需求都不做，直到弄清楚为什么要做这件事。

同样，需求也没那么重要，直到产品经理能说明白它为什么重要，尤其是为什么比其他需求重要。如果一个产品经理不能把几个需求排出优先级，你就可以把上面学到的内容给他讲一遍。

还有另一种可能，他给你的需求在他工作的上下文中，确实是最重要的内容了。但当有多个需求来源时，我们该如何确认哪个需求是最重要的呢？这时，才到了真正需要老板出场的时刻。

站在老板面前

在“解决了很多问题，为什么你依然在‘坑’里？”文章中，我曾经讲过，大家不要局限于程序员这个角色，不同角色真正的差异是工作上下文的不同。每个人都在自己的上下文里工作，上下文也就局限了很多人的视野。

试想，两个产品经理出现在你面前，一个告诉你，公司要拓展新方向，这个功能要做；另一个却说，公司要进一步盈利，那个功能必须做。

在你看来，他们两个说得都对，听上去都挺重要的。但骨感的现实是，你把两件事都接下来，等着你的是累死都完不成的任务。

这个时候，我们能做的是什么呢？跳出这个上下文，到更大的上下文中。你判断不了哪个需求更重要，就请更高一级的老板来判断。

有了基础知识的储备，我们终于可以站在了老板面前。你可以告诉老板：我资源有限，需要将这两个需求排个序，看哪个更重要。我的上下文有限，需要你帮我判断一下。

老板会和你说这起两个需求的起源，扩展盈利的需求是竞争对手都已经有了，客户也问这边要，再不做会影响客户关系，尤其是新财年快到了，下个阶段的合同会受到影响。而另外的新业务是某天一个高端聚会上得到的新启发，想尝试一下，他也不确定这个想法能带来多少收益，就让产品部门试一下。

听了老板的信息，你顿时明白这两件事的重要性，你也知道该如何面对两个产品经理了。

老板比你们的上下文大，因为他有看待这个问题更多的维度。所以，在你们眼里无比纠结的事情，老板几句话就云开雾散了，在他眼里，那根本不叫事。

如果你看过刘慈欣的《三体》，就会知道，这其实是“降维攻击”。另一个你可能熟悉的说法叫大局观。我经常和人说，当员工想不明白的事，换成老板的视角就全明白了。

我鼓励每个程序员在更大的上下文中工作，也就是想让人获得更多的思考维度。而今天的内容主要告诉你，如果自己的上下文不足时，我们可以引入新的元素，比如征求老板意见，扩大自己的上下文。

再发散讲几句，为人做事同样要不断扩展自己的上下文，这也就是我们常说的涨见识。

很多所谓的人生难题不过是因为见识有限造成的。比如，如果你觉得公司内总有人跟你比技术，莫不如把眼光放得长远一些，把自己放在全行业的水平上去比较。因为你是为自己的职业生涯在工作，而不是一个公司。

总结时刻

需求分解之后，最重要的是，排列需求的优先级。优先级的排列方式有很多，我们可以借鉴时间管理的方法，把事情按照重要和紧急的维度进行划分，得到了四个象限。我们要尽可能把精力放在重要的事情上，而不是把紧急的事情当成优先级排序的方式。

需求分解成一个个小块，其实也分解了原本合一的上下文。如果想要有效地管理需求，尤其是确定事情的重要程度，一种方式是找回丢失的上下文。如果我们自己无法判断上下文，一种好的办法是，引入外部更大的上下文。

如果今天的内容你只能记住一件事，那请记住：尽量做最重要的事。

最后，我想请你分享一下，你的团队在日常的需求管理中，还遇到哪些问题呢？欢迎在留言区写下你的想法。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

19 | 如何用最小的代价做产品？

前面我们讲了开发任务的分解和需求管理的分解，这些都是针对“已经确定好要做的事情”的分解策略，今天我们再上一个台阶，聊聊面对那些不确定的产品功能该如何分解。

产品经理的想法层出不穷，但是，如果我们一味闷着头实现产品经理的想法，无论你有多大的开发团队都是不够用的。我们要学会用最小的代价做产品。

谈到产品这个话题，在“精益创业：产品经理不靠谱，你该怎么办？”这篇文章中，我给你分享了精益创业的理念，任何的想法都要放到真实世界中检验。

我们的直觉当然是把所有的东西都实现了再去检验，但是世界不会停下来等着我们。事实也一次又一次教育我们，“憋大招”的瀑布式软件开发已经成为不合时宜的“老古董”。那我们的理想怎么实现呢？唯有分解。

我们前面提到，精益创业就是通过不断地尝试在真实世界中验证产品想法，其中一个重要的实践是最小可行产品（Minimum Viable Product，MVP），我们这次就把这个实践展开讨论一下。

什么叫最小可行产品？就是“刚刚好”满足客户需求的产品。客户需求好理解，怎么算“刚刚好”呢？其中的关键在于理解“最小”和“可行”。

最小的代价

先说“最小”。这里的“最小”，指的是最小的代价。怎么叫最小的代价，就是能不做的事情就不做，能简化的事情就简化。

首先，我们必须清楚一件事，我们要做的是验证一个想法的可行性，甚至不是为了开发一个软件，开发软件只是一种验证手段。

很多程序员都会有一个认识上的误区，容易把解决方案当做问题。我们开发软件的目的是为了解决问题，如果不写软件就把问题解决了，岂不是更好。

我先讲一个自己的经历，帮你理解一下什么叫“最小”。有一次，有一个朋友找我帮忙，他手头有一些制造业的客户，想做一个物联网相关的项目，帮助这些客户改造设备，实现物联网功能。

该怎么着手呢？把软件写好，给客户试用吗？这样时间太长，成本太高。那么，我们是怎么做的呢？

第一步，我们要验证这样一个想法是否可行。我们做了一个产品文档，就好像我们已经有了这个产品一样，让负责销售的同事拿着这个文档给客户讲讲，看看客户对这个想法的反映。

在这个过程中，我们验证了基本的想法，已有设备进行物联网化改造的需求存在，客户看到了这样的一个东西，各种各样的想法和要求就会冒出来。

此外，我们还获得了一个额外的收获，我们知道了客户对于这样一个产品能够接受的价格区间，这可以帮助团队给产品进行适当的定价。

验证了方向上的想法，我们开始进入到具体的产品设计阶段。这个阶段我们想验证的是，我们给出的产品设计用户是否可以接受。于是，我们决定把这个产品的交互做出来。

得益于原型工具的快速发展，我们用一个原型工具做出了相对完整的用户界面，而且把各种交互流都做出来了。在用户看来，这几乎就是完整的软件了。

他们甚至可以在自己的设备上体验一下这个产品用起来是什么感觉的。一旦上手用起来，他们就会抛出各种细节的问题：如果这样就好了，如果能做到这个就太棒了。当然，他们也会说，这个东西我不需要。

这个时候，我们就可以知道，我们在产品上的假设哪些是好的，哪些是不流畅的。团队拿到这些反馈，就可以再调整产品设计，然后，再给到用户去测试，如此反复进行。有的时候，产品会在一天之内改好几个版本。

经过多轮测试下来，团队有了一大堆的用户反馈，而且是来自真实用户的反馈。接下来，就是整理这些用户反馈，决定哪些可以真正的开发出来，这时候，团队才真正进入到开发阶段。

不知道你注意到了没有，迄今为止，这个团队验证了一大堆的想法，而代码却是一行都没有写，所有花费的工作量都是有针对性的验证。

我们经常听到一个段子，叫“就差一个程序员了”。这说的是，一个创业者把前期的准备都做好，就差程序员把产品开发出来了。

按照 MVP 的思想，这个创业者做的就是对的，前提是他真的把前期准备都做好了。

开发软件是一件成本很高的事情。如果只是验证想法，无论是创业方向，还是产品设计，我们可以找到各种各样的手段，不用写代码。

即便我们不是在做一个新产品，我们依然可以运用这个“最小代价”的理念在日常工作中做事。比如，怎么来衡量产品经理的产品设计是不是好的。我会问，这个功能不做，用户会怎么样？有没有什么替代方案等等。以此来帮助产品经理想清楚自己的产品设计是否真的有价值。

可行的路径

说完了"最小"，我们再来看"可行"。可行是要找到一条路径，给用户一个完整的体验。做程序员出身的人，对软件系统的认识总是一个模块一个模块的，相对比较弱的方面是缺少一个完整的图景。

但从产品可行的角度，我们需要转换一下思路，不是一个模块做得有多完整，而一条用户路径是否通畅。

我再给你分享一个我当年做 P2P 项目经历，这里的 P2P 指的是个人对个人的互联网借贷平台。

这是一个从头开始的项目，项目方和所有的项目方一样，希望昨天这个项目就上线了，如果不能，那就尽快上线一个版本。他们给我们一个时间线，第一个上线的版本是一个月之后。

摆在我们面前的问题是，无论如何，在一个“一穷二白”的基础上，要在一个月内完成一个完整的借贷平台是不太可能的。

时间有限，我们只能做最基本的东西，许多运营上的想法，比如，发红包代金券之类的，第一期一律不做。即便如此，我们仍然认为完成完整的借贷循环是不现实的。

于是，我们就开始从需求完整性的角度动脑筋。这是一个借贷系统，其最基本的模型是：贷款方贷款之后，一次性拿到所有的钱，然后用等额本息的方式每个月还款，最后一个月剩多少钱一次性全还了。

我们在这个模型中找到了一个关键点，每个月还款。换句话说，第一笔贷款发生之后，最早的一笔还款是发生在一个月之后的。

于是，我们做了一个决定，第一个版本只包含贷款能力。是的，这个版本只能贷款，不能还款。因为用户一个月之内不会用到这个功能，你从页面上，完全看不出这样的能力缺失，因为一个月内，根本没有任何用户有可还的款项。

因为缩减了项目规模，我们在预期的一个月内完成所有开发，成功地把项目送上了线。第一批早期用户就开始了使用。从用户的视角看，这是一个功能完整的项目，虽然简单了点，但它是完整的。

当然，我们把还款排到了下一期。按照我们两周一迭代的节奏，在第一期上线两周之后，我们就会上线还款功能，届时贷款方将拥有一个真正的还款功能。

不过，这个还款功能只是每期的等额本息还款，最后的一次性还剩余所有贷款的功能，我们依然是不支持的。因为根据需求设计，最后一次还款最早发生在一年之后。

在我们把基本的功能全部送上线之后，这个系统就是一个真正的、完整的借贷平台了。但是，相对于其他提供相同能力的平台而言，这个系统依然还是很简单。比如，常见的运营功能、短期借贷计划，这个平台都没有。

但我们有了基础，接下来，就是在基础上叠加，而且随着项目方自己团队的构建，我们拥有了够大的团队，可以同时做几个大需求了。

就这样，几个月之后，我们就逐步上线了一个功能相对完整的 P2P 平台。在这个过程中，我们每个阶段都会上线新功能，从用户可见的角度，他看到的始终是一个完整的平台，其中的变化只有站在内部实现者的角度才能看得清楚。

和大家分享这个例子，主要是想破除大家对于一个“完整”系统概念的认识。当时间有限时，我们需要学会找到一条可行的路径，在完整用户体验和完整系统之间，找到一个平衡。

站在开发团队的角度，我们怎样把 MVP 理念运用在自己的工作中呢？当产品经理有一大堆要实现的功能时，我们就可以根据 MVP 理念，从这些产品功能中找出一条最小的可行路径，重新安排一个合理的开发计划。

总结时刻

产品同样需要分解，目前在探索产品的不确定性上的最佳实践是精益创业，而精益创业就包含了将庞大的产品分而治之的方式：最小可行产品（Minimum Viable Product，MVP）。最小可行产品就是“刚刚好”满足客户需求的产品。

想要在实践中运用好最小可行产品的理念，就是要用最小的代价找到一条可行的路径。最小的代价就是能不做的事就不做，能简化的事情就简化。

程序员经常愿意用自己的代码解决问题，而写代码通常是代价非常高的解决方案，它应该成为最后的产品解决方案。

可行的路径，是一条完整的用户体验路径，至少在用户眼中是这样的。我们常常会想给客户一个完整的系统，但在时间有限的情况下，我们必须学会分解。

如果今天的内容你只能记住一件事，那请记住：做好产品开发，最可行的方式是采用 MVP。

最后，我想请你分享一下，你遇到或听说过采用 MVP 或类似方法解决问题的案例吗？欢迎在留言区写下你的想法。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

划重点 | 关于“任务分解”，你要重点掌握哪些事？

你好，我是郑晔，恭喜你，又完成了一个模块的学习。

在这个模块中，我主要讲解的是“任务分解”这个知易行难的工作原则。普通人与高手之间的差异，很大程度上取决于任务分解的粒度大小。但真正理解并应用好“任务分解”的原则并不容易，希望你能勤于练习，将知识内化成为你的能力。

重点复习

在这个模块中，我们学习到了一些最佳实践：

测试金字塔
– 行业中测试组合的最佳实践。
– 多写单元测试是关键。

测试驱动开发
– 测试驱动开发的节奏是：红——绿——重构，重构是测试驱动开发区别于测试先行的关键。
– 有人把测试驱动开发理解成测试驱动设计，它给行业带来的思维改变是，编写可测的代码。

艾森豪威尔矩阵（Eisenhower Matrix）
– 将事情按照重要和紧急进行划分。
– 重要且紧急的事情要立即做。重要但不紧急的事情应该是我们重点投入精力的地方。紧急但不重要的事情，可以委托别人做。不重要不紧急的事情，尽量少做。

最小可行产品
– “刚刚好”满足客户需求的产品。
– 在实践中，要用最小的代价找到一条可行的路径。

另外，我还提到了一些可以直接在工作中应用的做法和评判标准：

尽量不写 static 方法；

主分支开发模型是一种更好的开发分支模型；

好的用户故事应该符合 INVEST 原则；

估算是一个加深对需求理解的过程，好的估算是以任务分解为基础的；

好的测试应该符合 A-TRIP。

我也带你学习了一些重要的思想，帮你更好地改善自己的开发工作：

分而治之，是人类解决问题的基本手段；

软件变更成本，它会随着时间和开发阶段逐步增加；

测试框架把自动化测试作为一种最佳实践引入到开发过程中，使得测试动作可以通过标准化的手段固定下来；

极限编程之所以叫“极限”，它背后的理念就是把好的实践推向极限；

大师级程序员的工作秘笈是任务分解，分解到可以进行的微操作；

按照完整实现一个需求的顺序安排开发任务。

实战指南

在“任务分解”的板块，我也将每篇内容浓缩为一句实战指南，现在一起回顾一下。

动手做一个工作之前，请先对它进行任务分解。
—— 《11 | 向埃隆·马斯克学习任务分解》

多写单元测试。
——《12 | 测试也是程序员的事吗？》

我们应该编写可测的代码。
——《13 | 先写测试，就是测试驱动开发吗？》

将任务拆小，越小越好。
——《14 | 大师级程序员的工作秘笈》

按照完整实现一个需求的顺序去安排分解出来的任务。
——《15 | 一起练习：手把手带你拆任务》

要想写好测试，就要写简单的测试。
——《16 | 为什么你的测试不够好？》

想要管理好需求，先把需求拆小。
——《17 | 程序员也可以“砍”需求吗？》

尽量做最重要的事。
——《18 | 需求管理：太多人给你安排任务，怎么办？》

做好产品开发，最可行的方式是采用 MVP。
——《19 | 如何用最小的代价做产品？》

额外收获

在这个部分的最后，针对大家在学习过程中的热门问题，我也进行了回答，希望你懂得：

对不了解技术的任务，先要去了解技术，然后再做任务分解；

通过一次技术 Spike ，学习新技术；

丢弃掉在 Spike 过程中开发的原型代码；

分清目标与现状，用目标作为方向，指导现状的改变；

多个功能并行开发可以考虑使用 Feature Toggle；

在遗留系统上做改造可以考虑使用 Branch by Abstraction 。

——《答疑解惑 | 如何分解一个你不了解的技术任务？》

留言精选

在“任务分解”的模块中，有很多同学非常用心，将自己的学习心得和工作中的经验进行了分享，在此我挑选了一些同学的留言，与你一起学习。

在讲大师级程序员的工作秘笈时，西西弗与卡夫卡 同学提到：

最近在做战略拆解，都是一样的道理。战略飘在空中遥不可及，要落地就必须拆解。比如说达成目标有哪几个方面可以努力，各方面都需要做哪些事，这是路径。这些路径里哪些优先级最高，需要配置哪些组织资源。心里有数之后就是制订计划时间表。

另外，西西弗与卡夫卡 同学还为 Spike 给出了一个很生动的解释：

“技术 Spike”可以翻译成“技术撩”，就是撩妹的那个撩。试探下，有戏就继续，撩不动就算或者放一段时间再说。

针对分解的粒度问题，大彬 同学也分享了自己的心得：

我会的任务分解，不仅可执行，粒度还很细。比如说，我要修复一个 rpc 接口的 bug。我会列出每个代码的修改点，要修改的测试，要增加的测试，合并到哪个分支，修改 rpc 文档，文档中有哪些点要修改。
每一步都非常容易执行，看起来没多少必要，但在我当前的工作环境特别有用：（1）事前思考，不会造成遗漏；（2）任务实施过程中经常被打断，比如，测试有疑问和你讨论、主管找你谈事、紧急会议来了，这种“硬中断”完全打破了节奏，而任务列表，让我清楚知道当前做了多少，该从哪一步继续。

对于单元测试，树根 同学提到：

我的想法可以在复杂度高，重要核心的模块先开始写单元测试。特别是公用、底层的，因为这些靠功能测试很难覆盖。
单元测试难以推行主要是没有碰到质量的痛点，通常都依靠测试工程师来保证质量。我们之前就遇到过质量崩塌，倒逼着我们去做，以保证质量。

树根 同学还分享了自己的任务分解实践心得：

刚改了编程习惯，先在 notion 写出思路、需要用到的知识点，api 等，写出各个小任务，然后对应写出关键代码段。最后真正敲代码就花了 10 来分钟。
重新开始看极客就看到这篇，实践过来读，很认同。
我特别佩服国外的工程师写的代码，代码块很小，非常清晰易读。特别记得之前参加 infoq 会议，听 socketio 作者的分享，看他现场撸码，思路、代码结构都非常顺畅和清晰。

关于 TDD 的具体应用， 萧 同学提到了遇到的问题：

不久前第一次接触 TDD 时为它的思想而惊叹，感觉它能极大的提升编码效率，编码后期的大量重构，还能保障代码质量。后面自己在写代码的时候也注意使用它的思想，但说实话，理解是一回事，用起来就不是那么回事了，很多的东西还不是太熟练，前期说实话比较耗时间，有些拖进度。
由于也毕业不久，经验上有些欠缺，还不太熟练，有些测试还不知道怎么写。现在写多了一点，感受到的是代码质量上的提高，bug 比起以前少了，需求变更下改动，也不伤筋动骨了，但还是有许多感觉做的不够好的地方。看了这篇文章，补充了对 TDD 的认知，感受到如果和任务分解结合起来，TDD 会有更好的效果，期待后面的文章！

关于“任务分解”的执行问题，如明如月 同学分享了感悟：

对任务分解的体会非常深刻，刚入职的时候任务评估不准。现在想想主要是两个原因：（1）需求梳理的不清晰，还没清楚地搞明白需求就动手写代码，导致返工和一些“意想不到”的情况。（2）任务分解做的不好，没有将任务分解成非常清晰地可执行的单元，导致有些时候无从下手，而且任务时间评估不准确。

在讲到为什么很多人的测试不够好这个问题时， 毅 同学提到：

本节课我有以下几点体会：
（1）从开发者的视角看，编码和测试是不分家的，是可以通过重构形成良性生态圈的，类似之前课程中的反馈模型和红绿重构模型；
（2）A-TRIP 是个很好的总结和行动指南，在今后工作中应一以贯之，把工作做到扎实有成效；
（3）对文中提到的数据库依赖的问题，我也说说自己的浅见。我觉得在测试代码中，尽量避免与数据库打交道，测试更关注领域与业务，往往爆雷更多的是 resource 和 service，模型的变化往往牵动着表结构的变化，与其两头兼顾不如多聚焦模型。
我常用的做法是用例配合若干小文件 (数据忠实于模型)，保证库操作临门一脚前所有环节都是正确的，同时方便适应变化。一旦出现异常，也比较容易定位是否是数据库操作引发的问题。 (此点基于，我在工作中发现，项目型程序员大多是先急于把表结构定义出来，好像不这么做，写代码就不踏实)

针对需求的管理问题，WL 同学提到的点也非常关键：

程序员也应该更积极主动一些, 最好能推动事情发展, 当这件事情由你推动时，主动权就在你的手里了。

感谢同学们的精彩留言。在下一个模块中，我将为大家分享“沟通反馈”这个原则的具体应用。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

答疑解惑 | 如何分解一个你不了解的技术任务？

在“任务分解”这个模块，我以测试为核心，讲解了任务分解这个原则，同时也给你介绍了一些最佳实践，帮助你更好地理解任务分解的重要性，以及应该怎样分解任务。

同学们对任务分解这个原则大多是表示认同的，但就一些具体应用的场景，还是提出了自己的问题。

在今天的答疑中，我选择了几个非常典型的问题来进行深入讨论。

问题 1：面对不了解的技术，我该如何分解任务？

pyhhou 同学提到

很想听听老师的意见，就是在一个自己不熟悉的，充满未知的项目中该怎么更好地进行任务分解？
——《11 | 向埃隆·马斯克学习任务分解》

shniu 同学提到

想请问一下老师，面对探索型的需求，调研型的需求如何做任务分解呢？
——《15 | 一起练习：手把手带你分解任务》

这是一个很好的问题。在这个模块讨论开发中的任务分解时，我说的都是确定了解的某项技术，比如，数据库、REST 服务等等，因为这是开发中最常见的场景，也是最基础的能力，连熟悉的技术都做不好分解，就别说不熟悉的技术了。

那如果不了解这项技术呢？答案很简单，先把它变成你熟悉的技术。一旦变成了你熟悉的技术，你就可以应用在这个模块中学到的，面对确定性技术的分解方案。

我知道，这个答案你并不满意。其实，你真正的问题是，怎么把它变成你熟悉的技术。

我的答案是，做一次技术 Spike。这里之所以用英文，是因为我没有找到一个特别合适的词来翻译。Spike 这个词的原意是轻轻地刺，有人把它翻译成调研，我觉得是有些重了。

Spike 强调的重点在于快速地试，和调研的意思不太一样。既然是快速地试，就要在一定的时间内完成，比如，五人天，也就是一个人一周的时间，再多就不叫 Spike 了。一些简单的技术，用一天时间做 Spike 就差不多了。

这里强调的重点在于，要做一次技术 Spike。Spike 的作用就在于消除不确定性，让项目经理知道这里要用到一项全团队没有人懂的技术，需要花时间弄清楚。

项目经理比你更担心不确定性，你清楚地把问题呈现在他面前，项目经理是可以理解的，他更害怕的是，做到一半你突然告诉他，项目进度要延期。

把事情做在前面，尽早暴露问题，正是我们要在下一个模块要讨论的一个主题。

好，那么接下来的问题变成了：怎么做技术 Spike 呢？

这里，我假设你已经通过各种渠道，无论是新闻网站，还是技术 blog，又或是上级的安排，对要用的技术有了一些感性的认识，至少你已经知道这项技术是干什么的了。

接下来，我们要进入到技术 Spike 的任务分解。

首先，快速地完成教程上的例子。稍微像样点的技术都会有一个教程，跟着教程走一遍，最多也就是半天的时间。之所以要快速地完成教程上的例子，是为了让你有一个直观的认识，这时候，你对这项技术的认识就会超过新闻网站的报道。

其次，我们要确定两件事：这项技术在项目中应用场景和我们的关注点。

技术最终是要应用到项目中的，本着“以终为始”的原则，我们就应该奔着结果做，整个的 Spike 都应该围绕着最终的目标做。

很多程序员见到新技术都容易很兴奋，会把所有的文档通读一遍。如果是技术学习，这种做法无可厚非，但我们的目标是做 Spike，快速地试，没有那么多时间，必须一切围绕结果来。

项目中的场景有无数，我们需要选择最重要的一个场景，而针对着这项最重要的场景，我们还要从这项技术无数功能中选取最需要的几个，而不是“满天撒网”。

再有是我们要找准关注点，比如，采用新的缓存中间件是为了提高性能，那关注点就是性能，采用新的消息队列是为了提升吞吐，那关注点就是吞吐。我们选用一项新技术总是有自己的一些假设，但这些假设真的成立吗？这是我们需要验证的。

无论场景，还是关注点，我们要在前面先想清楚，其目的就是为了防止发散。当时间有限时，我们只能做最重要的事，这也是我在专栏中不断强调的。

确定好场景和关注点，接下来，我们要开发出一个验证我们想法的原型了。这个原型主要目的就是快速地验证我们对这项技术的理解是否能够满足我们的假设。开发一个只有主线能力的原型，对大部分程序员来说并不难，这里就不赘述了。

当你把想法全部验证完毕，这项技术就已经由一项不熟悉的技术变成了熟悉的技术。我们前面的问题也就迎刃而解了。这时候，你就可以决定，对于这项技术，是采纳还是放弃了。

但是，我这里还有一点要提醒，当你确定要使用这项技术时，请丢弃掉你的原型代码。

你或许会说，我辛辛苦苦写了几天的代码就这么丢了？是的，因为它是原型，你需要为你的项目重新设计。

如果顺着原型接着做，你可能不会去设计，代码中会存在着大量对这项技术直接依赖的代码，这是值得警惕的，所有第三方技术都是值得隔离的。这是我们会在“自动化”模块讨论的内容。

问题 2：项目时间紧，该怎么办？

在这个模块里，我花了大量的篇幅在讲测试，很多同学虽然认同测试的价值，却提出了开发中普遍存在的一些情况。

玄源 同学提到

很多时候，项目时间很紧，经常会提测后，再补测试，或者直接 code review，测试就不写了。
——《12 | 测试也是程序员的事吗？》

这是一个非常典型的问题，我在之前做咨询的时候，经常会遇到很多团队说，项目时间紧，所以，他们没有时间做测试。

这里面有一个非常经典误区：混淆了目标与现状。目标是应该怎么做，现状是我们正在怎么做。我们都知道现状是什么样的，问题是，你对现状满意吗？如果每个人都对现状是满意的，就不会有人探索更好的做法。

假设现在不忙了，你知道该怎么改进吗？

遗憾的是，很多人根本回答不了这个问题，因为忙是一种借口，一种不去思考改进的借口。

我之所以要开这个专栏，就是为了与大家探讨行业中一些好的做法。

回到这个具体问题上，我们在专栏开始就在讲以终为始，首先要有一个目标，专栏中介绍的各种实践都可以成为你设置目标的参考。有了这个目标再来考虑，如何结合我们工作的现状来谈改进。

接下来，我们以测试为例，讨论一下具体的改进过程。用我们专栏最初讲过的思考框架看一下，假如我们的现状是团队之前没什么自动化测试，而我们的目标是业务代码 100% 测试覆盖。如果要达成这个目标，我们需要做一个任务分解。

这时你会发现，分解的过程主要需要解决两方面的问题，一个是与人的沟通，另一方面是自动化的过程。

与人的沟通，就是要与团队达成共识。关于这点，你可以尝试将专栏里讲到的各种最佳实践以及其背后的逻辑，与团队进行沟通，也可以把专栏文章分享给他们。

再来，我们考虑一下自动化的改进，因为我们的现状是没什么测试，所以，不能强求一步到位，只能逐步改进。下面我给出了一个具体的改进过程：

把测试覆盖率检查加入到工程里，得到现有的测试覆盖率。

将测试覆盖率加入持续集成，设定当前测试覆盖率为初始值。测试覆盖率不达标，不许提交代码。

每周将测试覆盖率调高，比如，5% 或 10%，直到测试覆盖率达到 100%。

这样，我们就找到了一条由现状通往目标的路径，接下来，就是一步一步地具体实施了，由团队成员逐步为已有代码补充测试。

问题 3：多个功能同时开发，怎么办？

妮可 同学提到

公司经常存在有两个需求同时开发的情况。请问老师所在的团队如何解决单分支上线不同步的情况呢？
——《14 | 大师级程序员的工作秘笈》

在主分支开发模型中，有一些常见的解决多功能并行开发的方法，其中，Feature Toggle 是最常用的一个，也就是通过开关，决定哪个功能是对外可用的。

关于这一点，Y024 同学也补充了一些信息。

Feature toggle（功能开关）分享两篇文章：
1. Feature Toggles (aka Feature Flags)
2. 使用功能开关更好地实现持续部署

不过，如果用户故事划分得当，你可以很快完成一个完整的业务需求。实际上，Feature Toggle 只是一个非常临时的存在。但如果你在一个遗留系统上工作，一个功能要跨越很长的周期，Feature Toggle 才显得很有用。

额外补充一个与主分支开发模型相关的常用技术，如果你想对遗留系统做改造，传统的做法是，拉出一个分支。

如果在一个分支上怎么做呢？可以考虑采用 Branch by Abstraction，简言之，再动手改造之前，先提取出来一个抽象，把原先的实现变成这个抽象的一个实现，然后，改造的过程就是提供这个抽象的一个新实现。这种做法对设计能力有一定要求，所以，对很多团队来说，这是一个挑战。

好，今天的答疑就到这里，请你回想一下，你在工作中是否也遇到过类似的问题呢？你又是怎么解决的呢？欢迎大家在留言区写下你的想法。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

20 | 为什么世界和你的理解不一样

从今天起，我们要开启一个新的模块：沟通反馈。

如果看到沟通反馈几个字，你就以为我打算在这里教一些谈话技巧，那你还真的想错了。

在这个模块里，我打算与你讨论的主题是，生活在真实世界中。沟通反馈和生活在真实世界这两个话题是怎么联系到一起的呢？请听我慢慢道来。

《大富翁》里的沙隆巴斯有句口头禅：人生不如意，十之八九！但是不知道你有没有想过这样的一个问题，为什么人生如此不如意？如果这是一篇鸡汤文，我应该告诉你世事艰辛。但我要说的是，真实的原因往往是因为你想得太美好，用我们做软件的例子来看一下：

在我们的愿望中，做出来的产品应该一举成名，现实却是惨淡经营；

在我们的愿望中，产品经理给出的需求应该是清晰明了的，现实却是模模糊糊；

在我们的愿望中，写出来的代码，应该是快捷无错的，维护也很容易，现实却是 bug 百出，越修改，修改的时间就越长；

在我们的愿望中，你给我布置任务，我应该迅速地理解到关键，现实却是做出来的与你的目标根本就是天差地别；

……

为什么会这样？欢迎来到真实世界，真实世界不是以美好愿望驱动的，它有着自己的运行规律。虽然我们都生活在同一个世界中，但每个人理解世界的方式确实是千差万别。

我们努力地学习各种知识，为的就是更好地理解这个世界的运作方式，而沟通反馈，就是我们与真实世界互动的最好方式。

你也许会好奇，为什么我们对世界的理解会出现偏差？接下来，让我们一起用一个信息论的视角看一下。

一个信息论视角的解释

1948 年，克劳德·香农（Claude Elwood Shannon）在《贝尔系统技术学报》（Bell System Technical Journal）上发表了一篇论文《通信的数学原理》（A Mathematical Theory of Communication），这是现代信息论的开端。我们程序员熟知的通信、数据压缩、密码学、自然语言处理等诸多领域都有信息论的身影。

我们这里要借鉴的是香农信息论中的一个通信模型，如下图所示：

这个图中包含了几个要素：

信源（Information Source），它负责产生信息（Message）。

发送器（Transmitter），它会对信息进行某些操作，也就是对信息编码，产生信号（Signal）。

信道（Channel），它是信号传送的媒介。

接收器（Receiver），它是对信号执行发送器的逆操作，解码信号，提取出信息。

信宿（Destination），它负责接收信息。

当然，图中还有一个因素叫做噪声（Noise），指的是削弱信号的东西。不过，它并不是我们这里讨论的重点，我们暂时忽略它。

我们用一个实际工作中的例子来理解一下这个过程。假设你的项目经理来给你布置一项工作，在这里，项目经理就是一个信源。他的想法就是他的消息，他要把这件事告诉你，先要在大脑中做一次编码，转换成语言表达出来。他说出来的这段话就是信号。

比如，这个信号是“完成一个需求”。这段话通过信道，也就是空气传播到你耳朵里，接收到这段话之后，你会按照自己对这段话的理解进行解码，作为信宿的你，形成了自己的想法，这就是你接到的消息，整个过程就完成了。

我们来看一下，理解偏差是怎么产生的。

项目经理给你传输的信号是“完成一个需求”，在项目经理脑子中，这个信号的原始信息可能是这样的：编写完成这个功能所需的代码，然后为这段代码写好自动化测试，再将它与现有系统集成好，通过测试人员的验证。

而在学习这个专栏之前，你从“完成一个需求”这个信号中解码出来的信息却是：把功能代码写完。这样，问题就出现了。即便这里忽略了噪声的干扰，当编码和解码不是一个版本的时候，无论如何，项目经理的信息都很难准确地传达到你这里。

这就是人们往往对世界产生误解的原因。

信息的传达要经过编码和解码两个过程，无论是编码出现问题，还是解码出现问题，都会造成信息的不准确。

一方面，有些人表达不清楚，一件简单的事，他说了半天，你依然是云里雾里。这就相当于，信源发出的信息经过编码得到的信号已经不准确了。

另一方面，就像听一些技术演讲，人家说得很清楚，但因为自己没有相关背景，依然无法得知人家表达的信息。这就相当于信号虽然准确，但我们没有对应的解码装置，信号无法转成有效信息。

再有就是像前面这个例子，收发双方编解码器不配套，同样的信号得到的信息截然不同，信息传达的目的也不能很好地完成。

有了理论做基础，我们就容易理解世界为什么总和我的理解不一样，这就是编解码的过程出了问题。因为每个人经历见识的差异，造成了各自编解码器的差异。世界是同一个世界，每个人看到的却是千姿百态。

如果想在这个真实的世界中生活得更幸福一些，我们能做点什么呢？那就是改善我们的编解码器。怎么改善自己的编解码器呢？这就是“沟通反馈”这个模块要讨论的内容。

改善编解码

站在改善编解码效果的角度，我们要考虑哪些问题呢？

首先，我们要考虑一下编码器的效果。换句话说，当我们想把信息传达给别人的时候，我们得把信息编码成一个有效的信号，至少要保证在我们这里信息不丢失。

我举个例子，有一次，我在客户现场做咨询，客户的一个程序员给我介绍他们的系统，他讲了二十分钟，我还是听得一头雾水。于是，我打断他，花了五分钟用我的语言给他讲了一遍，然后问他，“你想说的是不是这个意思？”。他猛劲点头，“就是这样的。”

为什么会这样呢？究其原因就是，他上来就在给我讲实现细节，完全没有任何铺垫。

要知道，我是来了解情况的，所以，我的背景知识肯定是不足的，凭空理解这些细节是非常困难的一件事。从沟通的角度上看，这么做浪费了大量的时间，因为在过程中，我要不断地让他给我补充这些缺失的背景。这几乎是很多程序员讲东西的通病：讲东西直奔细节。

我在面试中也经常遇到过类似的情况，一些候选人上来就给我讲技术细节，我对他做过的系统一无所知，所以，我只好打断他，让他先把背景给我介绍一下。

同样，很多人抱怨别人不能理解自己，其实，首先应该想的问题是，自己到底有没有把话说清楚。这就是编码器出现问题的情况。

其次，我们还要考虑一下解码器的效果，也就是说，当一个信号呈现在我们面前时，作为接收者，我们是否能够有效地解码信息。

著名作家王小波曾经讲过一个花剌子模信使的故事，说的是中亚古国花剌子模有一个奇怪的风俗，凡是给君王带来好消息的信使，就会得到提升，给君王带来坏消息的人则会被送去喂老虎。如此一来，谁还敢把坏消息带给君王呢？但问题是，君王不听坏消息，坏消息就不存在了吗？

这就相当于解码器出了问题，过滤掉了很多真实的信息。但真实世界就是真实世界，它不会按照人们的美好愿望运行。

再举一个我们身边的例子，能做程序员的人，大多是很聪明的人， 当几个人一起讨论问题时，别人往往刚开了个头，他就认为自己已经理解了别人的想法，然后开始表达自己的观点。信息都不全，何谈解码。所以，开发团队的讨论中常常出现一个人高谈阔论，却离题万里的情况。

我们要想让自己更好地工作生活，就必须接纳真实世界的反馈，而接纳真实世界的反馈，一是需要我们打开自己的接收器，把信号接纳进来，让反馈进来，这是解码的前提；二是扩展见识，提升自己解码器的效果，更好地理解别人要表达的内容到底是什么。

说了编码器和解码器可能出现的问题，我们再来看另外一个可能造成影响的问题：编解码器算法，也就是怎么协调沟通双方更有效地进行沟通。

既然前面已经说了算法不够好会影响到信息的传递，那接下来的问题就是怎样找到一个好的算法。其实，我们从始至终在讲的各种最佳实践就是一个个好的算法，帮助我们改善沟通的效果。

还是回到前面提到“完成一个需求”的例子，我们在“以终为始”模块已经讲过了，通过制定“完成的定义”就可以帮助改善这个过程。这就相当于，沟通的双方都有了一个编解码手册。

当“完成一个需求”这样的信号发出时，作为接收方，你的解码动作就变成了，先要查一下手册里，关于“完成一个需求”的标准动作都有哪些。于是，你对事情的估计也就不会再那么简单了。

在“沟通反馈”这个模块下，我还会给你介绍各种“算法”，也就是最佳实践，帮你在工作中提高“信息”传递的效率。

回到我们这部分主题上，沟通反馈就是改善编码、解码以及算法的方式。无论是“发送”得更清楚，还是“接收”得更明白，抑或是通过各种协调算法，都是为了让通信的双方做好准备。

总结时刻

人生不如意，十之八九，之所以很多人有如此多的不如意，很大原因在于我们对真实世界有着很多不切实际的幻想，美好的愿望并不能驱动这个世界，在软件开发中也是如此。虽然人和人生活在一个世界中，但对世界的理解却是千差万别的。

我们借用了信息论的一个通信模型解释为什么每个人看到的世界会有如此大的差异，其核心就在于，人和人拥有不同的编解码器。想要在这个真实世界中生活得更幸福一些，需要我们不断地改善自己的编解码器。

改善编解码，需要从几个角度着手，分别是：编码器，让信息能输出更准确；解码器，减少信号过滤，改善解码能力；还有编解码算法，也就是各种来自行业的“最佳实践”，协调沟通的双方。

如果今天的内容你只能记住一件事，那请记住：通过沟通反馈，不断升级自己的编解码能力。

最后，我想请你回想一下，你在工作中遇到过哪些因为沟通反馈不畅造成的问题呢？欢迎在留言区写下你的想法。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

21 | 你的代码为谁而写？

关于“沟通反馈”的话题，我准备从代码开始讲起，毕竟我们程序员是靠代码与机器进行沟通的。

写代码是每个程序员的职责，程序员们都知道要把代码写好。但究竟什么叫写好呢？每个人的理解却是各有差异。

编写可维护的代码

初涉编程的程序员可能觉得能把功能实现出来的代码，就是好代码，这个阶段主要是基本功的学习，需要掌握的是各种算法、数据结构、典型的处理手法、常用的框架等等。

经过一段时间工作，日常工作所需的大多数代码，在你看来都是不在话下的。尤其像搜索和问答网站蓬勃发展之后，你甚至不需要像我初入职场时那样，记住很多常见的代码模式，现在往往是随手一搜，答案就有了。

再往后，更有追求的程序员会知道，仅仅实现功能是不够的，还需要写出可维护的代码。于是，这样的程序员就会找一些经典的书来看。

我在这方面的学习是从一本叫做《程序设计实践》（The Practice of Programming）的书开始的，这本书的作者是 Brian Kernighan 和 Rob Pike，这两个人都出身于大名鼎鼎的贝尔实验室，参与过 Unix 的开发。

写出可维护的代码并不难，它同样有方法可循。今天，我们用写代码中最简单的一件事，深入剖析怎样才能写出可维护的代码，这件事就是命名。

命名难题

计算机科学中只有两大难题：缓存失效和命名。
—— Phil Karlton

这是行业里流传的一个经典说法，无论是哪本写讲代码风格的书，都会把命名放在靠前的位置。

估计你开始写程序不久，就会有人告诉你不要用 a、b、c 做变量名，因为它没有意义；步入职场，就会有人扔给你一份编程规范，告诉你这是必须遵循的。

不管怎样，你知道命名是很重要的，但在你心目中，合格的命名是什么样的呢？

想必你知道，命名要遵循编码规范，比如：Java 风格的 camelCase，常量命名要用全大写。

但是，这类代码规范给出的要求，大多是格式上的要求。在我看来，这只是底线，不应该成为程序员的追求，因为现在很多编码规范的要求，都可以用静态检查工具进行扫描了。

我们的讨论要从名字的意义说起。作为程序员，我们大多数人理解为什么要避免起无意义的名字，但对于什么样的名字是有意义的，每个人的理解却是不同的。

名字起得是否够好，一个简单的评判标准是，拿着代码给人讲，你需要额外解释多少东西。

比如，我们在代码评审中会看到类似这样的场景：

评审者：这个叫 map 的变量是做什么用的？
程序员：它是用来存放账户信息的，它的键值是账户 ID，值就是对应的账户信息。
评审者：那为什么不直接命名成 accounts？

你知道评审者给出的这个建议是什么意思吗？如果不能一下子意识到，遇到类似的问题，你可能会和这个程序员一样委屈：这个变量本来就是一个 map，我把它命名成 map 怎么了？

变量的命名，实际上牵扯到一个重要问题，代码到底是给谁写的？

代码为谁而写？

任何人都能写出计算机能够理解的代码，只有好程序员才能写出人能够理解的代码。
—— Martin Fowler

代码固然是程序员与机器沟通的重要途径，但是，机器是直白的，你写的代码必须是符合某种规则的，这一点已经由编译器保证了，不符合规则的代码，你想运行，门都没有。

所以，只要你的代码是符合语言规则的，机器一定认。要让机器认，这并不难，你写得再奇怪它都认。行业里甚至有专门的混乱代码比赛。比如，著名的 IOCCC（The International Obfuscated C Code Contest，国际 C 语言混乱代码大赛）。

但是，我们写代码的目的是与人沟通，因为我们要在一个团队里与人协同工作。

与人沟通，就要用与人沟通的方式和语言写代码。人和机器不同，人需要理解的不仅是语言规则，还需要将业务背景融入其中，因为人的目的不是执行代码，而是要理解，甚至扩展和维护这段代码。

人要负责将业务问题和机器执行连接起来，缺少了业务背景是不可能写出好代码的。

我们在“为什么世界和你理解的不一样”这篇内容中就讲过，沟通的时候，输出时的编码器很重要，它是保证了信息输出准确性的关键。

很多程序员习惯的方式是用计算机的语言进行表达，就像前面这个例子里面的 map，这是一种数据结构的名字，是面向计算机的，而评审者给出的建议，把变量名改成 accounts，这是一个业务的名字。

虽然只是一个简单的名字修改，但从理解上，这是一步巨大的跨越，缩短了其他人理解这段代码所需填补的鸿沟，工作效率自然会得到提高。

用业务语言编程

写代码的时候，尽可能用业务语言，会让你转换一个思路。前面还只是一个简单的例子，我们再来看一个。

我们用最常用的电商下单过程来说，凭直觉我们会构建一个订单类 Order。什么东西会放在这个类里呢？

首先，商品信息应该在这个类里面，这听上去很合理。然后，既然是电商的订单，可能要送货，所以，应该有送货的信息，没问题吧。再来，买东西要支付，我们会选择一些支付方式，所以，还应该有支付信息。

就这样，你会发现这个订单类里面的信息会越来越多：会员信息可能也要加进去，折扣信息也可能会加入。

你是一个要维护这段代码的人，这个类会越来越庞大，每个修改都要到你这里来，不知不觉中，你就陷入了一个疲于奔命的状态。

如果只是站在让代码运行的角度，这几乎是一个无法解决的问题。我们只是觉得别扭，但没有好的解决方案，没办法，改就改呗！

但如果我们有了看业务的视角，我们会问一个问题，这些信息都放在“订单”是合理的吗？

我们可以与业务人员交流，询问这些信息到底在什么场景下使用。这时候你就会发现，商品信息主要的用途是下单环节，送货信息是在物流环节，而支付信息则用在支付环节。

有了这样的信息，你会知道一件事，虽然我们在用一个“订单”的概念，但实际上，在不同的场景下，用到信息是不同的。

所以，更好地做法是，把这个“订单”的概念拆分了，也就有了：交易订单、物流订单和支付订单。我们原来陷入的困境，就是因为我们没有业务知识，只能笼统地用订单去涵盖各种场景。

如果你在一个电商平台工作，这几个概念你可能并不陌生，但实际上，类似的错误我们在很多代码里都可以看到。

再举个例子，在很多系统里，大家特别喜欢一个叫“用户”的概念，也把很多信息塞到了“用户”里。但实际上，在不同的场景下，它也应该是不同的东西：比如，在项目管理软件中，它应该是项目管理员和项目成员，在借贷的场景下，它应该是借款方和贷款方等等。

要想把这些概念很好地区分出来，你得对业务语言有理解，为了不让自己“分裂”，最好的办法就是把这些概念在代码中体现出来，给出一个好的名字。这就要求你最好和业务人员使用同样的语言。

如果了解领域驱动设计（Domain Driven Design，DDD），你可能已经分辨出来了，我在这里说的实际上就是领域驱动设计。把不同的概念分解出来，这其实是限界上下文（Bounded Context）的作用，而在代码里尽可能使用业务语言，这是通用语言（Ubiquitous Language）的作用。

所以，一个好的命名需要你对业务知识有一个深入的理解，遗憾的是，这并不是程序员的强项，需要我们额外地学习，但这也是我们想写好代码的前提。现在，你已经理解了，取个好名字，并不是一件容易的事。

总结时刻

代码是程序员与机器沟通的桥梁，写好代码是每个程序员的追求，一个专业程序员，追求的不仅是实现功能，还要追求代码可维护。如果你想详细学习如何写好代码，我推荐你去读 Robert Martin 的《代码整洁之道》（Clean Code），这本书几乎覆盖了把代码写好的方方面面。

命名，是写程序中最基础，也是一个程序员从业余走向专业的门槛。我以命名为基础，给你解释了写好代码的提升路径。最初的层次是编写可以运行的代码，然后是编写符合代码规范的代码。

对于命名，最粗浅的理解是不要起无意义的名字，遵循编码规范。但名字起得是否够好，主要看是否还需要额外的解释。很多程序员起名字习惯于采用面向实现的名字，比如，采用数据结构的名字。

再进一步提升，编写代码是要写出人可以理解的代码。因为代码更重要的作用是人和人沟通的桥梁，起一个降低其他人理解门槛的名字才是好名字。

实际上，我们很多没写好的程序有一些原因就是名字起错，把一些概念混淆在一起了。想起好名字，就要学会用业务语言写代码，需要尽可能多地学习业务知识，把业务领域的名字用在代码中。

如果今天的内容你只能记住一件事，那请记住：用业务的语言写代码。

最后，我想请你思考一下，想要写好代码，还有哪些因素是你特别看重的？欢迎在留言区写下你的想法。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

22 | 轻量级沟通：你总是在开会吗？

今天我们来探讨一个很多程序员日常工作中，经常碰到却会带来困扰的话题：开会。

头疼的开会

有一次，我听到两个程序员在聊天。一个资深程序员说：“还是晚上好，我可以一门心思写代码”，另一个年轻程序员不解地问：“你白天也可以写啊。”

资深程序员很无奈，“我倒是这样想，可是白天参加那么多会，哪有工夫啊！我的代码就只能加班写了。”

这段对话听上去让人有点心酸，但这种现象，确确实实广泛存在于程序员的日常工作中，尤其是你经验丰富又在一个大组织中工作，这几乎成了你的宿命。在这些程序员的认知中，开会太多影响了他们写代码。

你以为我想讨伐开会吗？并不是，开会本身并没有错，因为开会的本意是将大家组织起来解决问题。但请你回想一下，你参加的会议有多少解决了问题呢？

开会是为了解决问题，但真实情况却是开了会又没有解决多少问题，这真是一个奇特的矛盾。

回想一下，你参加过的会议里面，有没有效果特别好的呢？在我职业生涯中，凡是效果特别好的会议，基本上都是用来做信息同步的。比如，领导宣布一个事情，这种会议几乎不会浪费时间。宣布消息，大家收到消息，结束。

那效果不好的会议是什么样呢？几乎都是那些讨论会，你一言我一语，每个会几乎无一例外，都有几个擅长打岔的，这个会基本上都会跑偏，时间就会这样一分一秒地流逝了。

我给你举个例子，我之前参加过一个上线计划的评审会，这个团队的负责人要把相关利益方都召集起来，其中包括上下游可能会受影响的团队、测试、运维等等，一个不大的会议室里挤满了人。

这个负责人刚开始讲方案没几分钟，下游团队的负责人就站出来问：“这个方案为什么要这么做？我担心会对我们系统造成影响。”讲方案的人只好停下来解释。结果是越解释，细节越多，双方你来我往，一个方案评审会，就转变成一个技术讨论会了。

测试和运维的同事本来是想来听技术方案，以便为后续的工作做准备的。看着双方的讨论，一脸无奈，因为他们知道，方案没确定好，所有的事情还是下回再说吧！

怎么样？是不是很熟悉的感觉。为什么会这样？因为他们选错了沟通方式。

开会是一种重量级的沟通，几乎是我们日常工作中最重的。它有很强的仪式感，所以，大家相对来说会很重视。而且会议通常会牵扯到很多人，尤其是与这个事情相关度不那么高的人。

你可以想一下，有多少次开会，你是在精力集中的？如果你是高度集中的，那恭喜你，你是高效地参与其中。但更多时候，你可能神游天外，因为讨论的内容可能与你关系不大，或者你已经听不懂了，你坐在那里的唯一原因是，主持人还没宣布会议结束。

用开会这种重量级的方式讨论问题，就好比杀鸡用了牛刀，这是不恰当的。那该怎么解决这个问题呢？很简单，杀鸡用鸡刀。

轻量级沟通

实际上，真正在会议上能够积极参与讨论的人并不会觉得会议是浪费时间，因为高度参与其中，人是进入到心流状态的，时间流逝很快。觉得浪费时间的，往往是没有参与其中的人。

换句话说，会议之所以给人留下如此不堪的印象，一个重要的原因是，真正参与讨论的人并不多。所以，我们换个角度思考一下，只要把这些真正参与讨论的人拉到一起讨论不就好了？

所以，改善会议的第一个行动项是，减少参与讨论的人数。

有人会说，我这个讨论有好几个议题，每个议题要不同的人参与，那你要做的是，分别找这几个人专门讨论，而不是把大家放到一起。

不知道你发现没有，在讨论行动项的时候，我用的是“讨论”，而没有提到“会议”两个字。我之前说过了，会议是一种重量级的沟通方式。所以，我们会倾向于选择一种轻量级的沟通方式，比如面对面沟通，这样一来，每个人的压力就会小很多。

相比于会议的形式，面对面沟通因为注意力有限，参与的人数不可能太多。也因为参与的人数相对少一些，每个人的投入也会更多一些。

所以，我们的第二个行动项是，如果你要讨论，找人面对面沟通。

一旦理解了这些改进方式，我们就可以改进自己的行为方式。如果有一个问题需要讨论，我要做的是，分别找到相关人针对关心的主题进行讨论，然后，我把讨论的结果汇总再去征求大家意见。如果大家达成一致了，我才会选择开会。

这个时候，开会的目的不再是讨论，而是信息同步：我准备这么干了，相关各方已经同意了，知会大家一下，结束。

站立会议

我前面说过了，开会并非都是不好的，一些信息同步的会还是有必要的。

举个例子，有一种实践叫站会（Standup）。很多公司都在实践它，站会甚至成为每天的开工仪式。一般的做法是，早上大家来上班了，先开一个站会，让大家同步一下昨天的工作，然后开始今天的工作。

有的人一听到站会这个形式就会皱起眉头。如果是这样，多半是你的团队“站”错了。

你知道，这个会为什么是“站”会吗？因为按照一般人的习惯，站的时间不会太长，因为站的时间长，累啊！所以，如果站会超过 10 分钟，你的站会一定是错的。

也许你会说，这点时间恐怕不够给我们站会吧？因为每个人都有一大堆要说的。请问，你觉得其他人说那么多，你关心吗？现实是，一旦一个人说多了，跟你关系又不大，你就开始思维发散了。

所以，在总长固定的情况下，每个人发言的时间一定是有限的。在有限的时间内，你能说什么呢？我建议你只说三件事：

我昨天做了什么？

我今天打算做什么？

我在过程中遇到了什么问题，需要请求帮助。

“做了什么” ，是为了与其他人同步进展，看事情是否在计划上。一旦偏离计划，请主动把它提出，这样，项目经理可以过问，因为这会涉及到是否要调整项目计划；

“要做什么” ，是同步你接下来的工作安排。如果涉及到与其他人协作，也就是告诉大家，让他们有个配合的心理准备；

“问题和求助”， 就是与其他人的协作，表示：我遇到不懂的问题，你们有信息的话，可以给我提供一下。

这三件事都是与别人相关的，几句话快速说完，结束。因为这些事情与别人相关，所以，大家的注意力可以相对集中一些。

你或许会问，如果我的问题很复杂，需要讨论该怎么办。对不起，那是另外一件事，你可以在站会结束之后，找相关人去讨论，不要在这个会上浪费大家时间。在站会上，你只要在问题和求助中告诉大家，你有一个问题，需要相关人讨论，结束。

为了让大家保持注意力集中，我的一些团队还用过发言令牌的方式。比如，找一个毛绒玩具，谁拿到“令牌”谁发言，然后，随机地扔给一个人，一旦这个人走神，大家一下子就能发现了。

一些有趣的方式、短暂的时间，以及与所有人相关的事情，因为满足了这三点，所以普遍来说，这种站会效果还可以。

关于站会，有一个典型的错误是，有些团队把站会开成了汇报会。项目负责人指定一个个轮流发言，说的人都向负责人在汇报工作，其他人自然就容易走神了，因为事情与己无关。

还有一点你可能会有疑问，我所在的团队比较大，一个人几句话时间也会很长。

当团队很大时，更应该做的是把团队拆分了，因为你不太可能与 20 个人紧密地工作在一起。沃顿商学院曾经做过一项研究，5-12 个人是一个恰当的团队规模，每个人在其中都能发挥自己的重要作用。

总结时刻

开会是很多程序员的困扰，太多的会议甚至会影响到你工作的进展。开会的本意是为了解决问题，但实际上，大多数会议并不能很好地解决问题。因为会议是一种重量级的沟通方式，很多人参加会议时，并不能很好地参与其中。

如果你想用会议的形式与别人讨论问题，最好放弃这种打算，面对面的沟通是最好的方式。因为面对面沟通很轻，人数相对少，每个人参与度就会高很多。基于这种改进，我们可以把大部分会议都改成信息同步的会，效率就会得到提高。

我还给你介绍了一种特殊的会议：站会。之所以采用站会的方式，就是要控制时间。在站会上每个人说什么，我给了你一个建议的格式：

我昨天做了什么？

我今天打算做什么？

我在过程中遇到了什么问题，需要请求帮助。

如果你经常组织别人开会，请你想一下，是不是自己没有利用好开会这件事；如果你经常被别人组织开会，不妨把这篇文章转发给他，让他别总是开会“讨论”问题。

如果今天的内容你只能记住一件事，那请记住：多面对面沟通，少开会。

最后，我想请你思考一下，你在工作中，还遇到过哪些因为开会带来的问题呢？欢迎留言与我写下你的想法。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

23 | 可视化：一种更为直观的沟通方式

作为一个程序员，在这个技术快速发展的时代，我们唯有不断学习，才能保证自己不为时代所抛弃。那你是怎么跟上技术发展步伐的呢？

就个人经验而言，我会关注一些技术网站，最典型的就是 InfoQ。这样，我可以快速了解到技术发展的动向，比如，什么时候出了个新东西、哪个项目又有了重大的更新、某些技术有了哪些新的应用场景等等。

另外，我还有一种更系统地了解新知识的方式：ThoughtWorks 技术雷达。之所以我很喜欢这种方式，因为它是“可视化”的。

什么是技术雷达？

ThoughtWorks 技术雷达是由 ThoughtWorks 技术咨询委员会（Technology Advisory Board）编写的一份技术趋势报告，每 6 个月发布一次。ThoughtWorks 的项目多样性足够丰富，所以它能够发现诸多技术趋势。因此，相比于行业中其它的预测报告，技术雷达更加具体，更具可操作性。

ThoughtWorks 是我的老东家，所以，我在接触技术雷达的时间很早。我在 2013 年就已经开始与人讨论微服务，并在项目中尝试使用 Docker，而这一切信息的来源都是技术雷达。不过，我这里想和你讨论并不是技术雷达到底有多优秀，而是带你看看技术雷达这种组织知识的可视化形式。

（图片来源：ThoughtWorks 技术雷达）

技术雷达用来追踪技术，在雷达图的术语里，每一项技术表示为一个 blip，也就是雷达上的一个光点。

然后用两个分类元素组织这些 blip：象限（quadrant）和圆环（ring），其中，象限表示一个 blip 的种类，目前有四个种类：技术、平台、工具，还有语言与框架。

圆环表示技术一个 blip 在技术采纳生命周期中所处的阶段，目前这个生命周期包含四个阶段：采用（Adopt）、试验（Trial）、评估（Assess）和暂缓（Hold）。

每次技术雷达发布之后，我会特别关注一下“采用” 和 “暂缓”两项。

“采用”表示强烈推荐，我会去对比一下自己在实际应用中是否用到了，比如，在 2018 年 11 月的技术雷达中，事件风暴（Event Storming）放到了“采用”中，如果你还不了解 事件风暴 是什么，强烈建议你点击链接了解一下。

“暂缓” 则表示新项目别再用这项技术了，这会给我提个醒，这项技术可能已经有了更优秀的替代品，比如，Java 世界中最常见的构建工具 Maven 很早就放到了“暂缓”项中，但时至今日，很多人启动新项目依然会选择 Maven，多半这些人并不了解技术趋势。

从这几年的发展趋势来看，技术雷达在“采用”和“暂缓”这两项上给出的推荐，大部分是靠谱的。

至于“试验”和“评估”两项，有时间的时候，我会慢慢看，因为它们多半属于新兴技术的试验区，主要的作用是用来让我开拓视野的。

雷达图是一种很好的将知识分类组织的形式，它可以让你一目了然地看到并了解所有知识点，并根据自己的需要，决定是否深入了解。

所以，我的前同事们借鉴了这个形式，做出了一个程序员的读书雷达，将程序员的应该阅读的书籍做了一个整理。

（图片来源：ThoughtWorks 读书雷达）

事实上，这种将内容通过可视化方式的组织起来的形式非常好用，ThoughtWorks 鼓励每个组织都建立自己的知识雷达，甚至提供了一个工具辅助你将雷达图构建出来。

在我看来，雷达图不仅仅适用于组织，也可以适用于团队。

我也曾经按照雷达图的方式将自己的团队用到的技术组织起来。把最需要了解的技术必须放在内环，比如：一个 Java 项目。我会要求程序员了解 Java，向外扩展的就是你在这个团队内工作会逐渐接触到的技术，比如，像 Docker 这种与部署相关的知识。至于最外面一层，就是被我们放弃掉的技术，比如，Maven。

这样一来，团队成员可以更清晰地了解到团队中所用的技术。当有新人加入团队时，这个雷达可以帮助新人迅速地抓住重点，他的学习路径就是从内环向外学习。所以，我也推荐你打造自己团队的技术雷达。

构建技术雷达
构建雷达的程序库

你是否想过，为什么雷达图的形式可以帮助你更好地理解知识呢？因为人的大脑更擅长处理图像。

可视化的优势

在远古时代，人脑处理的内容大多是图像，比如，哪里有新的果实，哪里猛兽出没，文字则是很久之后才产生的。现在普遍的一种说法是，大约在公元前 3500 年左右，许多文明才刚刚发展出书写系统，相比于人类的历史来说，这几乎是微不足道的。

就人脑的进化而言，处理图像的速度远远快于处理文字，所以，有“一图胜千言”的说法。

通过创建图像、图标或动画等进行信息交流的形式，就是可视化（Visualization）。可视化有很多种不同的分类，我们最常用的应该是数据可视化和信息可视化。

我在“你的工作可以用数字衡量吗”这篇文章里说过，我上班第一件事是“看”数字，这就是典型的数据可视化，而上面介绍的技术雷达，就属于信息可视化。

很多做软件的人习惯于用文字进行沟通，一般在软件开发过程中，需要编写各种文档，但并不是所有的场景，文字都是好的沟通方式，所以，也会有很多人尝试着将可视化应用在软件开发过程中。

估计大多数程序员最熟悉的表达方式应该是流程图，如果你做过软件设计，可能还听说过 UML（统一建模语言，Unified Modeling Language）。如果使用得当，这种方式会极大地提高表达的准确性，降低其他人理解的门槛。

在日常工作中，你最熟悉的可视化方式，大概就是在纸上或白板上画的图。以我的经验看，很多人画这个图太随意，如果你也是这样，我给你一个建议，先写字后画框，这样图会显得整洁一些。

什么是看板？

我们再来看一个实践，这就是将“可视化”应用在工作中的典型案例：看板。

看板，是一种项目管理工具，它将我们正在进行的工作变得可视化。这个实践来自精益生产，前面讲精益创业时，我给介绍了“精益”这个来自丰田公司的管理理念。精益的理念在软件行业已经已经非常流行了，很多软件开发实践都是从“精益”而来，看板就是其中之一。

看板属于那种几乎是看一眼就知道怎么用的实践。它将工作分成几个不同的阶段，然后，把分解出来的工作做成一张卡片，根据当前状态放置到不同的阶段中。如果你采用了我们专栏之前讲过的用户故事，那么每个用户故事就是一张卡片。

在实际工作中，每当一个工作完成之后，它就可以挪到下一个阶段，工作怎么算完成就是由我们前面提到的 DoD 来决定的。

当然，要用好看板，还可以使用一些小技巧。比如，用不同颜色的卡表示不同类型的工作，给每个人一个头像，增添一些乐趣。

看板可以帮助你一眼看出许多问题，比如，你的团队中有 5 个人，却有 8 个正在进行的任务，那一定是有问题的。因为一个人多线程工作，效果不会好。用“精益”的术语来说，我们应该限制 WIP（Work-In-Progress）；再有，如果待开发的卡最多，那就证明现在的瓶颈在于开发，而不是其它阶段。

运用看板的方式，还有一个有趣的细节：使用实体墙还是电子墙。实体墙不难理解，就是找一面墙把看板做出来。现在有很多公司专门在做协同办公软件，其中的项目管理部分用到的就是看板理念，这就是电子墙的由来。

关于这点，顺便说一下我的建议，如果你的团队是在一起工作的，请考虑使用实体墙，除非你的办公空间实在太小。因为它可以方便地调整，也可以当做站会的集合地点，还可以让别人看见你们的工作或是问题，这样做的最大优势在于增强了人与人的互动。

电子墙的优势在于，随处可访问、数据不会丢失、便于统计等等，但每次访问它，都需要专门打开电脑，还是比较麻烦的。一种将二者结合的办法是，使用一个大电视，专门用来展示电子墙。

总之，看板就是要让工作在大家面前展现出来。

总结时刻

我给你介绍了一种结构化学习新知识的方式：技术雷达。

技术雷达就是一种将技术信息组织起来的方式。它通过将技术按照“象限”和“圆环”两个维度进行分类，让人可以直观地看到并理解不同的技术所处的发展阶段。

雷达图是一种很好的形式，不仅可以用在组织技术，还可以用来组织其它信息，比如，读书雷达。每个公司都可以利用雷达图的形式组织自己所有的技术，每个团队也可以利用雷达图的形式组织自己团队用到的技术，这样，方便团队成员结构化地理解用到技术，也方便新人的学习。

雷达图实际上是一种可视化的方法，人脑对于图像处理速度更快，因此，可视化是改善沟通的一种方式。大多数软件过程习惯采用文字的方式进行表达，对于“可视化”利用的还不够。当然，还是有一些利用“可视化”的方法，比如，流程图、UML 等。

最后，我给你介绍了一个利用可视化进行信息沟通的实践：看板。看板把工作分成了几个不同的阶段，在看板上对应不同的列，然后，每个任务作为一张卡贴在上面。每完成一张卡，就把这张卡挪到下一个阶段。

看板可以帮你发现许多问题，比如，当前进展是否合适，是否有人同时在做很多的事，发现当前工作的瓶颈等等。

如果今天的内容你只能记住一件事，那请记住：多尝试用可视化的方式进行沟通。

最后，我想请你思考一下，你在工作中，有哪些用到可视化方法解决沟通问题的场景？欢迎留言区写下你的想法。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

24 | 快速反馈：为什么你们公司总是做不好持续集成？

在“以终为始”那个模块，我们留下了一个巨大的尾巴。在“持续集成：集成本身就是写代码的一个环节”这篇文章中，我们是站在“以终为始”的角度阐述了集成，尤其是持续集成的重要性。

但怎么做好持续集成，才是很多人真正关心的内容。今天，我们就来谈谈如何做好持续集成。

既然我们打算讨论持续集成，不妨停下来先思考一个问题：你对持续集成的第一印象是什么。

持续集成？Jenkins？没错，很多人对持续集成第一印象都是持续集成服务器，也就是 CI 服务器，当年是 CruiseControl，今天换成了 Jenkins。

也正是因为如此，很多人就把 CI 服务器理解成了持续集成。我就曾经接触过这样的团队，他们恨不得把所有的事情都放在 CI 服务器上做：在 CI 服务器上做了编译，跑了代码检查，运行了单元测试，做了测试覆盖率的统计等等。

或许你会疑问，这有什么不对的吗？

在做软件这件事上，我们不会用对与错去衡量，我只能说，这种做法是可行的，但它不是最佳实践。我希望你去思考，有没有比这更好的做法呢？

想要回答这个问题，我们还是要回到持续集成的本质上去。持续集成的诞生，就是人们尝试缩短集成周期的结果。为什么要缩短周期呢？因为我们希望尽早得到反馈，知道自己的工作结果是否有效。

所以，想要做好持续集成，就需要顺应持续集成的本质：尽快得到工作反馈。

由此，我们便得到持续集成的关键点，你只要记住一句话，快速反馈。

快速反馈，这句分成两个部分，快速和反馈，这也就引出了持续集成的两个重要目标：怎样快速地得到反馈，以及什么样的反馈是有效的。

快速得到反馈

我们回到前面的例子上，把各种检查放到 CI 服务器上执行，它可以让我们知道代码是不是有问题，这是一个有效的反馈，但它的反馈够快速吗？虽然比起没有持续集成的状态，它是好很多。但是，我们需要问一个问题，能不能更快地得到反馈呢？

显然，我们还可以做得更快。在自己的开发机上执行这些检查，就会比在 CI 服务器快。也就是说，执行同样的操作，本地环境会快于 CI 服务器环境。

为什么会这样呢？我们先来看看所有检查在 CI 服务器上执行，每个程序员的动作是什么样的。

我们写好代码，然后需要提交代码，等待 CI 服务器运行检查结果，然后，用 CI 监视器查看执行结果。如果没问题，继续做下一个任务，如果有错误，修复错误，再执行同样的过程。

再来看看本地执行的动作。运行构建脚本，如果一切正确，你可以选择提交代码或是继续下一个任务，如果失败，立即修复。

对比之下，在本地运行这些检查，你不需要提交，不需要等 CI 服务器开始执行，不需要跑到额外的地方查看检查结果。所以，这个操作比提交到服务器上会快很多。

另外，这里还有一个关键点，我们的操作是连续的。一旦检查结果出错了，我们立刻进入修复环节。作为程序员，我们太了解连续操作的重要性了。这就像打游戏时，我们感觉不到时间流逝一般，有人把这种状态称之为“心流”。

而提交代码，等待 CI 服务器的检查结果，就等于强迫你停下来，你的心流就被打断了。

如果你对心流的概念感兴趣，可以去读米哈里·契克森米哈赖的著作《心流》，这位作者就是心流概念的提出者。

前面我们只是在说，你作为程序员个体，使用持续集成的效果，这只是为了简化讨论。接下来，我们向更真实的世界靠拢，引入另一个重要的因素：团队协作。

假设你的团队就是在 CI 服务器上执行检查。你兴高采烈地写完一段代码准备提交，结果，此时你隔壁的同事手快一筹，先提交了，你不得不停下来等他。如果很不幸，你同事的检查失败的话，那么他又要把它修复好，你等的时间就更长了。

一个小问题也就罢了，如果是个大问题，他可能要修很长一段时间。这个时候，你除了等待，也没有更好的选择。如此一来，大把的时间就被浪费掉了。

这里我们要“插播”持续集成中重要的一个提交纪律：只有 CI 服务器处于绿色的状态才能提交代码。有检查在运行不能提交，有错误不能提交。原因很简单，如果这个时候多个人提交了代码，检查失败了，那问题到底算谁的呢？

反之，如果一次只有一个人提交代码，责任是明确的。如果团队不大，这个纪律相对还好执行，提交之前看一眼，或是喊一声就可以了。

如果团队稍微有一点规模，可以用一个小东西当作令牌，谁拿到了谁来提交。如果真的有人在 CI 服务器还在运行的时候，提交了代码怎么办？很简单，谁提交谁负责，错了就他修，谁让他违反纪律了。

好，你已经理解了我说的重点：不能把检查只放到 CI 服务器上执行。那该怎么做呢？答案已经呼之欲出了，那就是在本地开发环境上执行。

想做好持续集成的一个关键点是，用好本地构建脚本（build script），保证各种各样的检查都可以在本地环境执行。

一旦有了构建脚本，你在 CI 服务器上的动作也简单了，就是调用这个脚本。也就是说，本地检查和 CI 服务器上的动作是一致的。

至于什么样的内容适合放在构建脚本里，这个话题我们先放一放，把它留到后续“自动化”模块再做讨论。

在“任务分解”模块中，我与你讨论了“小”动作在工作中的重要性，“小”动作完成得越快，工作反馈得到也越快，所以说，也只有坚持不懈地做“小”动作，才能缩短反馈周期。

现在我们把这个道理与持续集成结合起来理解，我们的工作流程就变成了这样：

每完成一个任务，在本地运行构建脚本，如果有问题，就修复；没问题，则可以同步代码。如果 CI 服务器上没有正在运行的服务，就可以提交代码了。

提交代码中最麻烦的动作，其实是合并代码。不过，因为我们做的是小任务，改动的代码量并不大，所以，即便有需要合并的代码，量也不会很大，所需的脑力以及工作时间都会少很多。如此一来，我们的开发效率才可能能真正得到提高。

当团队真正地实施起持续集成，你会发现随着时间增加，本地检查的时间会越来越长。原因有很多，比如，代码越来越多，测试也越来越多。总之，检查的时间长了，就会对集成的速度造成影响。

这个时候，本着快速反馈的理念，我们就必须想办法。比如，有的团队做了分布式测试运行，有的团队将测试分类，就是我们在测试金字塔中讲到的分类，在本地执行单元测试和集成测试，而把更复杂的系统测试放到 CI 服务器上运行。

简单来说，我们的目的就是快速地得到反馈。

得到有效的反馈

说完了“快速”，我们再来看看做好持续集成的第二个重点：反馈，也就是怎么得到有效的反馈。

为什么需要反馈，道理很简单，我们得知道自己做得对不对。你可能会问，根据前面的说法，如果本地和 CI 服务器上执行的是一样的脚本，我在本地通过了，还用关心 CI 服务器的反馈吗？

当然要。因为还会出现很多其他问题，比如说最简单的一种情况是，你漏提交了一个文件。

好，既然我们要关注持 CI 服务器的反馈，下一个问题就是，它怎么反馈给我们呢？

我们还是从一种常见的错误入手。有些团队做持续集成用的反馈方式是什么呢？答案是邮件。

以邮件进行反馈，问题出在哪里呢？很明显，邮件不是一种即时反馈的工具。

我不知道有多少人会把邮件客户端当作日常的工具，就我个人习惯而言，一天查看几次邮件就算不错了，如果以邮件作为反馈方式，很有可能是出错了很长时间，我都无知无觉。

我们前面一直在强调快速，需要的是即时反馈，一旦邮件成了持续集成链条中的一环，无论如何都快不起来。

那你可以怎么做呢？在前面各种讨论中，我其实已经透露了答案：持续集成监视器，也是 CI 监视器。

（图片来源：CI 监视器的示例 projectmonitor）

CI 监视器的原理很简单，CI 服务器在构建完之后，会把结果以 API 的方式暴露出来，早期有 RSS 和 ATOM 格式，后来有 JSON 的格式。得到的结果就可以用不同的方式进行展现了。市面上有很多 CI 监视器的软件，有的是拿到结果之后，做一个视觉呈现，有的是做桌面通知。

现在，我们终于要讲到这个部分的重点了：怎么呈现是有效的？

答案很简单：怎么引人注目，怎么呈现。

比如，很多团队的做法是，用一个大屏幕将持续集成的结果展示出来，这样一来，持续集成的结果所有人都能看到，一旦出错了，即便你一时疏忽，也会有人来提醒你。

还有一些感官刺激的做法，比如，有人用上了红绿灯，测试失败则红灯闪烁；还有人甚至配上了语音，用喇叭高喊：“测试失败了，请赶紧修复。”我在一个视频里见过一个更夸张的做法：有人用玩具枪，出错了，就瞄准提交者开上一枪。

你是聪明的程序员，你应该能想到更多有趣的玩法。

为什么要这么做呢？这里的重点是，想做好持续集成，需要整个团队都关注持续集成。

这些引人注目的做法，就是要提高持续集成的关注度。否则，即便持续集成的技术环节做得再出色，人的注意力不在，持续集成也很难起到作用。

所以，你看到了，持续集成的反馈，尤其是出错之后的反馈方式，几乎是所有实践中最为高调的，它的目的就是要引人注目。

这里再插播一条持续集成的纪律：CI 服务器一旦检查出错，要立即修复。原因很简单，你不修，别人就不能提交，很多人的工作就会因此停顿下来，团队的工作流就会被打断，耽误的是整个团队的工作。

如果你一时半会修不好怎么办，撤销你的提交。更关键的原因是，团队对于持续集成的重视度，长时间不修复，持续集成就失去了意义，人们就会放弃它，持续集成在你的项目中，也就发挥不出任何作用了。

总结时刻

持续集成是软件开发中的重要实践，做好持续集成的关键在于，快速反馈。这里面有两个目标，怎样快速地得到反馈，以及什么样的反馈是有效的。

做好快速反馈，要把本地能做好的事情，在本地做好；也要通过小步提交的方式，加快代码开发的节奏。什么是有效的反馈？一是即时的反馈，二是引人注目的反馈。有很多种持续集成相关的工具可以帮助我们达成有效的反馈。

想要做好持续集成，还要有一些纪律要遵循：

只有 CI 服务器处于绿色的状态才能提交代码；

CI 服务器一旦检查出错，要立即修复。

如果今天的内容你只能记住一件事，那请记住：做好持续集成的关键在于，快速反馈。

最后，我想请你分享一下，你的团队做持续集成吗？遇到过哪些困难呢？欢迎留言与我们分享。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

25 | 开发中的问题一再出现，应该怎么办？

看过《圣斗士星矢》的同学大多会对其中的一个说法印象颇深：圣斗士不会被同样的招数击败两次。

我们多希望自己的研发水平也和圣斗士一样强大，可现实却总不遂人愿：同样的线上故障反复出现，类似的 Bug 在不同的地方一再地惹祸，能力强的同学每天就在“灭火”中消耗人生。我们难道就不能稍微有所改善吗？

如果在开发过程中，同样的问题反复出现，说明你的团队没有做好复盘。

什么是复盘？

复盘，原本是一个围棋术语，就是对弈者下完一盘棋之后，重新把对弈过程摆一遍，看看哪些地方下得好，哪些下得不好，哪些地方可以有不同甚至是更好的下法等等。

这种把过程还原，进行研讨与分析的方式，就是复盘。

现如今，复盘的概念已经被人用到了很多方面，比如，股市的复盘、企业管理的复盘，它也成为了许多人最重要的工具，帮助个体和企业不断地提升。这其中最有名的当属联想的创始人柳传志老爷子，他甚至把“复盘”写到了联想的核心价值观里。

为什么复盘这么好用呢？在我看来有一个重要的原因，在于客体化。

俗话说，当局者迷，旁观者清。以我们的软件开发作为例子，在解决问题的时候，我们的注意力更多是在解决问题本身上，而很少会想这个问题是怎么引起的。

当你复盘时，你会站在另外一个视角，去思考引起这个问题的原因。这个时候，你不再是当事者，而变成了旁观者。你观察原来那件事的发生过程，就好像是别人在做的一样。你由一个主观的视角，变成了一个客观的视角。

用别人的视角看问题，这就是客体化。

在软件开发领域，复盘也是一个重要的做法，用来解决开头提到那些反复出现的问题，只不过，它会以不同的方式呈现出来。

回顾会议

回顾会议是一个常见的复盘实践，定期回顾是一个团队自我改善的前提。回顾会议怎么开呢？我给你分享我通常的做法。

作为组织者，我会先在白板上给出一个主题分类。我常用的是分成三类：“做得好的、做得欠佳的、问题或建议”。

还有不同的主题分类方式，比如海星图，分成了五大类：“继续保持、开始做、停止做、多做一些、少做一些”五类。

分类方式可以根据自己团队的喜好进行选择。我之所以选用了三类的分类方式，因为它简单直观，几乎不需要对各个分类进行更多的解释。

然后，我会给与会者五分钟时间，针对这个开发周期内团队的表现，按照分类在便签上写下一些事实。比如，你认为做得好的是按时交付了，做得不好的是 Bug 太多。

这里面有两个重点。一个是写事实，不要写感受。因为事实就是明摆在那里的东西，而感受无法衡量，你感觉好的东西，也许别人感觉很糟糕。

另外，每张便签只写一条，因为后面我要对便签归类。因为大家是分头写的，有可能很多内容是重复的，所以，要进行归类。

五分钟之后，我会号召大家把自己写的便签贴到白板上。等大家把便签都贴好了，我会一张一张地念过去。

这样做是为了让大家了解一下其他人都写了些什么，知道不同人的关注点是什么。一旦有哪一项不清楚，我会请这张便签的作者出来解释一下，保证大家对这个问题的理解是一致的。在念便签的同时，我就顺便完成了便签归类的工作。

等到所有的便签都归好类，这就会成为后续讨论的主题，与会者也对于大家的关注点和看到的问题有了整体的了解。

做得好的部分，是大家值得自我鼓励的部分，需要继续保持。而我们开回顾会议的主要目的是改善和提升，所以，我们的重点在于解决做得不好的部分和有问题出现的地方。

在开始更有针对性的讨论之前，我会先让大家投个票，从这些分类中选出自己认为最重要的几项。我通常是给每人三票，投给自己认为重要的主题。每个人需要在诸多内容中做出取舍，你如果认为哪一项极其重要，可以把所有的票都投给这个主题。

根据大家的投票结果，我就会对所有的主题排出一个顺序来，而这就是我们要讨论的顺序。我们不会无限制的开会，所以，通常来说，只有最重要的几个主题才会得到讨论。

无论是个人选择希望讨论的主题，还是团队选择最终讨论的主题，所有人都要有“优先级”的概念在心里。然后，我们就会根据主题的顺序，一个一个地进行讨论。

讨论一个具体的主题时，我们先会关注现状。我会先让写下反馈意见的人稍微详细地介绍他看到的现象。比如，测试人员会说，最近的 Bug 比较多，相比于上一个开发周期，Bug 增加了 50%。

然后，我会让大家分析造成这个现象的原因。比如，有人会说，最近的任务量很重，没有时间写测试。

再下来，我们会尝试着找到一个解决方案，给出行动项。比如，任务重，我们可以让项目经理更有效地控制一下需求的输入，再把非必要的需求减少一下；测试被忽略了，我们考虑把测试覆盖率加入构建脚本，当测试覆盖率不足时，就不允许提交代码。

请注意，所有给出的行动项应该都是可检查的，而不是一些无法验证的内容。比如，如果行动项是让每个程序员都“更仔细一些”，这是做不到的。因为“仔细”这件事很主观，你说程序员不仔细，程序员说我仔细了，这就是扯皮的开始。

而我们上面给出的行动项就是可检查的，项目经理控制输入的需求，我们可以用工作量衡量，还记得我们在讨论用户故事中提到的工作量评估的方式吗？

控制工作量怎么衡量？就是看每个阶段开发的总点数是不是比上一个阶段少了。而测试覆盖率更直接，直接写到构建脚本中，跑不过，不允许提交代码。

好，列好了一个个的行动项，接下来就是找责任人了，责任人要对行动项负责。

比如，项目经理负责需求控制，技术负责人负责将覆盖率加入构建脚本。有了责任人，我们就可以保障这个任务不是一个无头公案。下一次做回顾的时候，我们就可以拿着一个个的检查项询问负责人任务的完成情况了。

5 个为什么

无论你是否采取回顾会议的方式进行复盘，分析问题，找到根因都是重要的一环。

你的团队如果能一下洞见到根因固然好，如果不能，那么最好多问一些为什么。具体怎么问，有一个常见的做法是：5 个为什么（5 Whys）。这种做法是丰田集团的创始人丰田佐吉提出的，后来随着丰田生产方式而广为人知。

为什么要多问几个为什么？因为初始的提问，你能得到的只是表面原因，只有多问几个为什么，你才有可能找到根本原因。

我给你举个例子。服务器经常返回 504，那我们可以采用“5 个为什么”的方式来问一下。

为什么会出现 504 呢？因为服务器处理时间比较长，超时了。

为什么会超时呢？因为服务器查询后面的 Redis 卡住了。

为什么访问 Redis 会卡住呢？因为另外一个更新 Redis 的服务删除了大批量的数据，然后，重新插入，服务器阻塞了。

为什么它要大批量的删除数据重新插入呢？因为更新算法设计得不合理。

为什么一个设计得不合理的算法就能上线呢？因为这个设计没有按照流程进行评审。

问到这里，你就发现问题的根本原因了：设计没有经过评审。找到了问题的原因，解决之道自然就浮出水面了：一个核心算法一定要经过相关人员的评审。

当然，这只是一个例子。有时候，这个答案还不足以解决问题，我们还可以继续追问下去，比如，为什么没有按流程评审等等。

所以，“5 个为什么”中的“5”只是一个参考数字，不是目标。

“5 个为什么”是一个简单易上手的工具，你可能听了名字就知道该怎么用它。有一点需要注意的是，问题是顺着一条主线追问，不能问 5 个无关的问题。

无论是“回顾会议”也好，“5 个为什么”也罢，其中最需要注意的点在于，不要用这些方法责备某个人。我们的目标是想要解决问题，不断地改进，而不是针对某个人发起情感批判。

总结时刻

在软件研发中，许多问题是反复出现的，很多开发团队会因此陷入无限“救火”中，解决这种问题一个好的办法就是复盘。

复盘，就是过程还原，进行研讨与分析，找到自我改进方法的一个方式。这种方式使我们拥有了客体化的视角，能够更客观地看待曾经发生过的一切。这种方法在很多领域中都得到了广泛的应用，比如股市和企业管理。

在软件开发中，也有一些复盘的实践。我给你详细介绍了“回顾会议”这种形式。

无论哪种做法，分析问题，找到根因是一个重要的环节。“5 个为什么”就是一个常用的找到根因的方式。

如果今天的内容你只能记住一件事，那请记住：定期复盘，找准问题根因，不断改善。

最后我想请你分享一下，你的团队是怎么解决这些反复出现的问题呢？欢迎在留言区写下你的做法。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

26 | 作为程序员，你也应该聆听用户声音

在前面的专栏内容中，我们讨论过几次与产品经理的交流：你应该问问产品经理为什么要做这个产品特性，要用 MVP（最小可行产品）的角度，衡量当前做的产品特性是不是一个好的选择。

但还有一个问题可能困扰着我们：怎么判断产品经理说的产品特性是不是用户真的需要的呢？

很多时候，产品经理让你实现一个产品特性，你感觉这么做好像不太对，却又说不出哪不对，想提出自己的看法，却不知道从哪下手。之所以会遇到这样的问题，一个重要的原因就是，你少了一个维度：用户视角，你需要来自真实世界的反馈。

吃自家的狗粮

产品经理无论要做什么，他都必须有一个立足的根基：为用户服务。所以，如果你了解了用户怎么想，你就有资本判断产品经理给出的需求，是否真的是用户需要的了。

而作为一个程序员，欠缺用户视角，在与产品经理的交流中，你是不可能有机会的，因为他很容易用一句话就把你打败：“这就是用户需求。”

很多程序员只希望安安静静地写好代码，但事实上，对于大多数人来说，安安静静是不太可能写好代码的，只有不断扩大自己的工作范围，才可能对准“靶子”。

今天我们讨论的角度，就是要你把工作范围扩大，由听产品经理的话，扩大成倾听用户的声音。

作为程序员，你应该听说过一个说法“Eat your own dog food”（吃自家的狗粮）。这个说法有几个不同的来源，都是说卖狗粮的公司真的用了自家的狗粮。

从 1988 年开始，这个说法开始在 IT 行业流行的，微软的保罗·马瑞兹（Paul Maritz）写了一封“Eating our dog food”的邮件，提到要“提高自家产品在内部使用的比例。”从此，这个说法在微软迅速传播开来。

如今，自己公司用自己的产品几乎成了全行业的共识。抛开一些大公司用这个说法做广告的因素，不断使用自家的产品，会让你多出一个用户的视角。

在挑毛病找问题这件事上，人是不需要训练的，哪里用着不舒服，你一下子就能感受到。所以，不断地使用自家产品，你自己就是产品的用户，这会促使你不断去思考怎么改进产品，再与产品经理讨论时，你就自然而然地拥有了更多的维度。

比如，前面在讨论 MVP 时，我曾经讲过一个我做 P2P 产品的经历。在这个项目中，我就作为用户在上面进行了一些操作。当自己作为用户使用时，就发现了一些令人不爽的地方。

比如，一开始设计的代金券只能一次性使用，如果代金券金额比较大，又没那么多本金，只能使用代金券的一部分，就会让人有种“代金券浪费了”的感觉。

于是，我就提出是不是可以把代金券多次使用。很快，产品就改进了设计。这种改进很细微，如果你不是用户，只从逻辑推演的角度是很难看到这种差异的。

当你吃不到狗粮时

不过，不是每家公司的产品都那么“好吃”。“吃自家狗粮”的策略对于那些拥有“to C”产品的公司来说，相对是比较有效的。但有时候，你做的产品你根本没有机会用到。

我曾经与很多海外客户合作过，我做的很多产品，自己根本没有机会使用。比如，我做过五星级酒店的审计平台。除了能对界面上的内容稍微有点感觉之外，对于使用场景，我是完全不知道的。

如果没有机会用到自己的产品，我们该怎么办呢？我们能做的就是尽可能找机会，去到真实场景里，看看用户是如何使用我们软件的。

比如，做那个酒店审计平台时，我就和客户一起到了一家五星级酒店，看着他们怎样一条一条地按照审计项核查，然后把审计结果登记在我们的平台上。

那些曾经只在写程序时见到的名词，这回就活生生地呈现在我眼前了。后来再面对代码时，我看到就不再是一个死板的程序了，我和产品经理的讨论也就更加扎实了。

有的团队在这方面有比较好的意识，会主动创造一些机会，让开发团队成员有更多机会与用户接触。

比如，让开发团队到客服团队轮岗。接接电话，听听用户的抱怨，甚至是谩骂。你会觉得心情非常不好，但当你静下来的时候，你就会意识到自己的软件有哪些问题，如果软件做得不好，影响会有多大。

这时，你也就能理解，为什么有的时候，很多业务人员会对开发团队大发雷霆了，因为他们是直接面对用户“炮火”的人。

我们为什么要不断地了解用户的使用情况呢？因为用户的声音是来自真实世界的反馈。不去聆听用户声音，很容易让人自我感觉良好。还记得在 “为什么世界和你的理解不一样” 中，我们提到的那个只接收好消息的花剌子模国国王的例子吗？

我们要做一个有价值的产品，这个“价值”，不是对产品经理有价值，而是要对用户有价值。华为总裁任正非就曾经说过，“让听得见炮声的人来做决策。”

我们做什么产品，本质上不是由产品经理决定的，而是由用户决定的。只有听见“炮声”，站在一线，我们才更有资格判断产品经理给出的需求是否真的是用户所需。

当产品还没有用户时

如果你的团队做的是一个新的产品，还没有真正的用户，那又该怎么办呢？你可以尝试一下“用户测试”的方法。

之前我做过一个海外客户的项目。因为项目处于启动阶段，我被派到了客户现场。刚到那边，客户就兴高采烈地告诉我，他们要做一个用户测试，让我一起参加。当时，我还有点不知所措，因为我们的项目还没有开始开发，一个什么都没有的项目就做用户测试了？是的，他们只做了几个页面，就开始测试了。

站在今天的角度，我前面已经给你讲过了精益创业和 MVP，你现在理解起来就会容易很多。是的，他们就是要通过最小的代价获取用户反馈。

他们是怎么做测试的呢？首先是一些准备工作，找几个普通用户，这些人各有特点，能够代表不同类型的人群。准备了一台摄像机，作为记录设备，拍摄用户测试的全过程。还准备了一些表格，把自己关注的问题罗列上去。

然后，就是具体的用户测试了。他们为用户介绍了这个测试的目的、流程等一些基本信息。然后，请用户执行几个任务。

在这个过程中，测试者会适时地让用户描述一下当时的感受，如果用户遇到任何问题，他们会适当介入，询问出现的问题，并提供适当的帮助。

最后，让用户为自己使用的这个产品进行打分，做一番评价。测试者的主要工作是观察和记录用户的反应，寻找对用户使用造成影响的部分。做用户测试的目的就是看用户会怎样用这个网站，这样的网站设计会对用户的使用有什么影响。

当天测试结束之后，大家一起整理了得到的用户反馈，重新讨论那些给用户体验造成一定影响的设计，然后调整一版，再来做一次用户测试。

对我来说，那是一个难忘的下午，我第一次这么近距离地感受用户。他们的关注点，他们的使用方式都和我曾经的假设有很多不同。后面再来设计这个系统时，我便有了更多的发言权，因为产品经理有的角度，我作为开发人员也有。

最后，我还想说一个程序员常见的问题：和产品经理没有“共同语言。”

因为他们说的通常是业务语言，而我们的口中基本上是计算机语言。这是两个领域的东西，很难互通。前面在讨论代码的时候，我提到要用业务的语言写代码，实际上，这种做法就是领域驱动设计中的通用语言（Ubiquitous Language）。

所谓通用语言，不只是我们写代码要用到，而是要让所有人说一套语言，而这个语言应该来自业务，来自大家一起构建出的领域模型。

这样大家在交流的时候，才可能消除歧义。所以，如果你想让项目顺利进行，先邀请产品经理一起坐下来，确定你们的通用语言。

总结时刻

今天我们讨论了一个重要的话题：倾听用户声音。这是开发团队普遍欠缺的一种能力，更准确地说，是忽略的一种能力。所以，“吃自家的狗粮”这种听上去本来是理所当然的事情，才被反复强调，成为 IT 行业的经典。

在今天这一讲，我给你介绍了“了解用户需求”的不同做法，但其归根结底就是一句话，想办法接近用户。

无论是自己做用户，还是找机会接触已有用户，亦或是没有用户创造用户。只有多多听取来自真实用户的声音，我们才不致于盲目自信或是偏颇地相信产品经理。谁离用户近，谁就有发言权，无论你的角色是什么。

如果今天的内容你只能记住一件事，那请记住：多走近用户。

最后，我想请你思考一下，在你的实际工作中，有哪些因为走近客户而发现的问题，或者因为没有走近客户造成的困扰呢？欢迎在留言区写下你的想法。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

27 | 尽早暴露问题： 为什么被指责的总是你？

今天我准备讨论一个经常会让很多程序员郁闷的事情，为什么你已经工作得很辛苦了，但依然会被指责。在讨论这个问题之前，我们先来讲一个小故事。

程序员小李这天接到了一个新的任务。系统要做性能提升，原先所有的订单都要下到数据库里，由于后来有很多订单都撤了，反复操作数据库，对真正成交过程的性能造成了影响。所以，技术负责人老赵决定把订单先放到缓存里。

这就会牵扯到一个技术选型的问题，于是，老赵找了几个可以用作缓存的中间件。为了给大家一个交代，老赵决定让小李给这几个中间件做一个测试，给出测试结果，让大家一起评估。小李高兴了，做这种技术任务最开心，可以玩新东西了。

老赵问他：“多长时间可以搞定？”

小李说：“一个星期吧！”

老赵也很爽快，“一个星期就一个星期。不过，我得提个要求，不能是纯测中间件，得带着业务跑。”

“没问题。”小李一口答应下来。老赵怕小李做多了，还特意嘱咐他，只测最简单的下单撤单环节就好。

等真的开始动手做了，小李发现，带着业务跑没那么容易，因为原来的代码耦合度太高，想把新的中间件加进去，要先把下单和撤单环节隔离开来。而这两个操作遍布在很多地方，需要先做一些调整。

于是，小李只好开始不分白天黑夜地干起来。随着工作的深入，小李越发觉得这个活是个无底洞，因为时间已经过半了，他的代码还没调整完。

这时，老赵来问他工作进展，他满面愁容地说，估计干不完了。老赵很震惊，“不就是测试几个中间件吗？”

小李也一脸委屈，“我们为啥要带着业务跑啊？我这几天的时间都在调整代码，以便能够把中间件的调用加进去。”

老赵也很疑惑，“你为啥要这么做？”

“你不是说要带着业务跑吗？”

“我是说要带着业务跑啊！但你可以自己写一个下单撤单的程序，主要过程保持一致就好了。”小李很无奈，心里暗骂，你咋不早说呢？

是啊！你咋不早说呢？不过，我想说不是老赵，而是小李。

谁知道有问题？

我们来分析一下问题出在哪。在这个故事里，小李和老赵也算有“以终为始”的思维，在一开始就确定了一个目标，做一个新中间件测试，要带着业务跑。

小李可以说是很清楚目标的，但在做的过程中，小李发现了问题，原有代码很复杂，改造的工作量很大，工作可能没法按时完成。

到此为止，所有的做法都没有错。但接下来，发现问题的小李选择了继续埋头苦干，直到老赵来询问，无奈的小李才把问题暴露出来。

在老赵看来，这并不是大事，调整一下方案就好了。但是小李心生怨气，在他看来，老赵明明有简单方案，为啥不早说，害得自己浪费了这么多时间。

但反过来，站在老赵的角度，他是怎么想的呢？“我的要求是带着业务跑，最理想的方案当然是和系统在一起，你要是能搞定，这肯定是最好的；既然你搞不定，退而求其次，自己写一个隔离出来的方案，我也能接受。”

你看出来问题在哪了吗？老赵的选择没有任何问题，问题就出在，小李发现自己可能搞不定任务的时候，他的选择是继续闷头做，而不是把问题暴露出来，寻求帮助。

作为一个程序员，克服技术难题是我们工作的一个重要组成部分，所以，一旦有困难我们会下意识地把自己投入进去。但这真的是最好的做法吗？并不是，不是所有的问题，都是值得解决的技术难题。

在工作中遇到问题，这简直是一件正常得不能再正常的事儿了，即便我们讲了各种各样的工作原则，也不可避免会在工作中遇到问题。

既然是你遇到的问题，你肯定是第一个知道问题发生的人，如果你不把问题暴露出来，别人想帮你也是有心无力的。

如果老赵不过问，结果会怎么样？必然是小李一条路跑到黑。然后，时间到了，任务没完成。

更关键的是，通常项目计划是一环套一环的，小李这边的失败，项目的后续部分都会受到影响，项目整体延期几乎是必然的。这种让人措手不及的情况，是很多项目负责人最害怕见到的。

所以，虽然单从小李的角度看，这只是个人工作习惯的事，但实际上，处于关键节点的人可能会带来项目的风险。而小李的问题被提前发现，调整的空间则会大很多。

遇到问题，最好的解决方案是尽早把问题暴露出来。其实，这个道理你并不陌生，因为你在写程序的时候，可能已经用到了。

Fail Fast

写程序有一个重要的原则叫 Fail Fast，这是什么意思呢？就是如果遇到问题，尽早报错。

举个例子，我做了一个查询服务，可以让你根据月份查询一些信息，一年有 12 个月，查询参数就是从 1 到 12。

问题来了，参数校验应该在哪做呢？如果什么都不做，这个查询参数就会穿透系统，传到你的数据库上。

如果传入的参数是合法的，当然没有任何问题，这个查询会返回一个正常的结果。但如果这个参数是无意义的，比如，传一个“13”，那这个查询依然会传到数据库上。

事实上，很多不经心的系统就是这么做的，一旦系统出了什么状况，你很难判断问题的根源。

在这个极度简化的例子里，你可以一眼看出问题出在输入参数上，一旦系统稍具规模，请求来自不同的地方，这些请求最终都汇集到数据库上，识别来源的难度就会大幅度增加。尤其是系统并发起来，很难从日志中找出这个请求的来源。

你可能会说，“为了方便服务对不同数据来源进行识别，可以给每个请求加上一个唯一的请求 ID 吧？”

看，系统就是这么变复杂的，我经常调侃这种解决方案，就是没有困难创造困难也要上。当然，即便以后真的加上请求 ID，理由也不是现在这个。

其实，要解决这个问题，做法很简单。稍微有经验的人都知道，参数校验应该放在入口的位置上，不合法的请求就不让它往后走了。这种把可能预见的失败拦在外面的做法就是 Fail Fast，有问题不可怕，让失败尽早到来。

上面这个例子很简单，我再给你举一个例子。如果配置文件缺少了一个重要参数，比如，缺少了数据库最大连接数，你打算怎么处理？很多人会选择给一个缺省值，这就不是 Fail Fast 的做法。既然是重要参数，少了就报错，这才叫 Fail Fast。

其实，Fail Fast 也有一些反直觉的味道，很多人以构建健壮系统为由，兼容了很多奇怪的问题，而不是把它暴露出来。反而会把系统中的 Bug 隐藏起来。

我们都知道，靠 debug 来定位问题是最为费时费力的一种做法。所以，别怕系统有问题，有问题就早点报出来。

顺便说一下，在前面这个例子里，透传参数还有几个额外的问题。一是会给数据库带来额外的压力，如果有人用无意义查询作为一种攻击手段，它会压垮你的数据库。再有一点，也是安全问题，一些 SQL 攻击，利用的就是这种无脑透传。

克服心理障碍

对我们来说，在程序中尽早暴露问题是很容易接受的。但在工作中暴露自己的问题，却是很大的挑战，因为这里还面临着一个心理问题：会不会让别人觉得自己不行。

说实话，这种担心是多余的。因为每个人的能力是强是弱，大家看得清清楚楚。只有你能把问题解决了大家才会高看你，而把问题遮盖住，并不能改善你在别人心目中的形象。

既然是问题，藏是藏不住的，就像最开始那个故事里的小李，即便他试图隐藏问题，但最后他还是不可能完成的，问题还是会出来，到那时，别人对他的评价，只会更加糟糕。

比起尽早暴露问题，还有更进一步的工作方式，那就是把自己的工作透明化，让别人尽可能多地了解自己的工作进展，了解自己的想法。

如果能做到这一点，其他人在遇到与你工作相关的事情，都会给你提供信息，帮助你把工作做得更好。当然，这种做法对人的心理挑战，比尽早暴露问题更大。

从专栏开始到现在，我们讲了这么多原则和实践，其实，大多数都是在告诉你，有事先做。

一方面，这是从软件变更成本的角度在考虑；另一方面，也是在从与人打交道的角度在考虑。

越往前做，给人留下的空间和余地越大，调整的机会也就越充足。而在最后一刻出现问题的成本实在太高，大到让人无法负担。

总结时刻

我们今天讨论了一个重要的工作原则，把事情往前做，尽早暴露问题。我们前面讲的很多内容说的都是这个原则，比如，要先确定结果，要在事前做推演等等。越早发现问题，解决的成本就越低，不仅仅是解决问题本身的成本，更多的是对团队整体计划的影响。

一方面，事前我们要通过“以终为始”和“任务分解”早点发现问题；另一方面，在做事过程中，一旦在有限时间内搞不定，尽早让其他人知道。

这个原则在写程序中的体现就是 Fail Fast，很多程序员因为没有坚持这个原则，不断妥协，造成了程序越来越复杂，团队就陷入了无尽的泥潭。

原则很简单，真正的挑战在于克服自己的心理障碍。很多人都会下意识地隐瞒问题，但请相信你的队友，大家都是聪明人，问题是藏不住的。

如果今天的内容你只记住一件事，那请记住：事情往前做，有问题尽早暴露。

最后，我想请你回想一下，如果遵循了这样的工作原则，你之前犯过的哪些错误是可以规避掉的呢？欢迎在留言区写下你的想法。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

28 | 结构化：写文档也是一种学习方式

你写文档吗？我知道，你可能并不喜欢写文档，因为在你眼中，写文档是繁琐的，是旧时代软件工程的产物。

最开始我对写文档的印象也不好。

我的职业生涯是从一个通过了 CMM 5 级认证的大企业开始的。可能今天很多程序员已经对 CMM 感到陌生了，它是能力成熟度模型（Capability Maturity Model for Software）的缩写，用来评估一个组织的软件开发能力，曾在国内风靡一时，许多软件公司都以拥有 CMM 认证为努力方向。

在这个极其重视过程的企业里，文档是非常重要的一环。但我看到的真实场景却是，一个软件已经上线运行了，大家才开始为了应付过程纷纷补写文档。

每个部门都有专门的过程负责人，要求你严格按照格式写文档，保证字体字号的正确性。然后，用 A4 纸将文档打印出，封印在一个仓库里，再也无人问津。

然而，文档却是非常重要的。后来，我到过很多公司，凡是我能够比较快上手的，通常都是有比较详尽的文档，而那些文档缺失的公司，想要把信息梳理清楚，往往会花很长时间。

另外，我学习很多软件开发的相关知识，通常也是依赖各种各样的文档。对我们程序员这个走在时代前列的群体来说，大量阅读文档就是我们日常工作的一部分。

你发现矛盾了吗？一方面，我们讨厌写文档，另一方面，文档却对我们的工作学习有着不可忽视的作用。

我们竟然如此依赖于一个我们讨厌的东西。问题出在哪呢？

你为什么不喜欢写文档？

很多人会说，自己不愿意写那些无聊的流程文档，文档无聊，这固然是一个原因。不过，如今很多公司已经在这方面做得相当轻量级了，基本上只要求写必要的文档。那为什么依然有很多人不愿意写文档呢？

其实，很多人回避写文档的真正原因是，他掌握的内容不能很好地结构化。

在两种场景下，我们扮演的角色是不同的。写文档时，角色是作者；而读文档时，角色是读者。

作为读者，我们读文档，实际上就是按照作者梳理的结构在走，因为呈现出来的内容，多数是已经结构化的，读起来自然会比较顺畅；而作为作者，没有人告诉你结构应该是什么样，我们必须创造出一个结构来，而这正是很多人不擅长的。

想要成为一个好程序员，有一个良好的知识结构是极其重要的。

很多人抱怨程序员行业难，原因就在于，新技术层出不穷。是的，当你的知识都是零散的，任何新技术的出现，都是新东西。而当你建立起自己的知识结构，任何新东西都只是在原有知识上的增量迭加。

举个例子，今天炒得沸沸扬扬的微服务，小粒度的理念脱胎于 Unix 哲学中的“只做一件事，把它做好”，而服务化的理念则是当年 SOA（Service-Oriented Architecture）的产物。理解了这些背后的动机，微服务就只剩下工具层面的问题。

有了这样的知识结构，当我要构建应用时，只是需要把工具适配进去，到时我再来学习相应的知识，这是非常有针对性的，学习的效率也会得到大幅度提高。

将零散的知识结构化，有很多种方式，但输出是非常关键的一环。

知识输出

不知道你小时候是不是有过给同学讲题的经历，有时候，明明你已经将知识学得很好，但给同学讲解起来时，却总是讲不明白。因为你的同学总能从你想都没想过的角度问问题，这些角度和老师教的不一样。

输出的过程，本质上就是把知识连接起来的过程。自己以为自己懂的东西，当你真的需要把它按照一个完整的逻辑呈现出来时，那些缺失的细节就会冒出来，而补齐这些细节，一张知识地图就逐渐成型了。

这个模块的主题是“沟通反馈”，将知识对外输出就是一种获得反馈的方式。很多人自以为对知识的理解已经很深入了，但给别人一讲，却发现自己怎么也讲不清楚，这就说明他理解的程度，远未到达他以为的高度。

输出的方式有很多，对于程序员来说，最常接触到的两种应该是写作与演讲。

你读到很多书、很多技术文章，这都是别人通过写作的方式进行输出的结果。而很多技术大会上，常常会有各路高手在台上分享自己的所得，这就是演讲的输出方式。

软件行业的很多大师级程序员都是对外输出的高手。比如，开源概念的提出者 Eric Raymond，他的《大教堂与集市》推开了开源大门；前面多次提及的 Kent Beck，他写了《极限编程解析》、《测试驱动开发》、《实现模式》几本书；

而 Martin Fowler，几乎是对外输出的典范，他重新整理了很多似是而非的概念，让人们的讨论有了更标准的词汇，比如，重构、依赖注入（Dependency Injection）等等。

再往前，就要提到《计算机程序设计艺术》的作者高德纳，他系统地整理了算法的概念，为了好好写作，他甚至创造了一个排版软件 TeX。

也许你会说，说得很有道理，但我真的不擅长啊！这是因为你没有掌握基本的方法。

金字塔原理

首先，需要明确一点，我们的第一目标不是成为作家或演讲家，而只是要求把事情说清楚，把自己的知识清晰地呈现出来。那我们最好先来了解一下金字塔原理。看看下面这张图，你就知道它是怎么回事了：

首先，我们要确定想要表达的是什么，也就是找到中心论点，然后，再确定支撑这个论点的分论点，再来就是找到支撑每个分论点的论据。

从中心论点、分论点至论据，这样一层层向下展开，从结构上看，就像金字塔一样，所以，这个方法称之为金字塔原理。

以我们的专栏为例，我们的中心论点就是“高效工作是有方法可循的”，那支撑起这个中心论点的分论点就是我们的四个原则，针对每个原则，我们给出了各种实践和思想，这是我们的论据。

前面我说过了，一个人不擅长输出，更多的是因为缺乏知识的结构化，现在通过这样一种方式，就可以帮助自己，将某个知识结构化起来，有了结构，剩下的就是怎么输出了。

具体怎么输出就可以根据自己的喜好进行选择：要么自上而下的进行表达，也就是先说中心论点，然后说分论点 1，用论据证明分论点 1，再说分论点 2，用论据证明分论点 2，以此类推。

或者是自下而上来表达，先用证据得出分论点 1，然后再得出分论点 2，最后再归纳总结出中心论点。

听上去很简单，但不要以为懂得了金字塔原理，天下就尽在掌握了，你还需要更多的练习。

无他，唯手熟尔

我自己也曾经很不擅长写作和公开演讲，但是，这些东西都禁不住你大量的练习。我的对外输出，是从我刚开始工作不久开始的。那时候，市面上流行写 blog，我抱着好奇的心态开始了自己的 blog 之旅。

刚开始写 blog 的时候，我会把写好的东西分享给周边的朋友，他们会给我提出一些反馈，有赞许、有调侃、也有针对一些细节的讨论，这会让我觉得自己写的东西是有人看的，我也就有了坚持的原动力。

我也很羡慕那些很会写的人，于是，也经常会模仿他人的手法不断地改进自己的写作技巧。慢慢地，我的读者就从身边的人逐渐扩展开来，我也就有了更多的反馈。

正是这些反馈，让我对很多东西有了全新的认识，也就有了更强的分享动力，一个正向循环逐渐建立起来。到后来，写东西就成了我的习惯，坚持至今。

经过 blog 写作的锻炼，我写的东西有了自己的章法和套路，也就有了越来越多机会去在不同的地方写东西：给杂志写稿子，在网站上写东西，包括今天这个专栏，都起源于最初的 blog 写作。

除此之外，随着时间的累积，我收获的不仅仅是一些读者的赞许，还得到了更多的机会，比如，我人生中的第一次公开演讲，机会就来自于我 blog 的一个读者的邀请。

后来的一些职业机会，也是通过我写 blog 认识的朋友。考虑到我当时人在 IT 边缘的东北，能有后来的职业发展，很大程度都是常年坚持对外输出的结果。

同样，演讲能力也需要大量的练习。1977 年《Book of List》杂志曾经有一个关于“最恐惧事物”的调查，结果显示，公开演讲名列第一，超过了死亡。所以，你害怕公开演讲是很正常的。

我至今依然记得我第一次公开演讲时手抖的样子，今天想想还是挺傻的。我第一次在几百人的大会上做演讲，居然有一段时间，只顾着看大屏，背对着听众，也是很糗的一段经历。

我一直很羡慕那些在台上侃侃而谈的人，比如，乔布斯。直到我读了《乔布斯的魔力演讲》，我才知道，即便强如乔布斯，他的演讲也是经过大量练习的。

我自己公开演讲看上去正常一些，是我在经过一个咨询项目的大量练习之后。那时候，几乎每天要给客户讲东西，害得我只能不停地准备、不停地讲。所以，本质上，对演讲的惧怕只是因为练习不足。

好了，你现在已经了解获取这些技能的真谛了，无他，唯手熟尔！

总结时刻

程序员对文档有着一种矛盾的情感，一方面，需要依赖于文档获得知识，另一方面，很少有人愿意写文档。

文档在程序员心目中“形象不佳”，主要是传统的流程写了太多无用的文档。但对更多人来说，不愿意写文档，本质上是因为知识不能很好地结构化。

有结构的知识会让新知识的学习变得更加容易，今天很多人抱怨新知识层出不穷，就是因为知识过于零散，当知识有结构之后，学习新知识就只是在学习增量，效率自然就会大幅度提升。

输出是一种很好的方式，帮助你把知识连接起来，写作和做公开演讲都是很好的输出方式。

阻碍很多人进行知识输出的一个重要原因是缺乏输出的模型，金字塔原理就给出一个从中心论点到分论点，再到论据的模型，帮助我们将知识梳理出来。

而想要做好知识输出，还需要不断地进行练习，写作和做公开演讲都是可以通过练习提高的。

如果今天的内容你只能记住一件事，那请记住：多输出，让知识更有结构。

最后，我想请你分享一下，你的工作中，有哪些机会将自己的知识输出呢？欢迎在留言区写下你的想法。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

划重点 | 一次关于“沟通反馈”主题内容的复盘

你好，我是郑晔，恭喜你，又完成了一个模块的学习。

在“沟通反馈”这个模块中，我与你探讨了与人打交道的一些方法，只不过，这并非是传统意义上的谈话技巧。而是希望你能克服自己的心理障碍，主动与真实世界进行沟通，获取反馈，让自己对信息的编解码能力不断得到提升。

重点复习

在这个模块中，我们学习到了一些最佳实践。

看板

一种来自精益生产的可视化实践。

按阶段将任务放置其中。

可以帮助我们发现问题。

持续集成

做好持续集成的关键是，快速反馈。

本地检查通过之后再提交。

找到有效的反馈方式，比如：CI 监视器。

持续集成的纪律。

只有 CI 服务器处于绿色的状态才能提交代码。

CI 服务器一旦检查出错，要立即修复。

回顾会议

软件团队复盘的一种实践。

枚举关注点，选出重点，深入讨论，列出行动项，找到负责人。

5 个为什么

又一个来自丰田的实践。

沿着一条主线追问多个问题。

在这个模块中，我们还了解一些重要的思路，让我们把工作做得更好。

用信息论理解沟通反馈

写代码的进阶路径

编写可以运行的代码。

编写符合代码规范的代码。

编写人可以理解的代码。

用业务语言写代码。

会议是一种重量级的沟通方式

减少参会人数。

找人面对面沟通。

聆听用户声音

能做自己用户，做自己的用户。

能接近用户，接近用户。

没有用户，创造用户。

Fail Fast

一种编写代码的原则。

出现问题尽早报错。

金字塔原理

从中心论点，到分论点，再到论据。

实战指南

在“沟通反馈”的模块，我也将每篇内容浓缩为一句实战指南，现在一起回顾一下。

通过沟通反馈，不断升级自己的编解码能力。
——《20 | 为什么世界和你的理解不一样》

用业务的语言写代码。
——《21 | 你的代码为谁而写？》

多面对面沟通，少开会。
——《22 | 轻量级沟通：你总是在开会吗？》

多尝试用可视化的方式进行沟通。
——《23 | 可视化：一种更为直观的沟通方式》

做好持续集成的关键在于，快速反馈。
——《24 | 快速反馈：为什么你们公司总是做不好持续集成？》

定期复盘，找准问题根因，不断改善。
——《25 | 开发中的问题一再出现，应该怎么办？》

多走近用户。
——《26 | 作为程序员，你也应该聆听用户声音》

事情往前做，有问题尽早暴露。
——《27 | 尽早暴露问题： 为什么被指责的总是你？》

多输出，让知识更有结构。
——《28 | 结构化：写文档也是一种学习方式》

额外收获

在这个模块的最后，针对大家在学习过程中的一些问题，我也进行了回答，帮你梳理出一个思路，更好地理解学到的内容：

持续集成是一条主线，可以将诸多实践贯穿起来。

从持续集成到稳定的开发分支，到频繁提交，足够小的任务，到任务分解。

从持续集成到可检查，到测试防护网，到测试覆盖率，到单元测试，到可测试代码，到软件设计。

安全性检查，是回顾会议的前提条件。

在信息获取上，国内外程序员差别不大，开拓视野，改善工作习惯，是国内程序员亟需提高的。

——《答疑解惑 | 持续集成，一条贯穿诸多实践的主线》

留言精选

在讲到定期复盘，找准问题根因时，西西弗与卡夫卡 同学提到：

关于复盘，孙陶然曾经说过，如果他有所成就，一半要归功于复盘。他提出了几个步骤供大家参考。首先，先对比实际结果和起初所定目标之间有什么差距。其次，情景再现，回顾项目的几个阶段。然后，对每个阶段进行得失分析，找出问题原因。最后，总结规律，化作自己的技能沉淀，再次遇到时可以规避。

我再补充一点，复盘资料应该记录到知识库，无论新来的或是接手的人，都能从中获益，从而提升组织的能力。另外，好的复盘需要有坦诚的文化氛围，不然有可能变成互相指责甩锅，就失去了意义。

另外，西西弗与卡夫卡 同学还分享了提升开会效率的方法：

其他一些提升开会效率的方法，比如会前每个人要先做准备，把观点写下来，然后发给主持人。再比如六顶思考帽，大家按相近的思考角度讨论，而不是我说一趴，你说另一趴。还有，主持人控制这轮谁能发言，控制每个人的时长。方法很多，但实际上总有人破坏规则，特别是当这个人是老板…

在用信息论来讨论沟通反馈问题时，毅 同学将知识点融会贯通，提出了自己的心得：

不同角色间的沟通：克服上下文差异，分段解码，理解偏差早发现早反馈。相同角色间的沟通，信号相同，解码能力因人而异，要有一个主导的人，控制沟通广度与深度，抓主线适可而止，此时结合任务分解，反向沙盘推演。

关于如何做好复盘，like_jun 同学提到：

要让团队认识到复盘的重要性。
让每个人都深入思考项目运作过程中遇到了哪些问题。才能做好复盘。

在讲到通过金字塔原理进行知识输出时，Y024 同学丰富了金字塔原理的基本原则，具体如下：

金字塔原理的四个基本原则：“结论先行”（一次表达只支持一个思想，且出现在开头）、“以上统下”（任一层次上的思想都必须是其下一层思想的总结概括）、“归类分组”（每组中的思想都必须属于同一范畴）和“逻辑递进”（每组中的思想都必须按照逻辑顺序排列）。

前面两个特点是纵向结构之间的特点，后面两个特点则是横向结构之间的特点。以上内容收集整理自李忠秋老师的《结构思考力》，感兴趣的小伙伴可以看看。

另外，对于会议，Y024 同学也提出了他团队正在进行的摸索和尝试：

1. 沟通的指导原则之一就是在同步沟通的时候（比如开会），人越少越好。而在异步沟通的时候（比如 E-mail），涉及的听众越多越好。

2. 关于开会分享下我们正在摸索的。
（a）每个会开始前，会议发起人在石墨文档上以“会议记录”模版（我们持续形成自己的模版）新建一个纪要：说明议程、及讨论内容等前提内容并提前告知与会人员。会议过程中在同一个石墨文档上做纪要，保证纪要可以收集全所有的笔记和行动计划。如果是关联会议，则使用上次相关的石墨文档进行追加内容（保持事件连贯性、完整性）。
（b）半小时的会议设置为 25 分钟，一小时的会议设置成 50 分钟，留有冗余量应付需要换地方等临时情况，保证所有的会议不会有成员迟到的现象。

对于领域驱动设计，小浩子 同学提到了要特别关注可变项和不变项的分离：

领域驱动设计确实是写出合适的代码结构的一项训练，程序员会不由自主地按照自己的习惯，也就是按照计算机运行逻辑去设计代码，这样的代码很容易陷入难以维护的坑。在开始动手写代码之前跟用户交流清楚，理解设计的概念、流程、使用场景、特殊情况，这些都很重要。另外我特别关注的一点是可变项和不变项的分离，因为我们的业务场景对可扩展性要求很高。

经验越丰富的程序员，越能体会到“走进客户”的重要性，关于这一点，David Mao 同学提到：

我做了好多年的软件测试，前几年和销售一起去谈客户，才深深地体会到客户声音的重要性。客户关注的才是真需求，产品经理和开发想出来的很多是伪需求，很多不是客户想要的功能。

感谢同学们的精彩留言。在下一个模块中，我将为大家分享“自动化”这个原则的具体应用。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

用户故事 | 站在前人的肩膀上，领取属于你的高效工作秘籍

丁雪丰
极客时间《玩转 Spring 全家桶》专栏作者，平安壹钱包高级架构师

1. 为什么订阅《10x 程序员工作法》？

我与郑晔老师是多年好友，不过大家平时联系并不多，去年在一次聚会上得知他要在极客时间上开专栏，当时并没有去了解专栏的内容，直到专栏上线后我才知道，这门课就是《10x 程序员工作法》。

从专栏名称上就能大概得知，这门课程的目的主要是为了提升大家的工作效率，开篇词中有一句大实话：

大部分程序员忙碌解决的问题，都不是程序问题，而是由偶然复杂度导致的问题。

正所谓，前人的经验，我们的阶梯，我想看看作为一个超级资深的从业者，郑晔老师是怎么看待这个问题的，他又是怎么总结自己的心得的。要知道他曾经是 Thoughtworks 的首席咨询师，他帮这么多公司做过各种项目，一定是有秘籍的。因此我毫不犹豫地在第一时间订阅了这个专栏。

2. 对于高效工作有哪些心得？

郑晔老师在专栏每一课的最后，都会用一句话来总结这一课的内容，我很喜欢这个风格，那我也先用一句话来说一下，我对高效工作的理解：能用尽可能小的时间颗粒度来安排自己的工作。

高效工作，如果按字面理解，就是在指定时间里能完成更多的工作（当然还得是合格的工作）。《五分钟商学院》的作者刘润老师曾提到过时间颗粒度的概念，你会发现，很多成功人士划分时间的颗粒度都比较小。王健林的时间颗粒度是十五分钟，比尔盖茨的时间颗粒度是五分钟。

你可以思考一下，你是按什么颗粒度来安排自己工作的呢？1 天，半天，1 小时还是 15 分钟？

另外，在把时间拆细的同时，也不能忽略对工作内容的分解。其实，对工作内容进行分解的颗粒度大小，同样对工作效率有很大影响。

讲到这里，又不得不提到《10x 程序员工作法》专栏，我看到郑晔老师在“任务分解”这个模块里，详细地讲解了怎样将工作任务进行拆解，如果对这部分内容感兴趣的同学，也可以去订阅专栏。

3. 学习《10x 程序员工作法》专栏的感受

我认为这个专栏最大的价值，就是为我们程序员的工作，划出了清晰的、可以遵循的高效工作原则。

我举个具体的例子吧，我团队中不止有一个同学是技术能力非常突出的，他们在拿到一个任务后，立马就能想到大概的解决方案，紧接着就开始写代码了……没错，撸袖子就开始写代码了。

结果就是，他们之后要开始漫长地“填坑”之旅。运气好的话，是可以在上线前把“坑”填完，上线后没什么事；运气不好的话，得要好几个版本才能把“坑”填完。

做为过来人，我在看到他们这么快就开始写代码后，就会把他们拦下来，跟他们聊聊各种需要考虑的事项，他们之前忽略掉的问题，或者是需要细化的点出来。

在我看了专栏中的“以终为始”这个模块内容之后，我发现这不正是我一直在寻找的东西吗？我平时零散地问各种问题，让他们考虑各种点，现在郑晔老师都已经把这些常见的实践，提炼总结成为高效工作的原则。

我们可以不用在针对工作中的各种问题孤立地作出反应，而是有了一套行之有效的原则可以遵循。

如果说我们平时实践的是“术”，那郑晔老师讲的就是“道”。我觉得只“以终为始”这一部分的内容就已经完全值回票价了。

Ivan（毅）
10+ 年研发经验，坐标苏州，目前在医疗信息化软件公司从事研发兼管理工作。

1. 为什么订阅《10x 程序员工作法》？

记得刚开始工作时，一门心思想着多做项目，多学技能，多攒经验，当然，战斗数值也增长不少，但就像武侠小说中的习武之人都会碰到的困惑，总觉得是围绕着招式在打转，而修为不足。

之后我也逐渐也有意识培养自己的思维方式和从多角度看问题的习惯，这些年也算形成了一些自己的价值观和方法论。

我现在服务的公司是一家初创公司，产品化和定制项目开发之路走的并不轻松，我发现不是团队效率低下，也不是不知道如何解决各种问题，而是我们投入很多精力在处理“偶然复杂度”的问题，我想这可能也是工作不轻松的原因之一吧。

所以在听郑晔老师的开篇词时，我就有一种被吸引的感觉，因为我觉得问题既然抛出了，后续肯定会有解决方案。

当然，每个人都会碰到自己的问题，所以要想解决问题需要有系统的思考框架，而大多数问题又存在共性，应该会有一些普适原则，这些在开篇词中都有提及，听着听着继续探究的兴趣也越来越浓了。

我是个偏向实用主义的人，除了“醍醐灌顶”的那一时快感，更看重方法在实践中的运用和反思，我相信《10x 程序员工作法》中介绍的经验和最佳实践一定能给我和团队带来工作效率和工作绩效的提升！

为了保持学习思考的连贯，我都是紧跟一周三次的更新节奏同步收听，即使春节期间也未间断，而且部分内容会反复听，每次重听都有新的感受。

而从第二部分开始，我觉得不光是收听，还要用文字表达出来，毕竟有互动有交流的学习才是有效的学习，而且我是强制自己一定要留言，有思考的留言。

2. 学习《10x 程序员工作法》的心得

基于之前的学习过程，我再总结下我的学习心得。

a. 思维方式的改变

课程虽然针对程序员，但受众却超过程序员范围。即使对于程序员来说，在工作中也要拓展自己的上下文，将自己放在更大的范围、平台上去思考问题，主动发掘问题关键点，在面对不同职能人员间沟通时，多运用“以终为始”模块中的知识尝试解决分歧，达成共识。

此时我或许明白了之前公司老板常说的一句话“你们都是工程师思维”，或许并不是将自己想象成其他角色，而是在更大的上下文背景中寻找突破点。

b. 工作习惯的培养

程序员的工作习惯不仅仅是每天上班打开邮箱，提交代码前编译通过等等事务性习惯，还包括：你的思维方式是否合适，由此产生的执行方式是否到位，检验方式是否科学，理论修正是否能产生更好的思维方式。

就像专栏中讲精益创业时，提到的“想法 -> 产品 -> 数据”对应的“开发 -> 测量 -> 认知”这样的反馈模型。

c. 可以做的更好

我们可能习惯于之前的工作方式，有时即便感觉别扭，但仍然只是希望在原有方式上修修补补，而不愿去仔细区分现实和期望，常常会以忙为理由，为自己辩解。

但如果不忙，你知道该怎么做吗？这恐怕是绝大多数程序员都要自省的一个问题，但这也恰恰说明我们在工作方式上是有很大的提升空间。

d. 主动思考也很重要

孔子说“学而不思则罔，思而不学则殆”，《10x 程序员工作法》并不是技术技能课，可以直接准备环境，代码走起，而是一种方法论课、答疑解惑课、实践验证课，听起来很过瘾，很能戳中痛点，但如何解决自己的实际问题则需要主动思考，主动交流。

这也是我后来逼着自己每学完一课都要思考后留言的初衷，并且我还会向周围人推荐这种学习方式。

其实，对于任何方面的知识，我都奉行“一学二懂三要用”，四要知道局限性的准则，不断更新自己的认知体系，力求做到融汇贯通，逐步形成适合自身的知识架构和行动准则。

One day
4 年研发工作经验，坐标上海，目前就职于一家汽车行业的公司做后端开发

1. 为什么订阅《10x 程序员工作法》？

想起刚毕业的时候，我还是能够前后端通吃的，因为那个时候，很多公司使用的还是 jsp+servlet 等简单的单体应用，所有功能全部集成在一个工程项目中。

等所有人把各自模块开发完，就丢一个 war 包到服务器，然后就进行测试就完事了。等测试小哥哥或小姐姐发现 Bug 的时候，自己就从前端到后端一步步的 debug，虽然有很多血与泪的历史，但是累并快乐着。

但工作久了之后，我发现很多工作是重复且无意义的，然后就是加班与熬夜并存。现在，我已经在软件开发行业已经混迹 4 年多，从一个混沌不知的菜鸟也渐渐有了自己学习方法。

但是个人经历总是有限，还是会遇到很多问题。工作久了也越发觉得时间的宝贵，也越来越迫切地想要追求更为高效的工作方法。

最开始看到专栏标题的时候，还心有疑惑，极客时间会有标题党？我学了这个专栏工作学习能提高 10 倍吗？

但当看到 10X 程序员工作法的内容，我就已经被郑晔老师的能力所折服，因为专栏目录中提到的各种思考原则，真的是很贴合实际。

工作能力的提升，本身是经验的积累，但是如果长时间堆砌同类型的业务知识，对于大脑来说是一种重复，也不利个人经验的沉淀。实践和理论总是相辅相成的，我是一个实践派，但是对于理论的方法，我也非常赞赏。

这相当于是站在前人的肩膀上，你在实践的过程中，得到与之类似或者不同的结果。以结果为导向，再结合理论就有不同的火花，我认为这是一件很神奇的事情。

2. 学习《10x 程序员工作法》专栏的心得

a. 多思、多问
我们的实际工作常常会被打乱，需求频繁变更，做出的东西与产品有出入等等。这其中很多情况我都经历过，比如，拿到新需求任务时，头脑没有思绪，大脑出现空白，直接上手就做。然后，到测试和交付的时候就经常出现问题。

结合老师说四项基本原则，简化思考框架，遇到问题多思考、多提问、多练习，去梳理真正要做的事情。清楚事物背后的本质，有效剔除不必要的思虑，让自己专注于任务本身。

现在我在接收一个新需求的时候，常常会思考这个需求的意义是什么？到底需不需要做？要怎么做才能更好？有没有好的替代方案？需求应用的场景是什么？这样做以后方便维护吗？

之类的问题，思考的越多，在以后的写代码的时候，就会考虑更多。需要并发吗？并发量多少? 我们服务器的承受压力多大? 我应该用多少个线程跑这个才合适呢？这样写出来的代码也会很健壮。修修改改，缝缝补补的代码即不利于个人发展，对后面的迭代也是一种煎熬。

b. 以结果为导向
我们日常都是按部就班的完成需求任务，确定好任务安排，于是就开始写代码。这个时候常常会有不那么预期的事情发生，因为测试一旦给提了 Bug，计划的周末约会可能就泡汤了，陷入加班的窘态中。

这时，我们就要以结果为导向，考虑到即将要做的事情，再将要做的事情一步步细分，然后做项目推演，带着明确的目的去工作，这样可以少走弯路。

c. 站在不同角色角度看问题
我们工作出现的很多问题，都是因为我们只站在程序员的角度，只能看到局部，缺少大局观导致的。不同职位在协作的过程中，认知和理解的的差异直接导致最终不能达成一致性意见，到最后还是苦逼了程序员，在做无限制的修改。

项目本身是有不同人员角色组合的共同体，大家为了达到共同的目的在做事，所以有必要跳出程序员角色思维的限制，站在项目本身去看待每一个问题。必要的沟通是需要的，对待不合理的需求，我们程序员也要敢于说不。

答疑解惑 | 持续集成，一条贯穿诸多实践的主线

“沟通反馈”模块又告一段落了，在这个模块中，我们把自己与真实世界的距离又拉近了一步。

一方面，我们强调主动沟通，把自身的信息更有效地传达出去；另一方面，我们也重视反馈，让真实世界的信息，更多地回到我们身边。同学们分享了很多经验，也提出了不少的问题。

在今天的答疑中，我选择了几个非常好的问题，从不同的角度丰富一下之前讲解的内容。

问题 1：单元测试做不好，是否会影响到 CI 的效果？

毅 同学提到

如果单元测试做的不到位，或者不满足 A-TRIP，是不是执行 CI 的效果就会弱很多？
——《24 | 快速反馈：为什么你们公司总是做不好持续集成？》

这是一个非常好的问题，问到了各种实践之间的关联。我们在前面用了两讲的篇幅介绍了持续集成这个实践，为什么要做持续集成以及如何做好持续集成。

在自动化模块，我们还会在这个基础之上继续延伸，介绍持续交付，这些内容是从操作的层面上进行介绍，都是对单一实践的描述。

利用这次答疑的机会，我再补充一个维度，谈谈实践之间的关联。

持续集成的价值在于，它是一条主线，可以将诸多实践贯穿起来。也就是说，想要真正意义上做好持续集成，需要把周边的很多实践都要做好。

我们具体地说一下这些实践。但请记住我们说过的，做好持续集成的关键是，快速反馈。

比如，我们想要做好 CI，需要有一个稳定的开发分支，所以，最好采用主开发分支的方式。想用好主分支开发，最好能够频繁提交；而频繁提交需要你的任务足够小，能够快速完成；将任务拆解的足够小，需要你真正懂得任务分解。要想在一个分支上开发多个功能，那就需要用 Feature Toggle 或者 Branch by Abstraction。

在这条线上，你有很多机会走错路。比如，你选择了分支开发模式，合并速度就不会太快，一旦反馈快不了，CI 的作用就会降低；再者，如果不能频繁提交，每次合并代码的周期就会变长，一旦合并代码的周期变长，人们就会倾向于少做麻烦事，也就会进一步降低提交的频率，恶性循环就此开启。

同样，即便你懂得了前面的道理，不懂任务分解，想频繁提交，也是心有余而力不足的。而多功能并行开发，则会让你情不自禁地想考虑使用多分支模型。

我们再来看另外一条线，也就是这个问题中提到的测试。

想做好 CI，首先要有可检查的东西，什么是可检查的东西，最简单的就是编译、代码风格检查，这些检查可以无条件加入构建脚本。但更重要的检查，应该来自于测试，而要想做好 CI，我们要有测试防护网。

什么叫测试防护网呢？就是你的测试要能给你提供一个足够安全的保障，这也就意味着你要有足够多的测试。换个更技术点的术语来说，就是要有足够高的测试覆盖率。

如果测试覆盖率不够，即便提交了代码，CI 都通过了，你对自己的代码依然是没有信心的，这就会降低 CI 在你的心中的地位。

如果想有足够高的测试覆盖率，你就要多写单元测试。我们在前面讲过测试金字塔了，上层测试因为很麻烦，你不会写太多，而且很多边界条件，通过上层测试是覆盖不到的，所以，测试覆盖率在经过了初期的快速提升后，到后期无论如何是提上不去的。要想提升测试覆盖率，唯有多写单元测试。

要想多写单元测试，就需要编写可以测试的代码，而要想编写可测的代码，就要懂软件设计，将系统之间耦合解开。

通过上面的分析，你已经看出来做好持续集成，让它完全发挥自己的价值，需要做的工作还是相当多的。但也请别灰心，实际上，我做咨询时，很多团队就是从持续集成下手，开始改造他们的软件开发过程。

这是一个“以终为始”的思路，先锁定好目标，就是要把持续集成做好，然后围绕着这个目标改进其他做得欠佳的方面。比如，原来是多分支的，就先固定一个主分支，然后，逐步改变大家的开发习惯，让他们进入单分支的开发状态。

再比如，原来没有测试，那就在 CI 上先加一个最低的测试覆盖率，然后定期去提高，比如，第一周是 10%，第二周是 20%，这样一步一步地提高，开发团队可以一边开发新东西，一边为既有代码补测试。等到覆盖率到了一定程度，提高有困难了，团队就可以考虑怎么改进设计了。

所以，CI 作为一个单独的实践，本身是很简单的，但它可以成为提纲挈领的主线，帮助团队不断改善自己的开发过程。

问题 2：老板参加复盘，不敢说真话怎么办？

grass10happy 同学提到

复盘是不是最好是团队内部进行，每次老板参加复盘，好像就没人说出真话了。
——《25 | 开发中的问题一再出现，应该怎么办？》

感谢 grass10happy 同学这个提问，把我因为篇幅原因省掉的一个部分给挽救了回来。

回顾会议的目的在于改进，它不仅仅在于让大家参与进来，更重要的是让团队成员能够敞开心扉，把问题暴露出来。暴露问题，是改进的前提条件。

我在《27 | 尽早暴露问题： 为什么被指责的总是你？》这篇文章中说过了，对于很多人来说，敢不敢暴露问题是个心理问题。你会发现，同事之间聊天，普遍是没有任何压力的，你几乎可以放心大胆地谈论各种问题，而一旦有领导在，很多顾虑就会出现了。

于是，问题就变成了怎么能够让大家放心地把问题暴露出来，一个办法就是设置一个安全的环境。

怎么设置一个安全的环境呢？对于标准的回顾会议来说，第一步应该是做安全性检查。

先由大家投票，最简单的方式是就是，给当前的环境打分。你觉得可以畅所欲言就打 1 分，你觉得还好，就打 0 分，如果你觉得不方便表达，比如，你看领导在，很多问题不适合反馈，就打 -1。

每个与会者都投出属于自己的一票。然后，主持人根据投票结果决定回顾会议是否进行，比如，有人投 -1 就不能继续。

会议能继续固然好，一旦会议不能继续，可以有多种解决方案。比如，把在场职位最高的人请出去，这个人可能就是老板。老板也许心里很不爽，但在这个过程中，大家都是按照规则在办事，并不存在对谁另眼相待的情况。

当老板离席之后，我们再进行一轮投票，判断环境是否变得安全了。如此反复，也许要进行几轮投票，直到大家觉得安全了。

当然，也有可能进行多轮，有人始终觉得不安全，那可能最好的选择是，取消今天的回顾会议，换个时间地点从头再来。而项目负责人则需要私下里解决一下团队内心安全的问题。

通过安全性检查之后，我们才会进入回顾会议的正式环节，具体内容在正文中已经讲过了，这里就不再赘述了。

问题 3：国内的技术信息落后吗？

One day 提到

老师能否多多介绍一下技术方面的网站之类的，新技术发展见闻之类的，或者技术总结方面。国内的技术基本都多少有些滞后。
——《23 | 可视化：一种更为直观的沟通方式》

这个问题让我感觉自己一下子回到了好多年前。我刚入行的那会，学习新知识确实要多看看英文网站，当时的信息传播速度不快，中文技术网站不多。

但在今天，显然已经不是这样了，如果只是想获得最新的技术信息，我在《23 | 可视化：一种更为直观的沟通方式》这篇文章中介绍了 InfoQ 和技术雷达，这上面的信息量已经很丰富了。你再只要稍微看几个网站，关注几个公众号，各种信息就会送到你面前。

所以，你根本不用担心会错过什么新技术，反倒是信息量太大，需要好好过滤一下。

国内程序员真正落后的不是信息，而是观念。

我讲的很多内容是软件工程方面的，以我对国内外程序员的了解来看，发达国家的程序员在这些内容的普及上，要比国内程序员好很多。

国内程序员的平均水平，大多停留在实现一个功能的理解上，而发达国家的程序员做事要专业许多。所以，以专业素养来看，国内程序员还有很大的提升空间。

在经济学里有“边际效用递减法则”（The Law Of Diminishing Marginal Utility），说的是当你手里某一物品总数越来越多时，新增一个单位该物品所获得的效用通常会越来越少。

当你的技术知识积累到一定程度时，还采用原来的学习方式，就很难获得真正意义上的提高，这是很多人抱怨 IT 行业不好混的原因。

同时，这也是我开设这个专栏的初衷，希望给大家一些不同的视角，一些新的前进动力。

好，今天的答疑就到这里。我想请你分享一下，你是怎么理解这些问题的呢？欢迎在留言区写下你的想法。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

29 | “懒惰”应该是所有程序员的骄傲

经过前面几个模块的学习，我们的专栏终于进入到程序员看上去最熟悉的一个主题：自动化。

每每提及自动化，我就会想起 Perl 语言的发明人 Larry Wall 一个经典叙述：优秀程序员应该有三大美德：懒惰、急躁和傲慢（Laziness, Impatience and hubris）。

有人甚至为此专门打造了一个三大美德的网站，阐释这个初看起来匪夷所思的说法。

懒惰，是一种品质，它会使你花很大力气去规避过度的精力消耗，敦促你写出节省体力的程序，别人也能很好地利用，你还会为此写出完善的文档，以免别人来问问题。

急躁，是计算机偷懒时，你会感到的一种愤怒。它会促使你写出超越预期的程序，而不只是响应需求。

傲慢，极度自信，写出（或维护）别人挑不出毛病的程序。

不知道你是否感受到，程序员独有的幽默和透露出的那种骄傲：我做的东西就应该是最好的。

之所以要从 Larry Wall 的这段话开启“自动化”这个模块，因为只要一说到自动化，我就会情不自禁地联想到“偷懒”这个词。是的，我们程序员的工作，本质上就是打造各种自动化的工具，让人们从各种繁复的工作中解脱出来，让人有机会“偷懒”。

不过，我也知道，从机器那里偷来的“懒”很快就被更多的工作填满了。但 Larry Wall 的这段话却可以鼓励我们不断地打造出更好的工具。

作为程序员，你当然知道“自动化”这件事的价值，在日常工作中，也实实在在地践行着打造自动化工具的任务，但很多人对自动化的理解可能有些单薄。今天，我就从一个你可能会忽略的主题开始讨论：不要自动化。

不要自动化

我先给你讲一个让我印象深刻的“不自动化”的例子。

之前在 ThoughtWorks 工作时，我们有一项工作是，帮助其他公司启动一些新产品。有一次，我的两个同事被一个公司请去启动一个视频网站的项目。那时候还不像如今的市场，已经由几大视频网站瓜分完毕，当时不少公司看到了视频网站的苗头，觉得自己有机会。这个来请我们的公司也不例外，觉得自己也能分一杯羹。

两个星期之后，我的两个同事回来了。我们饶有兴趣地去问项目的进展，因为项目启动之后，通常会有后续的开发合作，但结果令我们很意外，这个项目停止了。

“出了什么状况吗？”我们问。

“是我们建议用户停掉这个项目的。”他们回答到。

我们“恨恨地”问他们为什么丢掉了一个这么重要的机会。这两个同事的回答也很直白，他们结合着客户的想法算了一笔账：这个项目需要大量的资金投入，投入规模之大，是超出客户想象的，按照现有的规划投入，这个项目肯定会亏本。要么重新规划，要么取消这个项目。客户认真研究了一番，最终决定取消项目。

这件事大约发生在 10 年前，今天我们都看到各大视频网站在烧钱上的投入，以那个公司的实力，想要参加这场比拼，确实还差太多。

这件事之所以给我留下深刻印象，因为它是我职业生涯中见到的第一个通过“主动取消项目”获取项目成功的案例。

或许你不能理解我这里所说的“项目成功”。在我看来，做有价值的事是重要的，这里面的有价值，不仅仅是“做”了什么，通过“不做”节省时间和成本也是有价值的。我的两个同事阻止了客户的浪费，所以，我将这个项目视为成功。

对于开发来说，也遵循同样的道理。程序员这个群体技术能力实在太强，做一个技术方案简直是太符合直觉的做法，我们就是忠实地把一个个需求做出来，把“全世界”都自动化了。

但事实上，这个世界太多的浪费就是做了不该做的东西。在我们的专栏里，我反复地说，我们要多问问题，目的就是为了不做那些不该做的事。

小心 NIH 综合症

你可以从需求的角度判断哪些工作是可以不做的，但我们也要防止程序员自己“加戏”，我再给你讲一个技术人员普遍存在的问题：NIH 综合症（Not Invented Here Syndrome）。

NIH 是什么意思？就是有人特别看不上别人做的东西，非要自己做出一套来，原因只是因为那个东西不是我做的，可能存在各种问题。

这种现象在开源之前尤为流行，很多公司都要做自己的中间件，做自己的数据库封装。虽然很多公司因此有了自己特色的框架，但是因为水平有限，做出来的东西通常极为难用，很多人一边骂，一边还要继续在上面开发。

开源运动兴起之后，我以为这种现象会好一些，但事实证明，我想多了。

比如，这种乱象在前端领域也出现了，各种各样的框架，让很多前端程序员哭诉，实在学不动了。再比如，我曾经面试过一个接触 Go 比较早的程序员，他就是恨不得把所有框架都自己写。

因为他学 Go 的时候，确实框架比较少，但问题是，如今的 Go 已经不是他学习时的那个 Go 了，现在各种框架已经很丰富了，不需要什么都自己做。当时我问他，如果有一天你离开了，公司怎么办呢？实际上，他从来没考虑过这个问题。

说了这么多，无非就是想说明一件事，写代码之前，先问问自己真的要做吗？能不做就不做，直到你有了足够的理由去做。对应到 Larry Wall 的说法，你要懒惰，花大力气去规避精力消耗。

做好自动化

说完了不要自动化的部分，再来说说要自动化的部分。

我还是先从你可能会忽略的问题入手，你的日常工作是给别人打造自动化，但你自己的工作够自动化吗？还是问一个更具体的问题吧！如果你写的代码要上线，会经过怎样的过程？

我先给你看一个极其糟糕的例子。刚开始工作不久，我有一次出差到客户现场。临近下班时，我发现了程序的一个 Bug。在那个年代，我们的程序是按照官方推荐做法编写的 EJB（Enterprise JavaBean），今天很多年轻的程序员可能不了解了，它只有部署到应用服务器才能运行。

我的解决方案就是加上一些打印语句，然后部署到应用服务器上，看输出的结果，再加上另外一些语句，再部署，如此往复。那时我们完全是手工打包上传，每次至少要十几分钟。最终，定位到了问题，只修改了一行代码。但几个小时的时间就这样被无谓的消耗了。

那之后，我花了很长时间研究怎么做自动化的增量部署，最终让这个过程简化了下来。但这件事对我的影响很大，这是我第一次认识到一个部署过程可能对开发造成的影响，也让我对自动化在开发过程内的应用有了属于自己的认识。

相比于我刚开始工作那会。现在在工具层面做类似的事已经容易很多了，在后面的内容中，我会结合着具体的场景介绍一下现在的最佳实践。

你要懂得软件设计

最后，我们再来说说我们的本职工作，给别人打造自动化工具中需要的能力：软件设计。

软件设计，是很多人既熟悉又陌生的一个词，说熟悉，很多人都知道，做软件要设计，还能顺嘴说出几个设计模式的名字；说陌生，是因为在我的职业生涯中，遇到真正懂软件设计的程序员少之又少。大多数人都是混淆了设计和实现。

举个例子。有一次，我要在两个系统之间做一个连接器，让上游系统向下游系统发消息，或许你一听就知道了，这里需要的是一个消息队列。但实际上，我们需要的能力要比消息队列更丰富一些，比如，要将重复的消息去除。一个同事给我推荐了 Kafka 当作这个连接器的基础，我欣然地接受了。

不过，在后续设计的讨论中，我们就经常出现话语体系的分歧。我说，这个连接器要有怎样的能力，他会说 Kafka 能够如何如何。究其根因，我在讨论的是设计，而他说的是实现，所以，我们两个很难把问题讨论到一起。

为什么我会如此看重设计呢？在软件开发中，其它的东西都是易变的，唯有设计的可变性是你可以控制的。

同样以前面的讨论为例，尽管 Kafka 在当下比较火热，但是我不敢保证 Kafka 在未来不会被我换掉。因为就在几年前，消息队列还是传统中间件的强项，现在也渐渐被人淡忘了。

我不想让我的设计随着某一个技术选型而不断摇摆。如果工作许多年，知识体系只能靠各种新框架新工具支撑，我们做程序员就只剩下疲于奔命了。不懂软件设计，只专注各种工具，其结果一定是被新技术遗弃，这也是很多人经常抱怨 IT 行业变化快的重要原因。

回到 Larray Wall 的说法上，你要想写出一个别人挑不出毛病的程序，你先要懂得软件设计。幸运的是，软件设计这些年的变化真不大，掌握了软件设计再来看很多框架和工具，学习起来就会容易很多。在这个模块的后半部分，我会与你探讨软件设计的话题，降低自己给自己挖坑的概率。

总结时刻

Perl 语言的发明人 Larry Wall 曾经说过，优秀程序员应该有三大美德：懒惰、急躁和傲慢（Laziness, Impatience and hubris）。想要成为一个优秀的程序员，就要让机器为自己很好地工作，而这需要对自动化有着很好地理解。

我们学习自动化，先要知道哪些东西不要自动化，尽最大的努力不做浪费时间的事。一方面，我们要从需求上规避那些没必要做的事；另一方面，我们也从自身防止 NIH 综合症（Not Invented Here Syndrome），争取做一个懒惰的程序员。

对于要自动化的事，我们需要反思一下，在为别人打造自动化工具的同时，我们自己的工作过程有没有很好地自动化。而如果我们想拥有打造良好的自动化工具，我们需要对软件设计有着充分地理解。

如果今天的内容你只能记住一件事，那请记住：请谨慎地将工作自动化。

最后，我想请你分享一下，学习了本讲之后，你现在是怎样理解自动化的呢？欢迎在留言区写下你的想法。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

30 | 一个好的项目自动化应该是什么样子的？

进入自动化这个模块，我准备从程序员的日常工作开始。介绍“迭代 0”时，我提到构建脚本是项目准备的一个重要组成部分，但在那一讲中，我并没有具体说构建脚本长成什么样。

今天，我们以一个典型的 Java REST 服务为例，介绍一下最基本的构建脚本应该做到什么样子。这里我采用的 Java 技术中最为常见的 Spring Boot 作为基础框架，而构建工具，我选择了 Gradle。

估计很多 Java 程序员心中的第一个问题就是，为什么用 Gradle，而不是 Maven？Maven 明明是 Java 社区最经典的构建工具。答案是因为 Maven 不够灵活。

你可以回想一下，你有多少次用 Maven 实现过特定需求？估计大部分人的答案都是没有。随着持续集成、持续交付的兴起，构建脚本的订制能力会变得越来越重要，Maven 则表现得力有不逮。

其实，早在 2012 年，ThoughtWorks 技术雷达就将 Maven 放到了 暂缓（HOLD）里面，也就是说，能不用就不用。

为了配合这次的讲解，我写了一个 Demo，放在了 Github 上。它的功能非常简单：

通过向 /users POST 一个请求，实现用户注册；

访问 /users，查看已注册的用户。

如果方便的话，你最好把这个项目 clone 下来，以便参考。这里我主要是讲解自动化要做成什么样子，如果你想了解具体是怎么实现的，可以参考 Demo 里的代码。

好，我们开始！

基础准备

先把这个项目从 Github 上 clone 下来。

git clone  https://github.com/dreamhead/geektime-zero.git

然后，进入到项目所在的目录中。

cd geektime-zero

当你准备就绪，我们就开始进一步了解这个项目。

一般我们了解一个项目，都会用用一个 IDE 打开这个项目，这里我推荐使用 IntelliJ IDEA，这是目前行业中最好的 Java IDE。自从它的社区版免费之后，它就成为了我向他人推荐的首选。

我知道，开发工具是除了程序设计语言之外，另外一个容易引起“宗教战争”的话题，如果你喜欢其他的 IDE，那就用你最喜欢的 IDE 打开好了，只不过，需要调整一下构建脚本中的配置。

怎么打开这个项目呢？我们先用 Gradle 命令生成一个 IDEA 工程。

./gradlew idea

这个命令会生成一个.ipr 文件，这就是 IDEA 的工程文件，用 IDEA 打开即可。

这里有两点需要说明一下。

第一，这里用的 gradlew，它是 Gradle 命令的一个封装，它会自动下载一个构建这个项目所需的 Gradle，重点是通过这个命令锁定了 Gradle 的版本，避免因为构建脚本的差异，造成“你成功我失败”的情况。

第二，IDE 工程是生成的。很多人直觉的做法是用 IDE 直接打开。有一些团队的项目里有好多个构建文件，究竟用哪个打开，不去问人是根本不知道的，这对项目的新人是非常不友好的。

生成的做法与前面 Gradle 封装是类似的，它可以避免因为本地安装不同版本 IDE 造成各种问题。

另外，因为 IDE 的工程是生成的，如果项目里一旦增加了新的程序库依赖，你只需重新执行一次上面的命令就好了，现在的 IDE 都有很好的自动加载能力，当它检测到工程文件的变化，就会重新加载。

好，现在你可以用 IDE 打开，我们就可以进一步了解这个项目了。

初见项目

我们先来了解一点 Gradle 的配置文件，它也是我们做项目自动化的重点。

build.gradle，它是 Gradle 的配置文件。因为 Gradle 是由 Groovy 编写而成，build.gradle 本质上就是一个 Groovy 的脚本，其中的配置就是 Groovy 代码，这也是 Gradle 能够灵活订制的基础。

settings.gradle，这也是一个 Gradle 配置文件，用以支持多模块。如果说一个项目的每个模块都可以有一个 build.gradle，那整个项目只有一个 settings.gradle。

在 Gradle 里，许多能力都是以插件的形式提供的，比如，前面生成 IDEA 工程就是配置文件中的一句话。

apply plugin: 'idea'

所以，如果你是其他 IDE 的死忠粉，你可以把这句话，换成你喜欢的 IDE。

（注：这个项目采用 Lombok 简化代码，为了能让代码在你的 IntelliJ IDEA 编译运行，你可以安装 Lombok 插件，然后，在 “Build, Execution, Deployment”-> “Compiler” -> “Annotation Processors“”中，选中 Enable annotation processing）

好，有了基础知识之后，我们来了解一下代码组织。

首先是分模块。除非你的代码库规模非常小，否则，分模块几乎是一种必然。一种恰当的划分方式是根据业务划分代码。比如，把用户相关的内容放到一个模块里，把交易订单信息放到一个模块里，把物流信息放到另一个模块里。

如果你未来打算做微服务，那每一个模块就可以成为一个独立的服务。

在我们的项目里，我示例性地划分了两个模块：

zero-identity，是用户信息的模块；

zero-bootstrap，是多个模块打包成一个可部署应用的模块。

这两个模块的信息都配置在 settings.gradle 中。

include 'zero-bootstrap'

include 'zero-identity'

再来是目录结构。具体要怎么样组织代码，在 Java 世界里已经是一件约定俗成的事情了。

src/main/java 下放着你的源代码，src/main/resources 下放配置文件，src/test/java 放测试代码。这是约定优于配置（Convention over Configuration）思想的体现。如果你用的工具没有约定，你只能自己定好，让其他人遵守。

检查

在自动化过程中，一个最基本的工作是检查。检查的工作在我们的项目中通过一个 check 任务来执行。

./gradlew check

这个检查会检查什么呢？这取决于配置。在这个项目里，我们应用了 Java 插件，它就可以编译 Java 文件，检查代码是否可以正常编译，运行测试，检查代码是否功能正常等等。但我要求更多。

讲“迭代 0”时，我说过，最基本的代码风格检查要放在构建脚本中，这里我用了 CheckStyle 来做这件事。缺省情况下，你只要应用 Checkstyle 插件即可。

apply plugin: 'checkstyle'

在这个项目里，我做了一些订制，比如，指定某些文件可以不做检查。

style.excludePackages = [

]

style.excludeClasses = [

]

测试覆盖率也应该加入到构建脚本中，这里我用了 JaCoCo。同样，缺省情况下，只要应用 JaCoCo 插件即可。

apply plugin: 'jacoco'

我依然是做了一些订制，比如，生成结果的 HTML 报表，还有可以忽略某些文件不做检查。

coverage.excludePackages = [

]

coverage.excludeClasses = [

]

这里最特别的地方是，我将测试覆盖率固定在 1.0，也就是 100% 的测试覆盖。这是我做新项目的缺省配置，也是我对团队的要求。

如果一个新项目，能把这几个检查都通过，腐坏的速度应该就不会那么快了。当然，你也可以根据自己的需要，添加更多的检查。

数据库迁移

讲“迭代 0”时，我还提到了数据库迁移，也就是怎样修改数据库。在示例项目中，我选择的数据库迁移工具是
Flyway。

plugins {

    id "org.flywaydb.flyway" version "5.2.4"

}

下面先要做一些基本的配置，保证可以连接到数据库。（注：如果你想直接使用这里的配置，可以在本机的 MySQL 数据库上，创建一个 zero 的用户，密码是 geektime，然后，再创建一个 zero_test 的数据库。）

flyway {

    url = 'jdbc:mysql://localhost:3306/zero_test?useUnicode=true&characterEncoding=utf-8&useSSL=false'

    user = 'zero'

    password = 'geektime'

    locations = ["filesystem:$rootDir/gradle/config/migration"]

}

那修改数据库会怎么做呢？先添加一个数据库迁移文件，比如，在示例项目中，我创建一个迁移文件（gradle/config/migration/V2019.02.15.07.43__Create_user_table.sql），在其中创建了一个 User 表。

CREATE TABLE zero_users(

    id bigint(20) not null AUTO_INCREMENT,

    name varchar(100) not null unique,

    password varchar(100) not null,

    primary key(id)

);

这里的迁移文件版本，我选择了以时间戳的方式进行命名，还有一种方式是以版本号的方式，比如 V1、V2。

时间戳命名方式的好处是，不同的人可以同时开发，命名冲突的几率很小，而采用版本号命名的方式，命名冲突的概率会大一些。

添加好数据库迁移文件之后，只要执行下面这个命令就好：

./gradlew flywayMigrate

这样，对数据库的修改就在数据库里了，你可以打开数据库查看一下。

构建应用

做好了最基本的检查，数据库也准备就绪，接下来，我们就应该构建我们的应用了。

首先是生成构建产物，它只要一个命令。

./gradlew build

这个命令会在 zero-bootstrap/build/libs 下生成一个可执行 JAR 包，它就是我们最终的构建产物。此外，build 任务会依赖于 check 任务，也就是说，构建之前，会先对代码进行检查。

从前 Java 程序只是打出一个可部署的包，然后，部署到应用服务器上。感谢现在基础设施的进步，我们可以省去部署的环节，这个包本身就是一个可执行的。我们可以通过命令执行将 JAR 执行起来。

java -jar zero-bootstrap/build/libs/zero-bootstrap-*-boot.jar

在开发过程中，并不需要每次都将 JAR 包打出来，我们还可以直接通过 Gradle 命令将应用运行起来。

./gradlew bootRun

不过，我估计你更常用的方式是，在 IDE 中找到 Bootstrap 这个入口类，然后，直接运行它。

既然程序已经运行起来，我们不妨测试一下。我们通过一些工具，比如 Postman 或者 Curl，把下面的内容 POST 到 http://localhost:8080/users

{

	"username": "foo",

	"password": "bar"

}

然后，通过浏览器访问 http://localhost:8080/users
我们就可以看见我们刚刚注册的这个用户了。

总结时刻

总结一下今天的内容。今天我们通过一个具体的例子展示了一个最基本的项目自动化，包括了：

生成 IDE 工程；

编译；

打包；

运行测试；

代码风格检查；

测试覆盖率；

数据库迁移；

运行应用。

但这就是自动化的全部了吗？显然不是，我这里给出的只是一个最基本的示例。实际上，几乎每个重复的工作或是繁琐的工作，都应该自动化。我们不应该把时间和精力浪费在那些机器可以很好地替我们完成的工作上。

今天的基础设施已经让我们的自动化工作变得比以往容易了很多，比如，可执行 JAR 包就比从前部署到应用服务器上简化太多了。Gradle 也让订制构建脚本的难度降低了很多。

这里提到的项目自动化也是持续集成的基础，在持续集成服务上执行的命令，就应该是我们在构建脚本中写好的，比如：

./gradlew build

2011 年，我在 InfoQ 上发表了一篇《软件开发地基》，讨论的就是一个项目的构建脚本应该是什么样子。虽然其中用到的工具今天已经不再流行，但一些基础内容今天看来，依然是有效的。如果有兴趣，你也可以看一下。

如果今天的内容你只能记住一件事，那请记住：将你的工作过程自动化。

最后，我想请你分享一下，在日常开发工作中，你还把哪些过程自动化了呢？欢迎在留言区写下你的想法。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

31 | 程序员怎么学习运维知识？

在上一讲中，我们讲到了开发过程的自动化，我们的关注点在于如何构建出一个有效的部署包，这个包最终是要上线部署的，那接下来，我们就来关心一下部署的相关工作。

零散的运维知识

在一些稍具规模的公司，为部署工作设置了一个专有职位，称之为运维。当然，这个岗位的职责远不止部署这一件事，还要维护线上系统的稳定。不过，如果你的团队规模不大，或是项目处于初始阶段，这些工作往往也要由程序员自行完成。

对于一个程序员来说，了解自己的程序怎么部署上线，是非常重要的。我们既要了解一个软件的逻辑，也要知道它的物理部署。只有这样，出了问题才知道怎么修复。

更重要的是，我们在设计时，才能尽量规避部署带来的问题。而部署，恰恰也是最适合发挥自动化本领的地方。

好，即便下定决心准备学习运维相关知识，你准备怎么学呢？我先来问你个问题，提到运维，你会想到什么？

如果你是一个刚刚步入这个行业的程序员，你或许会想到 Docker，想到 Kubernetes；如果再早一点入行，你或许还会想到 Chef、Puppet、Ansible；更早一些入行的话，你会想到 Shell 脚本。没错，这些东西都是与运维相关的。那我就这么一个一个地都学一遍吗？

就我个人的学习经验而言，如果所有的知识都是零散的，没有一个体系将它们贯穿起来，你原有的知识无法帮助你学习新知识，这种学习方式效率极低，过程也极其痛苦。

如果是有结构的知识，所谓的学习新知识不过是在学习增量，真正要理解的新东西并不多，学习效率自然会大幅度提高。所以，想学好运维知识，首先你要建立起一个有效的知识体系。

你可能会问，这些运维知识看上去就是一个一个独立的工具啊？我曾经也为此困惑了许久，虽然我对各个工具已经有了不少的了解，但依然缺乏一个有效的知识体系，将它们贯穿起来，直到我上了一堂课。

感谢 Odd-e 的柴锋，有一次，他给我上了一堂 DevOps 课，他对运维知识的讲解让我茅塞顿开，从此，我的运维知识有了体系。

准确地说，他的这堂课就是讲给程序员的运维课。今天，我就把这个体系按照我的理解，重新整理一遍分享给你，也算是完成一次知识输出。

好，我们开始！

Java 知识体系

正如我前面所说，学习一个新东西，最好的办法是学习增量，如果能够找到它与已有知识体系的联系，我们就可以把已有知识的理解方式借鉴过去。

作为程序员，我们其实已经有了一个完善的知识体系，这就是我们对于程序设计的理解，而理解运维的知识体系，刚好可以借鉴这个体系。怎么理解这句话呢？

以最常见的 Java 开发为例，如果要成为一个合格的 Java 程序员，我应该知道些什么呢？

首先肯定是 Java 语言，我需要了解 Java 语言的各种语法特性。不过，只了解语法是写不出什么像样程序的，我们还需要掌握核心库。

对于 Java 来说，就是 JDK 中的各种类，比如，最常见的 String、List、Map 等等。

理论上来说，掌握了基本的语法和核心库，你就可以开发任何程序了。但在实践中，为了避免重新发明“轮子”，减少不必要的工作量，我们还会用到大量的第三方类库，比如，Google Guava、SLF4J 等等。

除了功能实现，还有一些结构性的代码也会反复出现。比如说，在常见的 REST 服务中，我们要将数据库表和对象映射到一起，要将结果转换成 JSON，要将系统各个组件组装到一起。

为了减少结构上的代码重复，于是，开发框架出现了，在 Java 中最常见的开发框架就是 Spring。

至此，你就可以完成基本的代码编写，但这还不够。

在 Java 中，你不会从底层完成所有事情，比如，虽然你写 REST 服务，但你很少会接触到最底层的 HTTP 实现，因为这些工作由运行时环境承担了。

我们要做的只是把打好的包部署到这些运行时环境上，在 Java 的世界里，这是 Tomcat、Jetty 之类的容器承担的职责。

如果你刚刚加入这一行，上来就用 Spring Boot 之类的框架写代码，你可能并没有碰到这样的部署过程，因为这些框架已经把容器封装其中，简化了部署过程。

Tomcat、Jetty 往往还只是在一台机器上部署，在现实的场景中，一台机器通常是不够用的，我们可能需要的是一个集群。

你可能会想到用 Nginx 来做一个负载均衡，但如果用原生的 Java 解决方案，这时候就轮到企业级的应用服务器登场了，比如：IBM WebSphere、Oracle WebLogic Server、JBoss Enterprise Application Platform 等等。

至此，一套完整的 Java 应用解决方案已经部署起来了。但我们知道了这些，和我们运维知识有什么关系呢？我们可以用同样的体系去理解运维知识。

运维知识体系

首先，要理解运维体系的语言。运维的语言是什么呢？是 Shell，人们最熟悉的应该是 Bash。我们通过操作系统与计算机打交道，但我们无法直接只用操作系统内核，Shell 为我们提供了一个接口，让我们可以访问操作系统内核提供的服务。

你可能会以为我这里用的是比喻，将 Shell 比喻成语言，但还真不是，Shell 本身就是一门编程语言。绝大多数人都知道 Shell 可以编程，但几乎没有人把 Shell 当成一门编程语言来学习，基本上都是在需要的时候，搜一下，然后照猫画虎地将代码复制上去。

这样造成的结果就是，一旦写一个脚本，就要花费大量的时间与语法做斗争，只是为了它能够运行起来。

有了语言，再来就是核心库了。运维的核心库是什么？就是 Shell 提供的各种 Unix/Linux 的核心命令，比如：ls、cd、ps、grep、kill、cut、sort、uniq 等等，它们几乎与操作系统绑定在一起，随着操作系统一起发布。

了解了核心的部分，还需要了解一些第三方库，运维知识的第三方库就是那些不属于操作系统核心命令的命令，比如：rsync、curl 等等。

Java 有框架可用，运维也有框架吗？你可以想一下，Java 的框架提供的是一些通用的能力，在运维工作中，也是有一些通用能力的，比如：在安装某个包之前，要检查一下这个包是否已经安装了；在启动一个服务前，要检查这个服务是否启动了，等等。所以，能够帮我们把这些工作做好的工具，就是我们的运维框架。

到这里，你应该已经明白了，我在说的运维框架其实就是像 Chef、Puppet、Ansible 之类的配置管理工具。它们做的事就是把那些繁琐的工作按照我们的定义帮我们做好。

有了对软件环境的基本配置，接下来，就要找一个运行时的环境将软件跑起来了。这时候，我们要了解像虚拟机、Docker 之类的技术，它们帮我们解决的问题就是在单机上的部署。

一般来说，了解了这些内容，我们就可以构建出一个开发环境或测试环境。除非用户非常少，我们可以在生产环境考虑单机部署，否则，我们迄今为止讨论的各种技术还都是在开发环节的。

如果我们需要一个集群或是高可用环境，我们还需要进一步了解其他技术，这时候，就轮到一些更复杂的技术登场了，比如，云技术，Amazon AWS、OpenStack，包括国内的阿里云。如果你采用的是 Docker 这样的基础技术，就需要 Kubernetes、Docker Swarm 之类的技术。

至此，一个相对完整的运维知识体系已经建立起来了，现在你有了一张知识地图，走在运维大陆上，应该不会轻易地迷失了。希望你可以拿着它，继续不断地开疆拓土。

总结时刻

我们今天的关注点在于，将开发过程产生的构建产物部署起来。部署过程要依赖于运维知识，每个程序员都应该学习运维知识，保证我们对软件的运行有更清楚地认识，而且部署工作是非常适合自动化的。

但是，对运维工具的学习是非常困难的，因为我们遇到的很多工具是非常零散的，缺乏体系。

这里，我给你介绍了一个运维的知识体系，这个体系借鉴自 Java 的知识体系，包括了编程语言、核心库、第三方库、开发框架、单机部署和集群部署等诸多方面。我把今天提到的各种技术整理成一个表格列在下面，你可以参考它更好地理解运维知识。

如果今天的内容你只能记住一件事，那请记住：有体系地学习运维知识。

最后，我想请你分享一下，你还能想到哪些运维知识可以放到这张知识地图上呢？欢迎在留言区写下你的想法。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

32 | 持续交付：有持续集成就够了吗？

在前面两讲，我给你讲了开发过程的自动化，将我们的程序打成发布包；然后讲了部署过程的自动化，通过各种工具将发布包部署起来。

有了这些基础，我们就可以考虑在每次开发完之后，将程序打包部署到环境中。开发完就自动打包，然后自动部署，听起来很像持续集成是不是？

关于持续集成，我在专栏里已经讲过两次，分别讨论了“为什么要做持续集成”和“怎么做好持续集成”。但持续集成的讨论只停留在开发环节。

有了前面两讲的准备，我们就可以把这个过程再进一步延伸。聪明的你或许已经听出来了，这次我要讲的主题是持续交付。

持续交付

让持续交付这个概念广为人知的是一本书，Jez Humble 和 Dave Farley 的《持续交付》（Continuous Delivery）。

前面讲持续集成的发展历史时，我提到了 CruiseControl，它是持续集成服务器的鼻祖。因为持续集成的不断发展，2007 年，我的老东家 ThoughtWorks 公司有意以 CruiseControl 为基础，提供企业级服务，于是成立了一个团队，打造一个更好的持续集成服务器，Jez Humble 就是在这个团队中工作的。

同样在这个团队工作的还有一个人，乔梁，他是《持续交付》这本书的中文版译者，而且在这本书出版近十年后，他自己写了《持续交付 2.0》，把自己多年来关于持续交付的新理解整理了进去。

那么，什么叫更好的持续集成服务器呢？当时我的理解很浅薄，只是希望它有更好的界面，更快的构建速度，而 Jez Humble 他们对于这个产品的构想远远超过了我当时的想象，他们将生产环境也纳入了考量。

什么是持续交付？简言之，它就是一种让软件随时处于可以部署到生产环境的能力。从一个打好的发布包到部署到生产环境可用，这中间还差了什么呢？那就是验证发布包，部署到环境中。

验证发布包，你或许会想，这不是测试的事吗？这不是已经在持续集成阶段完成的吗？不尽然。在持续集成阶段验证的包，往往缺少了环境的支持。

因为持续集成的环境往往是单机的，主要强调功能验证，而一些与生产环境相关的测试往往是欠缺的。所以，这里就引出了持续交付中一个需要关注的点：环境。

一般来说，在构建持续交付的基础设施时，会有下面几个不同的环境。

持续集成环境，持续集成是持续交付的前提，这个过程主要是执行基本的检查，打出一个可以发布的包。

测试环境（Test），这个环境往往是单机的，主要负责功能验证，这里运行的测试基本上都是验收测试级别的，而一般把单元测试和集成测试等执行比较快的测试放到持续集成环境中执行。

预生产环境（Staging），这个环境通常与生产环境配置是相同的，比如，负载均衡，集群之类的都要有，只是机器数量上会少一些，主要负责验证部署环境，比如，可以用来发现由多机并发带来的一些问题。

生产环境（Production），这就是真实的线上环境了。

你也看出来了，每个环境的作用是有差异的，所以，通常不会将所有的验证放在一起执行，而是要分阶段的去执行，一个阶段不通过，是不能进入下一阶段的，这种按照不同阶段组织构建的方式，称之为构建流水线（Build Pipeline）。

一旦通过了各种验证，就会到构建流水线的最后一个阶段，生产发布。通常来说，生产发布这个过程不是自动化的。我们说，持续交付的关注点在于，让软件具备随时可以发布的能力，但并不等于它要立刻上线，所以，最后这一下，还要由人来决定，到底是不是要上线。

如果把由人决定的是否上线变成自动化的，就成了另外一个实践：持续部署。但通常人们都会比较谨慎，最后这一下还是由人拍板比较稳妥，所以，持续交付是现在的主流。

至此，我们讨论了持续交付的第一个方面，验证发布包。接下来，我们再来看看另外一个重要部分：部署。

DevOps

早期人们做部署都是自己编写 Shell 脚本完成的，但在上一讲中，我提到的一些工具，比如：Chef、Puppet、Ansible 等等，大幅度地简化了部署脚本的编写。这些工具在业界的兴起与一个概念息息相关：DevOps。

DevOps 是一种软件交付的理念和方法，目的是增强软件的可靠性。从名字便不难发现，DevOps 是将开发（Development）和运维（Operations）组合在了一起。

在传统的 IT 公司中，开发和运维往往是井水不犯河水的两个职位，甚至是两个不同的部门，由此带来了很多问题，比如，开发人员修改了配置，但没有通知运维，造成了新代码不能运行。

DevOps 提倡的就是将二者融合起来，打破壁垒。2009 年，Flickr 做了一个分享《每天部署 10 次》，整个行业受到了极大的冲击，从此 DevOps 运动风起云涌。DevOps 给这个行业带来的理念冲击是很大的，想要做好 DevOps，需要在文化、流程和工具等诸多方面不断改善。

但对我们程序员的日常工作来说，最直接的影响是体现在各种工具上。Chef、Puppet、Ansible 这些工具基本上都是在那之后，兴起或广为人知的。

在上一讲中，我给你讲了这些配置管理工具在运维体系中的角色，它们相当于提供了一个框架。但对于行业来说，这些工具给行业带来了部署的规范。

从前写 Shell 的方式，那就是各村有各村的高招。你在 A 公司学会的东西，到 B 公司是没法用的，甚至在很多人的印象中，部署这件事就应该属于某个特定的场景，换台机器脚本都要重新写过。这种形势就如同 Spring 出现之前，几乎所有的公司都在写自己的框架一样。

Spring 的出现打破这一切，让你的 Java 技能从归属于一个公司变成了行业通用。同样，运维体系中这些配置工具也起到了这样的作用。它们甚至带来了一个新的理念：基础设施即代码（Infrastructure as code），将计算机的管理与配置变成了代码。

一旦成了代码，就可以到处运行，可以版本管理，那种强烈依赖于“英雄”的机器配置工作终于可以平民化了。这在从前是想都不敢想的事。

这些工具采用的都是声明式接口，从 Shell 那种描述怎么做，到描述做什么，抽象程度上了一个台阶，让开发者或系统管理员从琐碎的细节中脱身，把更多的注意力用于思考应该把机器配置成什么样子。

如果这些配置管理工具还需要有一台具体的机器去部署，放在持续交付中，也只能扮演一个部署环境的次要角色，那 Docker 的出现则彻底地改变最终交付物。

我在上一讲说过，Docker 相当于是一台机器。Docker 非常好的一点是，它是一台可以用代码描述的机器，在 Docker 配置文件中描述的就是我们预期中那台机器的样子，然后，生成镜像，部署到具体的机器上。

既然是要描述机器的样子，我们就可以在 Docker 的配置文件中使用前面提到的配置工具，如此一来，我们的配置工作就简单了。那既然我们在讨论持续交付，还可以通过配置工具将我们的发布包也部署到最终的镜像中。这样一来，最终生成的镜像就是包含了我们自己应用的镜像。

你或许已经知道我要说什么了，结合着这些工具，我们的生成产物就由一个发布包变成了一个 Docker 镜像。

< 图：交付物的变迁 >

Docker 在开发中扮演的角色，是一个构建在我们应用与具体机器之间的中间层。对应用而言，它就是机器，但对机器而言，它只是一个可以部署的镜像，统一了各种应用千奇百怪的部署差异，让部署本身变得更简单了。

到这里，我给你介绍了持续交付中最基础的东西，让你有了一个基本的框架理解持续交付。当然，如果你关注这个领域，就会发现，它早已超出了一个实践的层面，有更多组织、文化的内容。

Jez Humble 写《持续交付》时就已经想到如此完整的一个体系，受限于当时的环境，书中介绍的自动化还比较宽泛，不像今天有更加完善的工具支撑。

只可惜，虽然当时他对持续交付的理解已经到达如此高度，他所在的团队也做出了一个颇具先锋气质的持续交付工具，但是受限于产品推广策略，这个工具并没有成为主流，即便后来开源了。（如果你想了解一下这个工具，可以点击链接去查看）

总结时刻

总结一下今天的内容。我们延续了前两讲的内容，在准备好发布包和部署的基础设施之后，我们顺着持续集成的思路，将部署过程也加了进来，这就是持续交付。

持续交付，是一种让软件随时处于可以部署到生产环境的能力。让软件具备部署到生产环境的能力，这里面有两个关键点：验证发布包和部署。

验证发布包，不仅是功能上的验证，还包括与环境结合在一起的验证。所以，通常会用几个不同的环境验证，每一个环境都是一个单独的阶段，一个阶段不通过，是不能进入下一阶段的，这种按照不同阶段组织构建的方式，称之为构建流水线（Build Pipeline）。

与部署相关的一个重要概念是 DevOps，也就是将开发和运维结合起来。DevOps 包含了很多方面，对程序员最直接的影响是各种工具的发展，这些工具推动着另一个理念的发展：基础设施即代码（Infrastructure as code） 。有赖于这些工具的发展，今天定义交付，就不再是一个发布包，而是一个可以部署的镜像。

如果今天的内容你只能记住一件事，那请记住：将部署纳入开发的考量。

最后，我想请你分享一下，你对持续交付的理解是什么样的呢？欢迎在留言区写下你的想法。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

33 | 如何做好验收测试？

经过前面三讲的讲解，相信你对一个项目自动化应该是什么样子有了一个相对完整的认识：程序员写好程序，用构建脚本执行检查，提交代码，在服务器上打出一个发布镜像，部署到各个环境进行检查，检查好了，随时可以发布上线。

我们在前面的内容中只说了该检查，但怎么检查呢？这就轮到测试发挥作用了。

在“任务分解”的模块，我给你完整地介绍了一下开发者测试的概念，但在那个部分讲解的测试基本上还停留在单元测试和集成测试的范畴。对于整个应用该怎么测，我们并没有仔细讨论。

今天我们就来说说应用测试的话题：验收测试。

验收测试

验收测试（Acceptance Testing），是确认应用是否满足设计规范的测试。这种测试往往是站在用户的角度，看整个应用能否满足业务需求。

从名字上来看，验收应该是业务人员的事，但业务人员能做的最多只是验收，测试是他们无论如何也不太可能做仔细的。

所以，验收测试这件事，往往还是由技术团队自己完成，而且在很多公司，这就是测试人员的事。

时至今日，很多测试团队都拥有自动化的能力。所以，自动化验收测试自然是重点的考虑对象。今天，我们的重点就是怎么做好自动化的验收测试。

其实，验收测试应该是人们最早想到的自动化测试，早在单元测试还不流行的年代，人们就开始了对自动化验收测试的探索。有不少团队甚至还构建了自己的框架，只不过，这种框架不是我们今天理解的测试框架，而是针对着一个应用的测试框架。

比如，我曾经见过有人为通信软件构建的一套完整的测试框架，甚至构建了属于自己的语言，测试人员的工作就是用这种特定的语言，对系统进行设置、运行，看它是否满足自己的预期。

相对来说，他们的这种做法已经非常成熟了。但更多团队的现实情况是，自己把对应用的访问做一个简单的封装，然后，写测试就是编写代码调用这个封装。

让验收测试从各自为战的混乱中逐渐有了体系的是行为驱动开发（Behavior Driven Development）这个概念的诞生，也就是很多人知道的 BDD。

行为驱动开发

行为驱动开发中的行为，指的是业务行为。BDD 希望促进业务人员与开发团队之间的协作，换句话说，如果你想做 BDD，就应该用业务语言进行描述。

这与我们传统上理解的系统测试有着很大的差别，传统意义上的系统测试是站在开发团队的角度，所以，更多的是在描述系统与外部系统之间的交互，用的都是计算机的术语。

而 BDD 则让我们换了一个视角，用业务语言做系统测试，所以，它是一个更高级别的抽象。

BDD 是 2003 年由 Dan North 提出了来的。Dan North 不仅仅提出了概念，为了践行他的想法，他还创造了第一个 BDD 的框架：JBehave。后来又改写出基于 Ruby 的版本 RBehave，这个项目后来被并到 RSpec 中。

今天最流行的 BDD 框架应该是 Cucumber，它的作者就是 RSpec 的作者之一 Aslak Hellesøy。

Cucunber 从最开始的 Ruby BDD 框架发展成今天支持很多不同程序设计语言的 BDD 测试框架，比如，常见的 Java、JavaScript、PHP 等等。

BDD 框架给我们最直观的感受就是它给我们提供的一套语言体系，供我们描述应用的行为，下面是一个例子，它描述了一个交易场景，应用需要根据交易结果判定是否要发出警告。你可以感受一下：

Scenario:  trader is not alerted below threshold

Given a stock of symbol STK1 and a threshold of 10.0

When the stock is traded at 5.0

Then the alert status should be OFF

Scenario:  trader is alerted above threshold

Given a stock of symbol STK1 and a threshold of 10.0

When the stock is traded at 11.0

Then the alert status should be ON

我们在这里的关注点是这个例子的样子，首先是描述格式：“Given…When…Then”，这个结构对应着这个测试用例中的执行步骤。Given 表示的一个假设前提，When 表示具体的操作，Then 则对应着这个用例要验证的结果。

还记得我们讲过的测试结构吗？前置准备、执行、断言和清理，这刚好与“Given…When…Then”做一个对应，Given 对应前置条件，When 对应执行，Then 则对应着断言。至于清理，它会做一些资源释放，属于实现层面的内容，在业务层面上意义不大。

了解了格式，我们还要关心一下内容。你会看到这里描述的行为都是站在业务的角度进行叙述的，而且 Given、When、Then 都是独立的，可以自由组合。也就是说，一旦基础框架搭好了，我们就可以用这些组成块来编写新的测试用例，甚至可以不需要技术人员参与。

不过，这些内容都是站在业务角度的描述，没有任何实现的内容，那实现的内容放在哪呢？

我们还需要定义一个胶水层，把测试用例与实现联系起来的胶水层，在 Cucumber 的术语里，称之为步骤定义（Step Definition）。这里我也给出了一个例子，你可以参考一下：

public class TraderSteps implements En {

    private Stock stock;

    public TraderSteps() {

        Given("^a stock of symbol {string} and a threshold of {double}", (String symbol, double threshold) -> {

            stock = new Stock(symbol, threshold);

        });

        When("^the stock is traded at {double}$", (double price) -> {

            stock.tradeAt(price);

        });

        Then("the alert status should be {string}", (String status) -> {

            assertThat(stock.getStatus().name()).isEqualTo(status);

        })

    }

}

写好验收测试用例

有了对 BDD 框架的基本了解，接下来的问题就是，怎么用好 BDD 框架。我们举个简单的例子，如果我们要写一个登录的测试用例，你会怎么写呢？

有一种写法是这样的，为了方便叙述，我把它转成了中文描述的格式，Cucumber 本身是支持本地化的，你可以使用自己熟悉的语言编写用例：

假定 张三是一个注册用户，其用户名密码是分别是 zhangsan 和 zspassword

当 在用户名输入框里输入 zhangsan，在密码输入框里输入 zspassword

并且 点击登录

那么 张三将登录成功

这个用例怎么样呢？或许你会说，这个用例挺好的。如果你这么想，说明你是站在程序员的视角。我在前面已经说过了，BDD 需要站在业务的角度，而这个例子完全是站在实现的角度。

如果登录方式有所调整，用户输完用户名密码自动登录，不需要点击，那这个用例是不是需要改呢？下面我换了一种方式描述，你再感受一下：

假定 张三是一个注册用户，其用户名密码是分别是 zhangsan 和 zspassword

当 用户以用户名 zhangsan 和密码 zspassword 登录

那么 张三将登录成功

这是一个站在业务视角的描述，除非做业务的调整，不用用户名密码登录了，否则，这个用例不需要改变，即便实现的具体方式调整了，需要改变的也是具体的步骤定义。

所以，想写好 BDD 的测试用例，关键点在用业务视角描述。

编写验收测试用例的步骤定义时，还有一个人们经常忽略的点：业务测试的模型。很多人的第一直觉是，一个测试要啥模型？还记得我们讲好测试应该具备的属性吗？其中一点就是 Professional，专业性。想要写好测试，同写好代码是一样的，一个好的模型是不可或缺的。

这方面一个可以参考的例子是，做 Web 测试常用的一个模型：Page Object。它把对页面的访问封装了起来，即便你在写的是步骤定义，你也不应该在代码中直接操作 HTML 元素，而是应该访问不同的页面对象。

以前面的登录为例，我们可能会定义这样的页面对象：

public class LoginPage {

    public boolean login(String name, String password) {

      ...

    }

}

如此一来，在步骤定义中，你就不必关心具体怎么定位到输入框，会让代码的抽象程度得到提升。

当然，这只是一个参考，面对你自己的应用时，你要考虑构建自己的业务测试模型。

总结时刻

今天我和你分享了自动化验收测试的话题。验收测试（Acceptance Testing），是确认应用是否满足设计规范的测试。验收测试是技术交付必经的环节，只不过，各个团队实践水平有所差异，有的靠人工，有的用简单自动化，一些做得比较好的团队才有完善的自动化。

自动化验收测试也是一个逐步发展的过程，从最开始的各自为战，到后来逐渐形成了一个完整的自动化验收测试的体系。

今天，我以行为驱动开发（Behavior Driven Development，BDD）为核心，给你介绍了一种自动化验收测试的方式。这个在 2003 年由 Dan North 提出的概念已经成为了一套比较完善的体系，尤其是一些 BDD 框架的发展，让人们可以自己的项目中实践 BDD。

我以 Cucumber 为样例，给你介绍了 BDD 验收用例的编写方式，你知道“Given…When…Then”的基本格式，也知道了要编写步骤定义（Step Definition）将测试用例与实现连接起来。

我还给你介绍了编写 BDD 测试用例的最佳实践：用业务的视角描述测试用例。在编写步骤定义时，还要考虑设计自己的业务测试模型。

其实，验收测试的方法不止 BDD 一种，像实例化需求（Specification by Example，SbE）也是一种常见的方法。验收测试框架也不止 BDD 框架一类，像 Concordion 这样的工具甚至可以让你把一个验收用例写成一个完整的参考文档。

如果你有兴趣，可以深入地去了解。无论哪种做法，都是为了缩短业务人员与开发团队之间的距离，让开发变得更加高效。

如果今天的内容你只能记住一件事，那请记住：将验收测试自动化。

最后，我想请你分享一下，你的团队是怎么做验收测试的呢？欢迎在留言区分享你的做法。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

34 | 你的代码是怎么变混乱的？

前面几讲，我给你讲了开发过程的各种自动化，从构建、验证到上线部署，这些内容都是站在软件外部看的。从这一讲开始，我准备带领大家进入到软件内部。今天的话题就从写代码开始说起。

逐步腐化的代码

代码是程序员改造世界最直接的武器，却也是程序员抱怨最多的东西。为什么程序员会对代码如此不满呢？

你会抱怨写一段代码吗？你肯定不会，毕竟这是你养家糊口的本领，最基本的职业素养我们还是有的。那抱怨的是什么呢？是维护一段代码。

为什么维护代码那么难？因为通常来说，你维护的这段代码是有一定年龄的，所以，你总会抱怨前人没有好好写这段代码。

好，现在你拿到了一个新的需求，要在这段代码上添加一个新功能，你会怎么做呢？很多人的做法是，在原有的代码上添加一段新的逻辑，然后提交完工。

发现问题了吗？你只是低着头完成了一项任务，而代码却变得更糟糕了。如果我问你，你为什么这么做？你的答案可能是：“这段代码都这样了，我不敢乱改。”或者是：“之前就是这么写的，我只是遵循别人的风格在写。”

行业里有一个段子，对程序员最好的惩罚是让他维护自己三个月前写的代码。你一不小心就成了自己最讨厌的人。

从前，我也认为很多程序员是不负责任，一开始就没有把代码写好，后来，我才知道很多代码其实只是每次加一点。你要知道，一个产品一旦有了生命力，它就会长期存在下去，代码也就随着时间逐渐腐烂了。

而几乎每个程序员的理由都是一样的，他们也很委屈，因为他们只改了一点点。

这样的问题有解吗？一个解决方案自然就是我们前面说过的重构，但重构的前提是，你得知道代码驶向何方。对于这个问题，更好的答案是，你需要了解一些软件设计的知识。

SOLID 原则

提到软件设计，大部分程序员都知道一个说法“高内聚、低耦合”，但这个说法如同“期待世界和平”一样，虽然没错，但并不能很好地指导我们的具体工作。

人们尝试着用各种方法拆解这个高远的目标，而比较能落地的一种做法就是 Robert Martin 提出的面向对象设计原则：SOLID，这其实是五个设计原则的缩写，分别是

单一职责原则（Single responsibility principle，SRP）

开放封闭原则（Open–closed principle，OCP）

Liskov 替换原则（Liskov substitution principle，LSP）

接口隔离原则（Interface segregation principle，ISP）

依赖倒置原则（Dependency inversion principle，DIP）

早在 1995 年，Robert Martin 就提出了这些设计原则的雏形，然后在他的《敏捷软件开发：原则、实践与模式》这本书中，比较完整地阐述了这五个原则。后来，他有把这些原则进一步整理，成了今天的 “SOLID”。

学习这些设计原则有什么用呢？

今天的程序员学习软件设计多半是从设计模式入门的，但不知道你是否有这样的感觉，在学习设计模式的时候，有几个设计模式看上去如此相像，如果不是精心比较，你很难记得住它们之间的细微差别。

而且，真正到了工作中，你还能想得起来的可能就剩下几个最简单的模式了，比如工厂方法、观察者等等。

另外，有人常常“为赋新词强说愁”，硬去使用设计模式，反而会让代码变得更加复杂了。你会有一种错觉，我是不是学了一个假的设计模式，人人都说好的东西，我怎么就感受不到呢？

初学设计模式时，我真的就被这个问题困扰了好久。直到我看到了 Robert Martin 的《敏捷软件开发：原则、实践与模式》。这是一本被名字糟蹋了的好书。

这本书出版之际，敏捷软件开发运动正风起云涌，Robert Martin 也不能免俗地蹭了热点，将“敏捷”挂到了书名里。其实，这是一本讲软件设计的书。

当我看到了 SOLID 的五个原则之后，我终于想明白了，原来我追求的方向错了。如果说设计模式是“术”，设计原则才是“道”。设计模式并不能帮你建立起知识体系，而设计原则可以。

当我不能理解“道”的时候，“术”只能死记硬背，效果必然是不佳的。想通这些之后，我大大方方地放弃了对于设计模式的追求，只是按照设计原则来写代码，结果是，我反而是时常能重构出符合某个设计模式的代码。至于具体模式的名字，如果不是有意识地去找，我已经记不住了。

当然，我并不是说设计模式不重要，之所以我能够用设计原则来写代码，前提条件是，我曾经在设计模式上下过很多功夫。

道和术，是每个程序员都要有的功夫，在“术”上下过功夫，才会知道“道”的价值，“道”可以帮你建立更完整的知识体系，不必在“术”的低层次上不断徘徊。

单一职责原则

好，下面我就单拿 SOLID 中单一职责原则稍微展开讲一下，虽然这个原则听上去是最简单的，但也有很多误解存在。

首先，什么是单一职责原则呢？如果读过《敏捷软件开发：原则、实践与模式》，你对单一职责的理解应该是，一个模块应该仅有一个修改的原因。

2017 年，Robert Martin 出版了《架构整洁之道》（Clean Architecture），他把单一职责原则的定义修改成“一个模块应该仅对一类 actor 负责”，这里的 actor 可以理解为对系统有共同需求的人。

不管是哪个定义，初读起来，都不是那么好理解。我举个例子，你就知道了。我这里就用 Robert Martin 自己给出的例子：在一个工资管理系统中，有个 Employee 类，它里面有三个方法：

calculatePay()，计算工资，这是财务部门关心的。

reportHours()，统计工作时长，这是人力部门关心的。

save()，保存数据，这是技术部门关心的。

之所以三个方法在一个类里面，因为它们的某些行为是类似的，比如计算工资和统计工作时长都需要计算正常工作时间，为了避免重复，团队引入了新的方法：regularHours()。

接下来，财务部门要修改正常工作时间的统计方法，但人力部门不需要修改。负责修改的程序员只看到了 calculatePay() 调用了 regularHours()，他完成了他的工作，财务部门验收通过。但上线运行之后，人力部门产生了错误的报表。

这是一个真实的案例，最终因为这个错误，给公司造成了数百万的损失。

如果你问程序员，为什么要把 calculatePay() 和 reportHours() 放在一个类里，程序员会告诉你，因为它们都用到了 Employee 这个类的数据。

但是，它们是在为不同的 actor 服务，所以，任何一个 actor 有了新的需求，这个类都需要改，它也就很容易就成为修改的重灾区。

更关键的是，很快它就会复杂到没人知道一共有哪些模块与它相关，改起来会影响到谁，程序员也就越发不愿意维护这段代码了。

我在专栏“开篇词”里提到过，人的大脑容量有限，太复杂的东西理解不了。所以，我们唯一能做的就是把复杂的事情变简单。

我在“任务分解”模块中不断强调把事情拆小，同样的道理在写代码中也适用。单一职责原则就是给了你一个指导原则，可以按照不同的 actor 分解代码。

上面这个问题，Robert Martin 给了一个解决方案，就是按照不同的 actor 将类分解，我把分解的结果的类图附在了下面：

编写短函数

好，你已经初步了解了单一职责原则，但还有一点值得注意。我先来问个问题，你觉得一个函数多长是合适的？

曾经有人自豪地向我炫耀，他对代码要求很高，超过 50 行的函数绝对要处理掉。

我在专栏中一直强调“小”的价值，能看到多小，就可以在多细的粒度上工作。单一职责这件事举个例子很容易，但在真实的工作场景中，你能看到一个模块在为多少 actor 服务，就完全取决于你的分解能力了。

回到前面的问题上，就我自己的习惯而言，通常的函数都在十行以内，如果是表达能力很强的语言，比如 Ruby，函数会更短。

所以，你可想而知我听到“把 50 行代码归为小函数”时的心情。我知道，“函数长短”又是一个非常容易引起争论的话题，不同的人对于这个问题的答案，取决于他看问题的粒度。

所以，不讨论前提条件，只谈论函数的长短，其实是没有意义的。

单一职责原则可以用在不同的层面，写一个类，你可以问问这些方法是不是为一类 actor 服务；写方法时，你可以问问这些代码是不是在一个层面上；甚至一个服务，也需要从业务上考虑一下，它在提供是否一类的服务。总之，你看到的粒度越细，也就越能发现问题。

总结时刻

今天，我讲的内容是软件设计，很多代码的问题就是因为对设计思考得不足导致的。

许多程序员学习设计是从设计模式起步的，但这种学法往往会因为缺乏结构，很难有效掌握。设计原则，是一个更好的体系，掌握设计原则之后，才能更好地理解设计模式这些招式。Robert Martin 总结出的“SOLID”是一套相对完整易学的设计原则。

我以“SOLID” 中的单一职责原则为例，给你稍做展开，更多的内容可以去看 Robert Martin 的书。不过，我也给你补充了一些维度，尤其是从“小”的角度告诉你，你能看到多小，就能发现代码里多少的问题。

如果今天的内容你只能记住一件事，那请记住：把函数写短。

最后我想请你思考一下，你是怎么理解软件设计的呢？欢迎在留言区写下你的想法。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

35 | 总是在说MVC分层架构，但你真的理解分层吗？

作为程序员，你一定听说过分层，比如，最常见的 Java 服务端应用的三层结构，在《15 | 一起练习：手把手带你分解任务》中，我曾提到过：

数据访问层，按照传统的说法，叫 DAO（Data Access Object，数据访问对象），按照领域驱动开发的术语，称之为 Repository；

服务层，提供应用服务；

资源层，提供对外访问的资源，采用传统做法就是 Controller。

这几乎成为了写 Java 服务的标准模式。但不知道你有没有想过，为什么要分层呢？

设计上的分解

其实，分层并不是一个特别符合直觉的做法，符合直觉的做法应该是直接写在一起。

在编程框架还不是特别流行的时候，人们就是直接把页面和逻辑混在一起写的。如果你有机会看看写得不算理想的 PHP 程序，这种现象还是大概率会出现的。

即便像 Java 这个如此重视架构的社区，分层也是很久之后才出现的，早期的 JSP 和 PHP 并没有什么本质区别。

那为什么要分层呢？原因很简单，当代码复杂到一定程度，人们维护代码的难度就急剧上升。一旦出现任何问题，在所有一切都混在一起的代码中定位问题，本质上就是一个“大海捞针”的活。

前面讲任务分解的时候，我不断在强调的观点就是，人们擅长解决的是小问题，大问题怎么办？拆小了就好。

分层架构，实际上，就是一种在设计上的分解。

回到前面所说的三层架构，这是行业中最早普及的一种架构模式，最开始是 MVC，也就是 Model、View 和 Controller。

MVC 的概念起源于 GUI （Graphical User Interface，图形用户界面）编程，人们希望将图形界面上展示的部分（View）与 UI 的数据模型（Model）分开，它们之间的联动由 Controller 负责。这个概念在 GUI 编程中是没有问题的，但也仅限于在与 UI 有交互的部分。

很多人误以为这也适合服务端程序，他们就把模型部分误解成了数据库里的模型，甚至把它理解成数据库访问。于是，你会看到有人在 Controller 里访问数据库。

不知道你是不是了解 Ruby on Rails，这是当年改变了行业认知的一个 Web 开发框架，带来很多颠覆性的做法。它采用的就是这样一种编程模型。当年写 Rails 程序的时候我发现，当业务复杂到了一定规模，代码就开始难以维护了。我想了好久，终于发现，在 Rails 的常规做法中少了服务层（Service）的设计。

这个问题在 Java 领域，爆发得要比 Rails 里早，因为 Ruby 语言的优越性，Rails 实现的数据访问非常优雅。正是因为 Rails 的数据访问实在太容易了，很多服务实际上写到 Model 层里。在代码规模不大时，代码看上去是不复杂的，甚至还有些优雅。

而那时的 Java 可是要一行一行地写数据访问，所以，代码不太可能放在 Model 层，而放在 Controller 里也会让代码变复杂，于是，为业务逻辑而生的 Service 层就呼之欲出了。

至此，常见的 Java 服务端开发的基础就全部成型了，只不过，由于后来 REST 服务的兴起，资源层替代了 Controller 层。

到这里，我给你讲了常见的 Java 服务三层架构的来龙去脉。但实际上，在软件开发中，分层几乎是无处不在的，因为好的分层往往需要有好的抽象。

无处不在的分层

作为程序员，我们几乎每天都在与分层打交道。比如说，程序员都对网络编程模型很熟悉，无论是 ISO 的七层还是 TCP/IP 的五层。

但不知道你有没有发现，虽然学习的时候，你要学习网络有那么多层，但在使用的时候，大多数情况下，你只要了解最上面的那层，比如，HTTP。

很多人对底层的协议的理解几乎就停留在“学过”的水平上，因为在大多数情况下，除非你要写协议栈，不然你很难用得到。即便偶尔用到，90% 的问题靠搜索引擎就解决了，你也很少有动力去系统学习。

之所以你可以这么放心大胆地“忽略”底层协议，一个关键点就在于，网络模型的分层架构实现得太好了，好到你作为上层的使用者几乎可以忽略底层。而这正是分层真正的价值：构建一个良好的抽象。

这种构建良好的抽象在软件开发中随处可见，比如，你作为一个程序员，每天写着在 CPU 上运行的代码，但你读过指令集吗？你之所以可以不去了解，是因为已经有编译器做好了分层，让你可以只用它们构建出的“抽象”——编程语言去思考问题。

比如，每天写着 Java 程序的程序员，你知道 Java 程序是如何管理内存的吗？这可是令很多 C/C++ 程序员寝食难安的问题，而你之所以不用关心这些，正是托了 Java 这种“抽象”的福。对了，你甚至可能没有注意到编程语言也是一种抽象。

有抽象有发展

只有构建起抽象，人们才能在此基础上做出更复杂的东西。如果今天的游戏依然是面向显示屏的像素编程，那么，精彩的游戏视觉效果就只能由极少数真正的高手来开发。我们今天的大部分游戏应该依然停留在《超级玛丽》的水准。

同样，近些年前端领域风起云涌，但你是否想过，为什么 Web 的概念早就出现了，但前端作为一个专门的职位，真正的蓬勃发展却是最近十年的事？

2009 年，Ryan Dahl 发布了 Node.js，人们才真正认识到，原来 JavaScript 不仅仅可以用于浏览器，还能做服务器开发。

于是，JavaScript 社区大发展，各种在其他社区已经很流行的工具终于在 JavaScript 世界中发展了起来。正是有了这些工具的支持，人们才能用 JavaScript 构建更复杂的工程，前端领域才能得到了极大的发展。

如今，JavaScript 已经发展成唯一一门全平台语言，当然，发展最好的依然是在它的大本营：前端领域。前端程序员才有了今天幸福的烦恼：各种前端框架层出不穷。

在这里，Node.js 的出现让 JavaScript 成为了一个更好的抽象。

构建你的抽象

理解了分层实际上是在构建抽象，你或许会关心，我该怎么把它运用在自己的工作中。

构建抽象，最核心的一步是构建出你的核心模型。什么是核心模型呢？就是表达你业务的那部分代码，换句话说，别的东西都可以变，但这部分不能变。

这么说可能还是有点抽象，我们回到前面的三层架构。

在前面介绍三层架构的演变时，提到了一个变迁：REST 服务的兴起，让 Controller 逐渐退出了历史舞台，资源层取而代之。

换句话说，访问服务的方式可能会变。放到计算机编程的发展中，这种趋势就更明显了，从命令行到网络，从 CS（Client-Server） 到 BS（Browser-Server），从浏览器到移动端。所以，怎么访问不应该是你关注的核心。

同样， 关系型数据库也不是你关注的核心，它只是今天的主流而已。从前用文件，今天还有各种 NoSQL。

如此说来，三层架构中的两层重要性都不是那么高，那重要的是什么？答案便呼之欲出了，没错，就是剩下的部分，我们习惯上称之为服务层，但这个名字其实不能很好地反映它的作用，更恰当的说法应该可以叫领域模型（Domain Model）。

它便是我们的核心模型，也是我们在做软件设计时，真正应该着力的地方。

为什么叫“服务层”不是一个好的说法呢？这里会遗漏领域模型中一个重要的组成部分：领域对象。

很多人理解领域对象有一个严重的误区，认为领域对象属于数据层。数据存储只是领域对象的一种用途，它更重要的用途还是用在各种领域服务中。

由此还能引出另一个常见的设计错误，领域对象中只包含数据访问，也就是常说的 getter 和 setter，而没有任何逻辑。

如果只用于数据存储，只有数据访问就够了，但如果是领域对象，就应该有业务逻辑。比如，给一个用户修改密码，用户这个对象上应该有一个 changePassword 方法，而不是每次去 setPassword。

严格地说，领域对象和存储对象应该是两个类，只不过它俩实在太像了，很多人经常使用一个类，这还是个小问题。但很多人却把这种内部方案用到了外部，比如，第三方集成。

为数不少的团队都在自己的业务代码中直接使用了第三方代码中的对象，第三方的任何修改都会让你的代码跟着改，你的团队就只能疲于奔命。

解决这个问题最好的办法就是把它们分开，你的领域层只依赖于你的领域对象，第三方发过来的内容先做一次转换，转换成你的领域对象。这种做法称为防腐层。

当我们把领域模型看成了整个设计的核心，看待其他层的视角也会随之转变，它们只不过是适配到不同地方的一种方式而已，而这种理念的推广，就是一些人在说的六边形架构。

怎么设计好领域模型是一个庞大的主题，推荐你去了解一下领域驱动设计（Domain Driven Design，DDD），这个话题我们后面还会再次提到。

讨论其实还可以继续延伸下去，已经构建好的领域模型怎么更好地提供给其他部分使用呢？一个好的做法是封装成领域特定语言（Domain Specific Language，DSL）。当然，这也是一个庞大的话题，就不继续展开了。

总结时刻

我从最常见的服务端三层架构入手，给你讲了它们的来龙去脉。分层架构实际是一种设计上的分解，将不同的内容放在不同的地方，降低软件开发和维护的成本。

分层，更关键的是，提供抽象。这种分层抽象在计算机领域无处不在，无论是编程语言，还是网络协议，都体现着分层抽象的价值。有了分层抽象，人们才能更好地在抽象的基础上构建更复杂的东西。

在日常工作中，我们应该把精力重点放在构建自己的领域模型上，因为它才是工作最核心、不易变的东西。

如果今天的内容你只能记住一件事，那请记住：构建好你的领域模型。

最后我想请你思考一下，你还知道哪些技术是体现分层抽象的思想吗？欢迎在留言区写下你的想法。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

36 | 为什么总有人觉得5万块钱可以做一个淘宝？

今天，我们从软件行业的一个段子说起。

甲方想要做个电商网站，作为乙方的程序员问：“你要做个什么样的呢？”甲方说：“像淘宝那样就好。”程序员问：“那你打算出多少钱？”甲方想了想，“5 万块钱差不多了吧！”

这当然是个调侃客户不懂需求的段子，但你有没有想过，为什么在甲方看来并不复杂的系统，你却觉得困难重重呢？

因为你们想的根本不是一个东西。

在客户看来，我要的不就是一个能买东西的网站吗？只要能上线商品，用户能看到能购买不就好了，5 万块钱差不多了。

而你脑中想的却是，“淘宝啊，那得是多大的技术挑战啊，每年一到‘双 11’，那就得考虑各种并发抢购。淘宝得有多少程序员，5 万块你就想做一个，门都没有。”

如果放在前面“沟通反馈”的模块，我可能会讲双方要怎么协调，把想法统一了。但到了“自动化”的模块，我想换个角度讨论这个问题：系统是怎么变复杂的。

淘宝的发展历程

既然说到了淘宝，我们就以一些公开资料来看看淘宝的技术变迁过程。2013 年，子柳出版了一本《淘宝技术这十年》，这本书里讲述了淘宝是怎么一步步变化的。

按照书中的说法，第一个淘宝是“买来的”，买的是一个叫做 PHPAuction 的系统，即便选择了最高配，也才花了 2000 美元左右。这是一个采用 LAMP 架构的系统，也就是 Linux + Apache + MySQL + PHP，这在当年可是典型的开源架构。

团队所做的主要就是一些订制化工作，最大的调整就是将单一数据库的读写进行了拆分，变成了一个主库和两个从库。这种结构在今天来看，依然是很多团队做调整的首选。

当访问量和数据量不断提升，MySQL 数据库率先抗不住了。当年的 MySQL 默认采用的是 MyISAM 引擎，写数据的时候会锁住表，读也会被卡住，当然，这只是诸多问题中的一个。

2003 年底，团队将 MySQL 换成了 Oracle。由于 Oracle 的性能要好上许多，主从的数据库架构又改回了单一数据库。但由于 PHP 访问数据库的缺省方案没有连接池，只好找了开源的 SQL Relay，这也为后续的改进埋下了伏笔。

当数据量继续加大，本地存储就已经无法满足了，只能通过引入网络存储解决问题。数据量进一步增大之后，存储节点一拆再拆，依然不能解决问题，淘宝就踏上了购买小型机的道路。

IBM 的小型机、Oracle 的数据库和 EMC 的存储，这个阶段就踏上了 IOE 之路。

2004 年初，SQL Relay 已经成了一个挥之不去的痛点，于是，只能从更根本的方案上动脑筋：更换程序设计语言。作为当时的主流，Java 成了不二之选。

替换的方案就是给业务分模块，一块一块地替换。老模块只维护，不增加新功能，新功能只在新模块开发，新老模块共用数据库。新功能上线，则关闭老模块对应功能，所有功能替换完毕，则老模块下线。

淘宝的数据量继续增长，单台 Oracle 很快到了上限，团队采用了今天常见的“分库分表”模式，但“分库分表”就会带来新的问题，跨数据库的数据怎么整合？于是，打造出了一个 DBRoute，用以处理分库的数据。

但是，这种做法也带来了一个新的问题，同时连接多个数据库，任何一个数据库出了问题，都会导致整个网站的故障。

当淘宝的数据量再次增长，每次访问都到了数据库，数据库很难承受。一个解决方案就是引入缓存和 CDN（Content Delivery Network，内容分发网络），这样，只读数据的压力就从数据库解放了出来。

当时的缓存系统还不像今天这么成熟，于是，团队基于一个开源项目改出了一个。他们用的 CDN 最开始是一个商用系统，但流量的增加导致这个系统也支撑不住了，只好开始搭建自己的 CDN。

后来，因为 CDN 要消耗大量的服务器资源，为了降低成本，淘宝又开始研发自己的低功耗服务器。

随着业务的不断发展，开发人员越来越多，系统就越来越臃肿，耦合度也逐渐提升，出错的概率也逐渐上升。这时，不得不对系统进行分解，将复用性高的模块拆分出来，比如，用户信息。

业务继续发展，拆分就从局部开始向更大规模发展，底层业务和上层流程逐渐剥离，并逐渐将所有业务都模块化。

有了一个相对清晰地业务划分之后，更多的底层业务就可以应用于不同的场景，一个基础设施就此成型，新的业务就可以使用基础设施进行构建，上层业务便如雨后春笋一般蓬勃发展起来。

在这个过程中，有很多技术问题在当时还没有好的解决方案，或者是不适于他们所在的场景。所以，淘宝的工程师就不得不打造自己的解决方案，比如：分布式文件系统（TFS）、缓存系统（Tair）、分布式服务框架（HSF）等等。还有一些技术探索则是为了节省成本，比如，去 IOE 和研发低功耗服务器等等。

我这里以淘宝网站的发展为例，做了一个快速的梳理，只是为了让你了解一个系统的发展，如果你有兴趣了解更多细节，不妨自己找出这本书读读。当然，现在的淘宝肯定比这更加完整复杂。

同样的业务，不同的系统

为什么我们要了解一个系统的演化过程呢？因为作为程序员，我们需要知道自己面对的到底是一个什么样的系统。

回到我们今天的主题上，5 万块钱可以不可以做一个淘宝？答案是，取决于你要的是一个什么样的系统。最开始买来的“淘宝”甚至连 5 万块钱都不用，而今天的淘宝和那时的淘宝显然不是一个系统。

从业务上说，今天的淘宝固然已经很丰富了，但最核心的业务相差并不大，无非是卖家提供商品，买家买商品。那他们的本质差别在哪呢？

回顾上面的过程，你就可以看到，每次随着业务量的增长，原有技术无法满足需要，于是，就需要用新的技术去解决这个问题。这里的关键点在于：不同的业务量。

一个只服务于几个人的系统，单机就够了，一个刚刚入行的程序员也能很好地实现这个系统。而当业务量到达一台机器抗不住的时候，就需要用多台机器去处理，这个时候就必须考虑分布式系统的问题，可能就要适当地引入中间件。

而当系统变成为海量业务提供服务，就没有哪个已经打造好的中间件可以提供帮助了，需要自己从更底层解决问题。

虽然在业务上看来，这些系统是一样的，但在技术上看来，在不同的阶段，一个系统面对的问题是不同的，因为它面对业务的量级是不同的。更准确地说，不同量级的系统根本就不是一个系统。

只要业务在不断地发展，问题就会不断出现，系统就需要不断地翻新。我曾听到一个很形象的比喻：把奥拓开成奥迪。

你用对技术了吗？

作为一个程序员，我们都知道技术的重要性，所以，我们都会努力地去学习各种各样的新技术。尤其是当一个技术带有大厂光环的时候，很多人都会迫不及待地去学习。

我参加过很多次技术大会，当大厂有人分享的时候，通常都是人山人海，大家都想学习大厂有什么“先进”技术。

知道了，然后呢？

很多人就想迫不及待地想把这些技术应用在自己的项目中。我曾经面试过很多程序员，给我讲起技术来滔滔不绝，说什么自己在设计时考虑各种分布式的场景，如果系统的压力上来时，他会如何处理。

我就好奇地问了一个问题，“你这个系统有多少人用？”结果，他做的只是一个内部系统，使用频率也不高。

为了技术而技术的程序员不在少数，过度使用技术造成的结果就是引入不必要的复杂度。即便用了牛刀杀鸡，因为缺乏真实场景检验，也不可能得到真实反馈，对技术理解的深度也只能停留在很表面的程度上。

在前面的例子中，淘宝的工程师之所以要改进系统，真实的驱动力不是技术，而是不断攀升的业务量带来的问题复杂度。

所以，评估系统当前所处的阶段，采用恰当的技术解决，是我们最应该考虑的问题。

也许你会说，我做的系统没有那么大的业务量，我还想提高技术怎么办？答案是到有好问题的地方去。现在的 IT 行业提供给程序员的机会很多，找到一个有好问题的地方并不是一件困难的事，当然，前提条件是，你自己得有解决问题的基础能力。

总结时刻

今天，我以淘宝的系统为例，给你介绍了一个系统逐渐由简单变复杂的发展历程，希望你能认清不同业务量级的系统本质上就不是一个系统。

一方面，有人会因为对业务量级理解不足，盲目低估其他人系统的复杂度；另一方面，也有人会盲目应用技术，给系统引入不必要的复杂度，让自己陷入泥潭。

作为拥有技术能力的程序员，我们都非常在意个人技术能力的提升，但却对在什么样情形下，什么样的技术更加适用考虑得不够。采用恰当的技术，解决当前的问题，是每个程序员都应该仔细考虑的问题。

如果今天的内容你只能记住一件事，那请记住：用简单技术解决问题，直到问题变复杂。

最后，我想请你回想一下，你身边有把技术做复杂而引起的问题吗？欢迎在留言区写下你的想法。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

37 | 先做好DDD再谈微服务吧，那只是一种部署形式

在“自动化”模块的最后，我们来聊一个很多人热衷讨论却没做好的实践：微服务。

在今天做后端服务似乎有一种倾向，如果你不说自己做的是微服务，出门都不好意思和人打招呼。

一有技术大会，各个大厂也纷纷为微服务出来站台，不断和你强调自己公司做微服务带来的各种收益，下面的听众基本上也是热血沸腾，摩拳擦掌，准备用微服务拯救自己的业务。

我就亲眼见过这样的例子，几个参加技术大会的人回到公司，跟人不断地说微服务的好，说服了领导，在接下来大的项目改造中启用了微服务。

结果呢？一堆人干了几个月，各自独立开发的微服务无法集成。最后是领导站出来，又花了半个月时间，将这些“微服务”重新合到了一起，勉强将这个系统送上了线。

人家的微服务那么美，为什么到你这里却成了烂摊子呢？因为你只学到了微服务的形。

微服务

大部分人对微服务的了解源自 James Lewis 和 Martin Fowler 在 2014 年写的一篇文章，他们在其中给了微服务一个更清晰的定义，把它当做了一种新型的架构风格。

但实际上，早在这之前的几年，很多人就开始用“微服务”这个词进行讨论了。

“在企业内部将服务有组织地进行拆分”这个理念则脱胎于 SOA（Service Oriented Architecture，面向服务的架构），只不过，SOA 诞生自那个大企业操盘技术的年代，自身太过于复杂，没有真正流行开来。而微服务由于自身更加轻量级，符合程序员的胃口，才得以拥有更大的发展空间。

谈到微服务，你会想起什么呢？很多人对微服务的理解，就是把一个巨大的后台系统拆分成一个一个的小服务，再往下想就是一堆堆的工具了。

所以，市面上很多介绍微服务的内容，基本上都是在讲工具的用法，或是一些具体技术的讨论，比如，用 Spring Boot 可以快速搭建服务，用 Spring Cloud 建立分布式系统，用 Service Mesh 技术作为服务的基础设施，以及怎么在微服务架构下保证事务的一致性，等等。

确实，这些内容在你实现微服务时，都是有价值的。但必须先回答一个问题，我们为什么要做微服务？

对这个问题的标准回答是，相对于整体服务（Monolithic）而言，微服务足够小，代码更容易理解，测试更容易，部署也更简单。

这些道理都对，但这是做好了微服务的结果。怎么才能到达这个状态呢？这里面有一个关键因素，怎么划分微服务，也就是一个庞大的系统按照什么样的方式分解。

这是在很多关于微服务的讨论中所最为欠缺的，也是很多团队做“微服务”却死得很难看的根本原因。

不了解这一点，写出的服务，要么是服务之间互相调用，造成整个系统执行效率极低；要么是你需要花大力气解决各个服务之间的数据一致性。换句话说，服务划分不好，等待团队的就是无穷无尽的偶然复杂度泥潭。只有正确地划分了微服务，它才会是你心目中向往的样子。

那应该怎么划分微服务呢？你需要了解领域驱动设计。

领域驱动设计

领域驱动设计（Domain Driven Design，DDD）是 Eric Evans 提出的从系统分析到软件建模的一套方法论。它要解决什么问题呢？就是将业务概念和业务规则转换成软件系统中概念和规则，从而降低或隐藏业务复杂性，使系统具有更好的扩展性，以应对复杂多变的现实业务问题。

这听上去很自然，不就应该这么解决问题吗？并不然，现实情况可没那么理想。

在此之前，人们更多还是采用面向数据的建模方式，时至今日，还有许多团队一提起建模，第一反应依然是建数据库表。这种做法是典型的面向技术实现的做法。一旦业务发生变化，团队通常都是措手不及。

DDD 到底讲了什么呢？它把你的思考起点，从技术的角度拉到了业务上。

贴近业务，走近客户，我们在这个专栏中已经提到过很多次。但把这件事直接体现在写代码上，恐怕还是很多人不那么习惯的一件事。DDD 最为基础的就是通用语言（Ubiquitous Language），让业务人员和程序员说一样的语言。

这一点我在《21 | 你的代码为谁而写？》中已经提到过了。使用通用语言，等于把思考的层次从代码细节中拉到了业务层面。越高层的抽象越稳定，越细节的东西越容易变化。

有了通用语言做基础，然后就要进入到 DDD 的实战环节了。DDD 分为战略设计（Strategic Design）和战术设计（Tactical Design）。

战略设计是高层设计，它帮我们将系统切分成不同的领域，并处理不同领域的关系。我在前面的内容中给你举过“订单”和“用户”的例子。从业务上区分，把不同的概念放到不同的地方，这是从根本上解决问题，否则，无论你的代码写得再好，混乱也是不可避免的。而这种以业务的角度思考问题的方式就是 DDD 战略设计带给我的。

战术设计，通常是指在一个领域内，在技术层面上如何组织好不同的领域对象。举个例子，国内的程序员喜欢用 myBatis 做数据访问，而非 JPA，常见的理由是 JPA 在有关联的情况下，性能太差。但真正的原因是没有设计好关联。

如果能够理解 DDD 中的聚合根（Aggregate Root），我们就可以找到一个合适的访问入口，而非每个人随意读取任何数据。这就是战术设计上需要考虑的问题。

战略设计和战术设计讨论的是不同层面的事情，不过，这也是 Eric Evans 最初没有讲清楚的地方，导致了人们很长时间都无法理解 DDD 的价值。

走向微服务

说了半天，这和微服务有什么关系呢？微服务真正的难点并非在于技术实现，而是业务划分，而这刚好是 DDD 战略设计中限界上下文（Bounded Context）的强项。

虽然通用语言打通了业务与技术之间的壁垒，但计算机并不擅长处理模糊的人类语言，所以，通用语言必须在特定的上下文中表达，才是清晰的。就像我们说过的“订单”那个例子，交易的“订单”和物流的“订单”是不同的，它们都有着自己的上下文，而这个上下文就是限界上下文。

它限定了通用语言自由使用的边界，一旦出界，含义便无法保证。正是由于边界的存在，一个限界上下文刚好可以成为一个独立的部署单元，而这个部署单元就可以成为一个服务。

所以要做好微服务，第一步应该是识别限界上下文。

你也看出来了，每个限界上下文都应该是独立的，每个上下文之间就不应该存在大量的耦合，困扰很多人的微服务之间大量相互调用，本身就是一个没有划分好边界而带来的伪命题，靠技术解决业务问题，事倍功半。

有了限界上下文就可以做微服务了吧？且慢！

Martin Fowler 在写《企业应用架构模式》时，提出了一个分布式对象第一定律：不要分布对象。同样的话，在微服务领域也适用，想做微服务架构，首先是不要使用微服务。如果将一个整体服务贸然做成微服务，引入的复杂度会吞噬掉你以为的优势。

你可能又会说了，“我都把限界上下文划出来了，你告诉我不用微服务？”

还记得我在《30 | 一个好的项目自动化应该是什么样子的？》中提到的分模块吗？如果你划分出了限界上下文，不妨先按照它划分模块。

以我拙见，一次性把边界划清楚并不是一件很容易的事。大家在一个进程里，调整起来会容易很多。然后，让不同的限界上下文先各自独立演化。等着它演化到值得独立部署了，再来考虑微服务拆分的事情。到那时，你也学到各种关于微服务的技术，也就该派上用场了！

总结时刻

微服务是很多团队的努力方向，然而，现在市面上对于微服务的介绍多半只停留在技术层面上，很多人看到微服务的好，大多数是结果，到自己团队实施起来却困难重重。想要做好微服务，关键在于服务的划分，而划分服务，最好先学习 DDD。

Eric Evans 2003 年写了《领域驱动设计》，向行业介绍了 DDD 这套方法论，立即在行业中引起广泛的关注。但实话说，Eric 在知识传播上的能力着实一般，这本 DDD 的开山之作写作质量难以恭维，想要通过它去学好 DDD，是非常困难的。所以，在国外的技术社区中，有很多人是通过各种交流讨论逐渐认识到 DDD 的价值所在，而在国内 DDD 几乎没怎么掀起波澜。

2013 年，在 Eric Evans 出版《领域驱动设计》十年之后，DDD 已经不再是当年吴下阿蒙，有了自己一套比较完整的体系。Vaughn Vernon 将十年的精华重新整理，写了一本《实现领域驱动设计》，普通技术人员终于有机会看明白 DDD 到底好在哪里了。所以，你会发现，最近几年，国内的技术社区开始出现了大量关于 DDD 的讨论。

再后来，因为《实现领域驱动设计》实在太厚，Vaughn Vernon 又出手写了一本精华本《领域驱动设计精粹》，让人可以快速上手 DDD，这本书也是我向其他人推荐学习 DDD 的首选。

即便你学了 DDD，知道了限界上下文，也别轻易使用微服务。我推荐的一个做法是，先用分模块的方式在一个工程内，让服务先演化一段时间，等到真的觉得某个模块可以“毕业”了，再去开启微服务之旅。

如果今天的内容你只能记住一件事，那请记住：学习领域驱动设计。

最后，我想请你分享一下，你对 DDD 的理解是什么样的呢？欢迎在留言区写下你的想法。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

划重点 | “自动化”主题的重点内容回顾汇总

“自动化”模块终于全部更新完毕。至此，四个工作原则我已经给你全部介绍了一遍，相对而言，这个模块的内容比较“硬”，我也竭尽全力帮你串起更多知识的脉络，所以，信息量也是非常大的。希望你能够找到自己接下来努力的方向，不断提升自己的“硬实力”。

重点复习

在这个模块中，我们学习到了一些最佳实践。

持续交付

将生产部署纳入了开发的考量。

持续交付的基础设施通常包含持续集成环境、测试环境、预生产环境和生产环境。

构建流水线保证到了下游的交付物一定是通过上游验证的。

随着 Docker 的诞生，交付由发布包变成了 Docker 镜像。

DevOps

将开发和运维结合到一起。

环境配置工具上的进步，让基础设施即代码成了行业共识。

验收测试

验收测试要站在业务的角度编写。

BDD 是一种编写验收测试的方式。

Given…When…Then… 的描述给了一个描述业务的统一方式。

写好验收测试，需要构建测试模型。

SOLID 原则

设计模式背后的道理。

单一职责原则（Single responsibility principle，SRP）。

开放封闭原则（Open–closed principle，OCP）。

Liskov 替换原则（Liskov substitution principle，LSP）。

接口隔离原则（Interface segregation principle，ISP）。

依赖倒置原则（Dependency inversion principle，DIP）。

用好单一职责原则，前提条件是看待问题颗粒度要小。

DDD

它将思考的起点拉到了业务上。

DDD 分为战略设计和战术设计。

微服务

做好微服务的前提是划分好限界上下文。

微服务的第一步，不要划分微服务。

在这个模块中，我们还了解了一些重要的思路，让我们把工作做得更好。

程序员的三大美德：懒惰、急躁和傲慢（Laziness, Impatience and hubris）。

小心 NIH 综合症（Not Invented Here Syndrome）。

写好构建脚本，做好项目自动化。

参照 Java 知识体系，学习运维知识。

软件设计最基础的原则是“高内聚、低耦合”。

分层架构是一种设计上的分解。

不同业务量的系统本质上不是一个系统。

采用简单技术解决问题，直到问题变复杂。

实战指南

请谨慎地将工作自动化。
——《29 | “懒惰”应该是所有程序员的骄傲》

将你的工作过程自动化。
——《30 | 一个好的项目自动化应该是什么样子的？》

有体系地学习运维知识。
——《31 | 程序员怎么学习运维知识？》

将部署纳入开发的考量。
——《32 | 持续交付：有持续集成就够了吗？》

将验收测试自动化。
——《33 | 如何做好验收测试？》

把函数写短。
——《34 | 你的代码是怎么变混乱的？》

构建好你的领域模型。
——《35 | 总是在说 MVC 分层架构，但你真的理解分层吗？》

用简单技术解决问题，直到问题变复杂。
——《36 | 为什么总有人觉得 5 万块钱可以做一个淘宝？》

学习领域驱动设计。
——《37 | 先做好 DDD 再谈微服务吧，那只是一种部署形式》

额外收获

在这个模块的最后，针对大家在学习过程中的一些问题，我也进行了回答，帮你梳理出一个思路，更好地理解学到的内容：

持续集成的延伸。

持续集成完成系统集成。

持续交付完成可部署上线。

“持续验证”完成产品想法验证。

AB 测试，用一个软件的多个版本验证想法。

Selenium 用以完成浏览器的自动化。

熟练使用快捷键。

——《答疑解惑 | 持续集成、持续交付，然后呢？》

留言精选

在讲到 “懒惰”应该是所有程序员的骄傲时，jxin 同学提到：

有价值的事并不局限性于事情本身。做自动化很重要，写代码很重要。但根据现有情况判断是否需要自动化，是否需要写代码也很重要。有的放矢，任务分解。权衡跟设计是件很艺术的事情，令人着迷。

另外，关于持续交付，Jxin 同学也提出了自己的理解：

分而治之是解决复杂问题的一大利器。持续交互就像重构中小步快走（每次微调后运行测试代码验证），都能保证大工程的稳步前进。同时由于单元小了，所以也灵活了，持续交互可以结合最小产品的理念，以小成本做 test，收集数据后，即时调整产品发展方向。

关于软件设计， 毅 同学分享了自己的感悟：

我们常说任务到手不要着急去做，要从设计入手，把时间多花在前面。工作中发现大家都是思考了才动手的，那为什么越往后偏差越大呢？

共性原因有二：一是全局观不够，用咱们课里的话说就是上下文局限和反馈延迟（看到问题不提，直到代码写到那绕不过去了再沟通）；

二是没有领域的概念和有意识地去实践（纸上谈兵），尤其是做流程型任务，都喜欢先把表结构定义出来，再去生成实体，所以从领域层面来看这些实体就很不合适了。结果必然是用面向对象的工具写出了面向过程的代码，既然是面向过程那 OO 设计原则就鲜有用武之地了。这两点也是我个人理解要做好软件设计的两个必要条件。

讲到分层架构时， desmond 同学提到：

学了 REST 和 DDD，感觉两者有相通的地方：两者都以数据（一个是资源，另外一个是领域对象）为中心，并制定一套标准的数据操作（一个是 HTTP Verb，另外一个我项目主要用 JPA 这一套）；而核心是业务建模。

对于微服务的理解，风翱 同学提到：

公司说我们的开发方式是敏捷开发，实际上只是使用了一些敏捷开发的方法，只有遵守敏捷开发的价值观和原则，才能算是敏捷开发。微服务也是一样，不是说拆分成多个服务去部署，就叫做微服务。也不是采用市面上常用的微服务框架，就是微服务了。

对于一个好的项目自动化应该是什么样子这个问题，西西弗与卡夫卡 同学提到：

设想过这样的情景（还没实现，打算实践一把）：我们新招一名比较熟练的程序员，从 TA 入职拿到机器，到开发示意代码，再提交 SCM，然后 CI/CD，再发布到线上交付给用户，整个过程可以在入职当天的午饭之前完成。

这不光要求构建和集成自动化，甚至要求从入职开始的各个环节都能提前准备好，包括机器、开发环境、线上环境等，甚至连示范的需求都要能及时传递给 TA。理想情况下，程序员只需要开发好程序，保证质量，提交到 SCM 即可，其他事情都应该交给机器。

要知道程序员都很贵，越早给用户交付价值越好。

对于自动化验收测试， shniu 同学分享了他的学习感悟：

自动化验收测试确实是很好的东西，比如在回归测试，省去了很多的重复工作。但我理解 BDD 的初衷是驱动产品、业务、开发、测试等去深入讨论沟通需求，在还没有真的写代码的时候去实例化 story，并一起定义验收用例，让每个人对需求的理解都很透彻，当然特别注意的是要从统一的业务角度去描述，可见，真的做好 BDD 是需要不断的尝试和总结的。

对于“5 万块做淘宝”这个话题，enjoylearning 同学提到：

做一个淘宝那样的，客户指的是业务类似，但用户量多少，需要多少并发数，搜索性能等如何都是需要跟客户沟通后才能决定技术选型的。现实中我们的有些系统已经满足了业务需求，就没有必要为了追求技术复杂度而去拆分了，只有面向问题技术选型才会有成效。

关于运维知识，hua168 同学对文章内容进行了补充：

现在运维流行 DevOps，高级一点就是 AI，其中一篇文章《DevOps 详解》不错，链接如下：
https://infoq.cn/article/detail-analysis-of-devops

《DevOps 知识体系与标准化的构建》也不错，下载地址：
https://yq.aliyun.com/download/778

运维知识体系：
https://www.unixhot.com/page/ops

Web 缓存知识体系：
https://www.unixhot.com/page/cache

感谢同学们的精彩留言。在下一个模块中，我将结合具体的应用场景，将之前讲过的“思考框架”和“四个原则”进行综合应用的分析。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

加餐 | 你真的了解重构吗？

今天（3 月 15 日），Martin Fowler 《重构》第二版的中文版正式发布。前不久，人邮的杨海灵老师找到我，让我帮忙给这本书写推荐语，我毫不犹豫地就答应了，有机会为经典之作写推荐语，实属个人荣幸。

不过，我随即想到，在专栏里，我只是在谈 TDD 的时候提到了重构，并没有把它作为一个专门的话题来讲，于是，我决定给我的专栏读者加餐，专门谈谈重构，毕竟重构是几乎每个程序员都会用到的词汇。但你真的了解重构吗？

每个程序员都要做的事

作为程序员，我们都希望自己的代码是完美的。但没有代码是完美的，因为只要你的代码还有生命力，一定会有新的需求进来，而新的需求常常是你在编写这段代码之初始料未及的。

很多人直觉的选择是，顺着既有的代码结构继续写下去，这里添一个 if，那里加一个标记位，长此以往，代码便随时间腐坏了。

如果用一个物理学术语描述这种现象，那就是“熵增”，这也就是大名鼎鼎的热力学第二定律。如果没有外部干预，系统会朝着越来越混乱的方向发展。对抗熵增的一个办法就是引入负熵，让系统变得更加有序。而在代码中引入负熵的过程就是“重构”。

调整代码这件事是程序员都会有的习惯，但把这件事做到比较系统，上升为“重构”这个值得推广的实践是从一个小圈子开始的，这个小圈子的核心就是我们在专栏里前面提到过的两位大师级程序员：Ward Cunningham 和 Kent Beck。

而真正让这个概念走出小圈子，来到大众面前的，则是 Martin Fowler 在 1999 年写下那本软件行业的名著《重构：改善既有代码的设计》（Refactoring: Improving the Design of Existing Code）。

Martin Fowler 的本事就在于他极强的阐述能力，很多名词经过他的定义就会成为行业的流行语（Buzzword），重构就是其中之一。

重构这个说法可比“调整代码”听上去高级多了。时至今日，很多人都会把重构这个词挂在嘴边：“这个系统太乱了，需要重构一下。”

但遗憾的是，很多程序员对重构的理解是错的。

重构是一种微操作

你理解的重构是什么呢？就以前面那句话为例：这个系统太乱了，需要重构一下。如果我们接着问，你打算怎么重构呢？一些人就会告诉你，他们打算另立门户，重新实现这套系统。对不起，你打算做的事叫重写（rewrite），而不是重构（refactoring）。

《重构》是一本畅销书，但以我的了解，很少有人真正读完它，因为 Martin Fowler 是按照两本书（Duplex Book）来写的，这是他常用写书的风格，前半部分是内容讲解，后半部分是手册。

让这本书真正声名鹊起的就是前半部分，这部分写出了重构这件事的意义，而后半部分的重构手册很少有人会看完。很多人以为看了前半部分就懂了重构，所以，在他们看来，重构就是调整代码。调整代码的方法我有很多啊，重写也是其中之一。

如果真的花时间去看这本书的后半部分，你多半会觉得很无聊，因为每个重构手法都是非常细微的，比如，变量改名，提取方法等等。尤其是在今天，这些手法已经成了 IDE 中的菜单。估计这也是很多人就此把书放下，觉得重构不过如此的原因。

所以，行业里流传着各种关于重构的误解，多半是没有理解这些重构手法的含义。

重构，本质上就是一个“微操作”的实践。如果你不能理解“微操作”的含义，自然是无法理解重构的真正含义，也就不能理解为什么说“大开大合”的重写并不在重构的范畴之内。

我在《大师级程序员的工作秘笈》这篇文章中曾经给你介绍过“微操作”，每一步都很小，小到甚至在很多人眼里它都是微不足道的。

重构，也属于微操作的行列，与我们介绍的任务分解结合起来，你就能很好地理解那些重构手法的含义了：你需要把做的代码调整分解成若干可以单独进行的“重构”小动作，然后，一步一步完成它。

比如，服务类中有一个通用的方法，它并不适合在这个有业务含义的类里面，所以，我们打算把它挪到一个通用的类里面。你会怎么做呢？

大刀阔斧的做法一定是创建一个新的通用类，然后把这个方法复制过去，修复各种编译错误。而重构的手法就会把它做一个分解：

添加一个新的通用类，用以放置这个方法；

在业务类中，添加一个字段，其类型是新添加的通用类；

搬移实例方法，将这个方法移动到新的类里面。

得益于现在的 IDE 能力的增强，最后一步，按下快捷键，它就可以帮我们完成搬移和修改各处调用的工作。

在这个分解出来的步骤里，每一步都可以很快完成，而且，每做完一步都是可以停下来的，这才是微操作真正的含义。这是大刀阔斧做法做不到的，你修改编译错误的时候，你不知道自己需要修改多少地方，什么时候是一个头。

当然，这是一个很简单的例子，大刀阔斧的改过去也无伤大雅。但事实上，很多稍有规模的修改，如果不能以重构的方式进行，常常很快就不知道自己改到哪了，这也是很多所谓“重写”项目面临的最大风险，一旦开始，不能停止。

你现在理解了，重构不仅仅是一堆重构手法，更重要的是，你需要有的是“把调整代码的动作分解成一个个重构小动作”的能力。

重构地图

下面我准备给你提供一张关于重构的知识地图，帮你了解它与周边诸多知识之间的关系，辅助你更好地理解重构。

学习重构，先要知道重构的定义。关于这点，Martin Fowler 给出了两个定义，一个名词和一个动词。

重构（名词）：对软件内部结构的一种调整，目的是在不改变软件可观察行为的前提下，提高其可理解性，降低其修改成本。

重构（动词）：使用一系列重构手法，在不改变软件可观察行为的前提下，调整其结构。

之所以要了解重构的定义，因为重构的知识地图就是围绕着这个定义展开的。

首先，我们要对软件的内部结构进行调整，第一个要回答的问题是，我们为什么要调整。Martin Fowler 对于这个问题的回答是：代码的坏味道。

代码的坏味道，在我看来，是这本书给行业最重要的启发。很多人常常是无法嗅到代码坏味道的，因此，他们会任由代码腐坏，那种随便添加 if 或标记的做法就是嗅不出坏味道的表现。

我经常给人推荐《重构》这本书，但我也常常会补上一句，如果你实在没有时间，就去看它的第三章《代码的坏味道》。

顺便说一下，对比两版的《重构》，你会发现它们在坏味道的定义上有所差异，在新版的《重构》中，可变数据（Mutable Data）、循环语句（Loops）都定义成了坏味道，如果你不曾关注近些年的编程发展趋势，这样的定义着实会让人为之震惊。但只要了解了函数式编程的趋势，就不难理解它们的由来了。

换句话说，函数式编程已然成为时代的主流。如果你还不了解，赶紧去了解。

我们接着回到重构的定义上，重构是要不改变软件的可观察行为。我们怎么知道是不是改变了可观察行为，最常见的方式就是测试。

关于测试，我在“任务分解”模块已经讲了很多，你现在已经可以更好地理解重构、TDD 这些概念是怎样相互配合一起的了吧！

再来，重构是要提高可理解性，那重构到什么程度算是一个头呢？当年重构讨论最火热的时候，有人给出了一个答案：重构成模式（Refactoring to Patterns）。当然，这也是一本书的名字，有兴趣的话，可以找来读一读。

我个人有个猜想，如果这个讨论可以延续到 2008 年，等到 Robert Martin 的《Clean Code》出版，也许有人会提“重构成 Clean Code”也未可知。所以，无论是设计模式，亦或是 Clean Code，都是推荐你去学习的。

至此，我把重构的周边知识整理了一番，让你在学习重构时，可以做到不仅仅是只见树木，也可看见森林。当然，重构的具体知识，还是去看 Martin Fowler 的书吧！

总结时刻

总结一下今天的内容。今天我介绍了一个大家耳熟能详的概念：重构。不过，这实在是一个让人误解太多的概念，大家经常认为调整代码就是在做重构。

重构，本质上就是一堆微操作。重构这个实践的核心，就是将调整代码的动作分解成一个一个的小动作，如果不能理解这一点，你就很难理解重构本身的价值。

不过，对于我们专栏的读者而言，因为大家已经学过了“任务分解”模块，理解起这个概念，难度应该降低了很多。

既然重构的核心也是分解，它就需要大量的锤炼。就像之前提到任务分解原则一样，我在重构上也下了很大的功夫做了专门的练习，才能让自己一小步一小步地去做。但一个有追求的软件工匠不就应该这样锤炼自己的基本功吗？

如果今天的内容你只记住一件事，那请记住：锤炼你的重构技能。

最后，我想请你分享一下，你对重构的理解。欢迎在留言区写下你的想法。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

答疑解惑 | 持续集成、持续交付，然后呢？

“自动化”模块落下了帷幕，这是四个工作原则中最为“技术化”的一个，也应该是程序员们最熟悉的主题。

我从软件外部的自动化——工作流程讲起，让你能够把注意力专注于写好代码；讲到了软件内部的自动化——软件设计，选择恰当的做法，不贪图一时痛快，为后续的工作挖下深坑。

既然是一个大家都熟悉的话题，同学们自然也有很多经验分享，也有很多人看到了与自己不同的做法，提出了各种各样的问题。

在今天的答疑中，我选出了几个很有意思的问题，让大家可以在已有内容上再进一步延伸。

问题 1：持续交付是否可以再做扩展？

毅 同学提到

为达到有效交付的目标，用户能够尽早参与，我觉得也是比较重要的一环。从生产环境获得结果，是否可再做扩展，将用户也作为一个独立节点？
——《32 | 持续交付：有持续集成就够了吗？》

西西弗与卡夫卡 同学提到

持续交付可以是持续交付最大价值，那范围就不仅限于软件，还可以进一步延伸到运营，比如说结合 ABTest，自动选择最有效的运营策略，为用户交付最大价值。
——《32 | 持续交付：有持续集成就够了吗？》

两位同学能提出这样的想法，说明真的是已经理解了持续集成和持续交付，所以，才能在这个基础上继续延伸，思考进一步的扩展。

我在专栏中一直在强调，别把自己局限在程序员这个单一的角色中，应该了解软件开发的全生命周期。在前面的内容中，我讲了不少做产品的方法，比如，MVP、用户测试等等。如果只把自己定位在一个写代码的角色上，了解这些内容确实意义不大，但你想把自己放在一个更大的上下文中，这些内容就是必须要了解的。

回到两位同学的问题上，如果说我们一开始把持续集成定义成编写代码这件事的完成，那持续交付就把这个“完成”向前再推进了一步，只有上线的代码才算完成。

但放在整个软件的生命周期来说，上线并不是终点。把系统送上线，不是最终目的。那最终目的是什么呢？

回到思考的起点，我们为什么要做一个软件？因为我们要解决一个问题。那我们是不是真正的解决了问题呢？其实，我们还不知道。

在《06 | 精益创业：产品经理不靠谱，你该怎么办？》这篇文章中，我给你讲了做产品的源头。如果是采用精益创业的模式工作，我们构建产品的目的是为了验证一个想法，而怎么才算是验证了我们的想法呢？需要搜集各种数据作为证据。

所以，我曾经有过这样的想法，精益创业实际上是一种持续验证，验证想法的有效性，获得经过验证的认知（Validated Learning）。

现在有一些获取验证数据的方式，比如，西西弗与卡夫卡 同学提到的 AB 测试。

AB 测试是一种针对两个（或多个）变体的随机试验，常常用在 Web 或 App 的界面制作过程中，分别制作两个（或多个）版本，让两组（或多组）成分相同的用户随机访问不同版本，收集数据，用以评估哪个版本更好。每次测试时，最好只有一个变量。因为如果有多个变量，你无法确认到底是哪个变量在起作用。

AB 测试的概念在其他领域由来已久。2000 年，Google 的工程师率先把它应用在了软件产品的测试中，时至今日，它已经成为很多产品团队常用的做事方式。

AB 测试的前提是用户数据搜集。我在《09 | 你的工作可以用数字衡量吗？》这篇文章给你介绍了在开发过程中，用数字帮助我们改善工作。在产品领域实际上更需要用数字说话，说到这里，我“插播”一个例子。

很多产品经理喜欢讲理念、讲做法，偏偏不喜欢讲数字。用数字和产品经理沟通其实是更有说服力的。

我就曾经遇到过这样的事情，在一个交易平台产品中，一个产品经理创造性地想出一种新的订单类型，声称是为了方便用户，提高资金利用率。如果程序员接受这个想法，就意味着要对系统做很大的调整。

我问了他几个问题：第一，你有没有统计过系统中现有的订单类型的使用情况？第二，你有没有了解过其他平台是否支持这种订单类型呢？

产品经理一下子被我问住了。我对第一个问题的答案是，除了最基础的订单类型之外，其他的订单类型用得都很少，之前做的很多号称优化的订单类型，实际上没有几个人在用。

第二个问题我的答案是，只有极少数平台支持类似的概念。换句话说，虽然我们想得很美，但教育用户的成本会非常高，为了这个可能存在的优点，对系统做大改造，实在是一件投资大回报小的事，不值得！

再回到我们的问题上，一旦决定了要做某个产品功能，首先应该回答的是如何搜集用户数据。对于前端产品，今天已经有了大量的服务，只要在代码里嵌入一段代码，收集数据就是小事一桩。

前端产品还好，因为用户行为是比较一致的，买服务就好了，能生成标准的用户行为数据。对于后端的数据，虽然也有各种服务，但基本上提供的能力都是数据的采集和展示，一些所谓的标准能力只是 CPU、内存、JVM 之类基础设施的使用情况。对于应用来说，具体什么样的数据需要搜集，还需要团队自己进行设计。

说了这些，我其实想说的是，持续验证虽然是一个好的想法，但目前为止，还不如持续集成和持续交付这些已经有比较完整体系做支撑。想做到“持续”，就要做到自动化，想做到自动化，就要有标准化支撑，目前这个方面还是“八仙过海各显神通”的状态，没法上升到行业最佳实践的程度。

其实道理上也很简单，从一无所有，到持续集成、再到持续交付，最后到持续验证，每过一关，就会有大多数团队掉队。所以，真正能达到持续交付的团队都少之又少，更别提要持续验证了。

问题 2：Selenium 和 Cucumber 的区别是什么？

没有昵称 同学提到

老师，Selenium 跟 Cucumber 有区别吗？
——《33 | 如何做好验收测试？》

这是一个经常有人搞混的问题。为了让不熟悉的人理解，我先讲一点背景。

Selenium 是一个开源项目，它的定位是浏览器自动化，主要用于 Web 应用的测试。它最早是 Jason Huggins 在 2004 年开发出来的，用以解决 Web 前端测试难的问题。

之所以取了 Selenium 这个名字，主要是用来讽刺其竞争对手 Mercury 公司开发的产品。我们知道，Mercury 是水银，而 Selenium 是硒，硒可以用来解水银的毒。又一个程序员的冷幽默！

Cucumber 的兴起伴随着 Ruby on Rails 的蓬勃发展，我们在之前的内容中提到过，Ruby on Rails 是一个改变了行业认知的 Web 开发框架。所以，Cucumber 最初主要就是用在给 Web 应用写测试上，而 Selenium 刚好是用来操作浏览器的，二者一拍即合。

于是，你会在很多文章中看到，Cucumber 和 Selenium 几乎是同时出现的，这也是很多人对于二者有点傻傻分不清楚的缘由。

讲完了这些背景，结合我们之前讲的内容，你就不难理解了。Cucumber 提供的是一层业务描述框架，而它需要有自己对应的步骤实现，以便能够对被测系统进行操控；而 Selenium 就是在 Web 应用测试方面实现步骤定义的一个非常好的工具。

问题 3：IntelliJ IDEA 怎么学？

hua168 同学提到

IDEA 怎么学呢？是用到什么功能再学？还是先看个大概，用到时再仔细看？
——《30 | 一个好的项目自动化应该是什么样子的？》

一个工具怎么学？我的经验就是去用。我没有专门学过 IntelliJ IDEA，只是不断地在使用它。遇到问题就去找相应的解决方案。

如果说在 IDEA 上下过功夫，应该是在快捷键上。我最早写代码时的风格应该是鼠标与键盘齐飞，实话说，起初也没觉得怎么样。加入 ThoughtWorks 之后，看到很多人把快捷键运用得出神入化，那不是在写一行代码，而是在写一片代码。我当时有一种特别震惊的感觉。

我自以为在写代码上做得已经相当好了，然而，有人却在你很擅长的一件事上完全碾压了你，那一瞬间，我感觉自己这些年都白学了。这种感觉后来在看到别人能够小步重构时又一次产生了。

看到差距之后，我唯一能做的，就是自己下来偷偷练习。幸好，无论是快捷键也好，重构也罢，都是可以单独练习的。花上一段时间就可以提高到一定的水平。后来，别人看我写代码时也会有类似的感觉，我会安慰他们说，不要紧，花点时间练习就好。

其实，也有一些辅助的方法可以帮助我们练习，比如，我们会给新员工发放 IntelliJ IDEA 的快捷键卡片，写代码休息之余，可以拿来看一下；再比如，IntelliJ IDEA 有一个插件叫 Key Promoter X，如果你用鼠标操作，它会给你提示，帮你记住快捷键。有一段时间，我已经练习到“看别人写代码，脑子里能够完全映射出他在按哪个键”的程度。

写代码是个手艺活，要想打磨手艺，需要看到高手是怎么工作的，才不致于固步自封。如果你身边没有这样的人，不如到网上搜一些视频，看看高手在写代码时是怎么做的，这样才能找到差距，不断提高。

好，今天的答疑就到这里，你对这些问题有什么看法呢？欢迎在留言区写下你的想法。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

38 | 新入职一家公司，怎么快速进入工作状态？

经过前面几个模块的学习，我们分别领略了各个原则在不同场景下的应用，相信你对于这些原则的理解也上了一个台阶。但实际工作并不会清晰地告诉你，到底该运用哪个原则来解决问题。

所以，在接下来的三讲中，我挑选了程序员职业生涯中三个非常经典的场景，与你一起看看怎么在实际的工作中运用好已经学习到的这些原则。

在综合运用这个模块的第一讲，我们就来谈谈，当你加入一家新公司时，应该怎么做。

IT 行业快速发展，无数的机会涌现了出来，程序员频繁流动是这个行业的一个典型特征。频繁换工作，无论是对公司，还是对个人都是成本很高的一件事。所以，在加入一个新公司时，怎么让自己快速融入，尽快发挥价值，是摆在我们面前的一个重要问题。

以行业标准来看，我换工作的速度是很低的，但因为之前工作的原因，我需要到不同的公司与不同的人合作，每到一个新公司，工作的内容就是全新的，就如同换了一个新工作一般。因为合作周期有限，我不可能像普通员工入职新公司一样，花几个月时间慢慢熟悉，只能在尽可能短的时间内，快速上手，而且还要提出自己的新想法。

那我是怎么做的呢？其实，我就是运用这个专栏里提到的各种方法解决这个问题。下面我就来分享一下具体的做法。

运用思考框架

还记得专栏之初我提出的思考框架吗？我们要问三个问题：

Where are we?（我们现在在哪？）

Where are we going?（我们要到哪儿去？）

How can we get there?（我们如何到达那里？）

先来看第一个问题，如果刚刚加入一家公司，哪怕我们不是一脸懵，也只是对公司业务有一个简单地了解，这是我们的现状。

第二个问题来看看我们的目标。一般来说，我们都是打算在新公司大展身手，但这个答案太宽泛了，我们还需要进一步细化。在这个公司长远的发展是以后的事，我们还是把第一步的目标制定成能够达到上手工作的程度，比如，能够解决日常的项目问题。

那接下来，我们就需要回答第三个问题了，怎么才能够达到这个目标呢？我们需要做一个分解。

你可以回想一下过往的工作经验，要在一个项目上工作起来，先要了解什么呢？很多人的第一反应是技术，我是程序员嘛，当然就是技术优先了。估计大多数人进到项目里，都是一头奔进代码里，然后，从各种细节研究起来。技术肯定是你要了解的，但它不应该是第一位的。

技术解决的是“怎么做”的问题，而我们第一个应该了解的问题是“做什么”。一个软件到底在做什么，能够回答这个问题的就是业务。所以，我们排在第一优先级的事情应该是业务。

了解业务和技术都只是让你扮演好你个人的角色，但我们通常都是在一个团队内工作的，所以，还要解决好与其他人协作的问题，这就需要我们了解团队本身是如何运作的。

好，我们已经将大目标做了一个分解，得到了三个小目标：

业务；

技术；

团队运作。

从大图景入手

接下来，我们来针对每一个目标，进一步看看需要了解哪些内容。

业务

首先是业务。这是程序员入手新项目时最容易忽略的点。在这个专栏中，我在不同的模块中都说到了知识结构的重要性，没有结构的知识是零散的。所以，不管做任何项目，都要先从大图景入手。只有了解了大图景，各种知识才能各归其位。

对于一个普通的程序员来说，业务就是这个大图景。

如果你了解了业务，你自己就可以推演出基本的代码结构。但反过来，如果让你看了代码，从中推演出业务，那几乎是不可能的。

事实上，每次了解到一个业务，我都会在脑子中过一下，如果是我做这个业务，我会怎么做。这样一来，我就会先在整体上有一个预判，后面再对应到实际的代码上，就不会那么陌生了。

要了解业务，我一般都会请人给我讲一下，这个业务是做什么的，解决什么样的问题，具体的业务流程是什么样子的，等等。

在初期的了解中，我并不会试图弄懂所有的细节，因为我的目标只是建立起一个基本的框架，有了这个初步的了解，后续再有问题，我就知道该从哪里问起了。

理论上，了解业务是每个程序员都该做的事，但事实上，这也常常是出问题的地方。在请别人给我讲解业务的过程中，我发现，很多人是分不清业务和技术的，经常把二者混在一起讲。如果你跟着他的思路走，很容易就会陷入到对细节的讨论中。

所以，了解业务时，一定要打起精神，告诉自己，这个阶段，我要了解的只是业务，千万别给我讲技术。

技术

了解完业务，就该到技术了。这是程序员最喜欢的话题。但即便是了解技术，也要有个顺序，所以，我们先从宏观内容开始。第一个问题就是这个系统的技术栈：Java、JavaScript 还是.NET，这样，我就可以对用到的工具和框架有个大致的预期。

接下来是系统的业务架构，这个系统包含了哪些模块，与哪些外部系统有交互等等。最好能够有一张或几张图将架构展现出来。现实情况是，不少项目并没有现成的图，那就大家一起讨论，在白板上一起画一张出来，之后再来慢慢整理。

有了一个初步的体系，接下来，就可以稍微深入一些。

我会选择从外向内的顺序了解起。首先是外部，这里的外部包括两个部分：

这个系统对外提供哪些接口，这对应着系统提供的能力；

这个系统需要集成哪些外部系统，对应着它需要哪些支持。

一旦涉及到与外部打交道，就涉及到外部接口是什么样子的，比如，是用 REST 接口还是 RPC（Remote Procedure Call，远程方法调用） 调用，抑或是通过 MQ（Message queue，消息队列）传递消息。

不要简单地认为所有接口都是你熟悉的，总有一些项目会采用不常见的方式，比如，我曾见过有系统用 FTP 做接口的。

所有这些都相当于信息承载方式，再进一步就是了解具体的信息是什么格式，也就是协议。

今天常见的协议是 JSON 格式，或者是基于某个开源项目的二进制编码，比如：Protocol Buffers、Thrift 等等。一些有年头的系统可能会采用那时候流行的协议，比如：XML；有一些系统则采用自己特定领域的协议，比如，通信领域有大量 3GPP 定义的协议。

一般来说，从外部接口这件事就能看出一个项目所处的年代，至少是技术负责人对技术理解的年代。

了解完外部，就该了解内部了。了解内部系统也要从业务入手，对应起来就是，这个系统由哪些模块组成，每个模块承担怎样的职责。如果系统已经是微服务，每个服务就应该是一个独立的模块。

通常这也是一个发现问题的点，很多系统的模块划分常常是职责不清的，因此会产生严重的依赖问题。在前面的内容中，我多次提到限界上下文，用限界上下文的视角衡量这些模块，通常会发现问题，这些问题可以成为后续工作改进的出发点。

业务之后是技术，对应着我需要了解分层。前面说过，分层结构反应着系统的抽象。我希望了解一个模块内部分了多少个层，每个层的职责是什么。了解了这些对系统的设计，也就对系统有了一个整体的认识。

设计之后，就到了动手的环节，但还不到写代码的时候。我会先从构建脚本开始，了解项目的常用命令。我预期从版本控制里得到的是一个可以构建成功的脚本，如果不是这样，我就知道哪里需要改进了。

最后才是代码，比如，代码的目录结构、配置文件的位置、模块在源码上的体现等等，这是程序员最熟悉的东西，我就不多说了。作为初步的接触，了解基本的东西就够了，代码是我们后期会投入大量精力的地方，不用太着急。

团队运作

最后，我们还要了解一下团队运作。同样从外部开始，这个团队有哪些外部接口，比如，需求是从哪来的，产品最终会由谁使用，团队需要向谁汇报。如果有外部客户，日常沟通是怎么安排的。

再来就是内部的活动，一方面是定期的活动，比如，站会、回顾会议、周会，这些不同活动的时间安排是怎样的；另一方面是团队的日常活动，比如，是否有每天的代码评审、是否有内部的分享机制等等。

通过了解这些内容，基本上可以大致判断出一个团队的专业程度，也可以知道自己需要帮助的时候，可以找谁帮忙，为自己更好地融入团队打下基础。

你也许会问，了解这么多东西需要很长时间吧？其实不然，因为只需要从整体上有认知，如果有人很清楚团队现状的话，你可以去请教，也许一天就够了，这也是我往往能够快速上手的原因。接下来，就该卷起袖子干活了！

总结时刻

我给你介绍了怎么把前面学到的知识运用在了解一个项目上，按照业务、技术和团队运作三个方面去了解。

大多数程序员习惯的工作方式，往往是从细节入手，很难建立起一个完整的图景，常常是“只见树木不见森林”，而我的方式则是从大到小、由外而内，将要了解的内容层层分解，有了大图景之后，很容易知道自己做的事情到底在整体上处于什么样的位置。我把上面的内容总结了成一份供你参考。

附赠一点小技巧：使用“行话”。在交流的过程中，学习一点”行话“。这会让人觉得你懂行，让你很快得到信任，尽早融入团队。

如果今天的内容你只能记住一件事，那请记住：了解一个项目，从大图景开始。

最后，我想请你分享一下，你在入职一个新公司遇到过哪些困难呢？欢迎在留言区写下你的想法。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

39 | 面对遗留系统，你应该这样做

在上一讲中，结合着“新入职一家公司”的场景，我给你讲了如何在具体情况下应用我们前面学到的知识。这一讲，我们再来选择一个典型的实际工作场景，将所学综合应用起来。这个场景就是面对遗留系统。

在《34 | 你的代码是怎么变混乱的？》中，我给你讲了代码是会随着时间腐化的，无论是有意，还是无意。即便是最理想的场景，代码设计得很好，维护得也很精心，但随着技术的不断升级进步，系统也需要逐步升级换代。

比如，我们一直认为电信是一个独特的领域，与 IT 技术是完全独立的，学好 CT（Communication Technology，通信技术）就可以高枕无忧了。但随着 IT 技术的不断发展，今天的电信领域也开始打破壁垒，拥抱 IT 技术，提出了 ICT 的概念（Information and Communications Technology，信息通信技术）。

所以，无论怎样，系统不断升级改造是不可避免的事。问题是，你连自己三个月前写的代码都不愿意维护，那当面对庞杂的遗留系统时，你又该何去何从呢？

很多人的第一直觉是，我把系统重写一下就好了。不经思考的重写，就像买彩票一样，运气好才能写好，但大多数人没有这么好运气的，我们不能总指望买彩票中大奖改变生活。那有什么稍微靠谱的一点的路呢？

分清现象与根因

面对庞大的遗留系统，我们可以再次回到思考框架上寻找思路。

Where are we?（我们现在在哪？）

Where are we going?（我们要到哪儿去？）

How can we get there?（我们如何到达那里？）

第一个问题，面对遗留系统，我们的现状是什么呢？

我在这个专栏前面的部分，基本上讨论的都是怎么回答目标和实现路径的问题。而对于“现状”，我们关心的比较少。因为大多数情况下，现状都是很明显的，但这一次不一样。也许你会说，有什么不一样，不就是遗留系统，烂代码，赶紧改吧。但请稍等！

请问，遗留系统和烂代码到底是不是问题呢？其实并不是，它们只是现象，不是根因。

在动手改动之前，我们需要先分析一下，找到问题的根因。比如，实现一个直觉上需要两天的需求，要做两周或更长时间，根因是代码耦合太严重，改动影响的地方太多；再比如，性能优化遇到瓶颈，怎么改延迟都降不下来，根因是架构设计有问题，等等。

所以，最好先让团队坐到一起，让大家一起来回答第一个问题，现状到底是什么样的。还记得我在《25 | 开发中的问题一再出现，应该怎么办？》中提到的复盘吗？这就是一种很好的手段，让团队共同确认现状是什么样子的，找到根因。

为什么一定要先做这个分析，直接重写不就好了？因为如果不进行根因分析，你很难确定问题到底出在哪，更关键的是，你无法判断重写是不是真的能解决问题。

如果是架构问题，你只进行模型的调整是解决不了问题的。同样，如果是模型不清楚，你再优化架构也是浪费时间。所以，我们必须要找到问题的根源，防止自己重新走上老路。

确定方案

假定你和团队分析好了遗留系统存在问题的根因，顺利地回答了第一个问题。接下来，我们来回答第二个问题：目标是什么。对于遗留系统而言，这个问题反而是最好回答的：重写某些代码。

你可能会问，为什么不是重构而是重写呢？以我对大部分企业的了解，如果重构能够解决的问题，他们要么不把它当做问题，要么早就改好了，不会让它成为问题。所以我们的目标大概率而言，就是要重写某些代码。

但是，在继续讨论之前，我强烈建议你，先尝试重构你的代码，尽可能在已有代码上做小步调整，不要走到大规模改造的路上，因为重构的成本是最低的。

我们真正的关注点在于第三个问题：怎么做？我们需要将目标分解一下。

要重写一个模块，这时你需要思考，怎么才能保证我们重写的代码和原来的代码功能上是一致的。对于这个问题，唯一靠谱的答案是测试。对两个系统运行同样的测试，如果返回的结果是一样的，我们就认为它们的功能是一样的。

不管你之前对测试是什么看法，这个时候，你都会无比希望自己已经有了大量的测试。如果没，你最好是先给这个模块补测试。因为只有当你构建起测试防护网了，后续的修改才算是走在坚实的道路上。

说到遗留代码和测试，我推荐一本经典的书：Michael Feathers 的《修改代码的艺术》（Working Effectively with Legacy Code），从它的英文名中，你就不难发现，它就是一本关于遗留代码的书。如果你打算处理遗留代码，也建议你读读这本书。

在 2007 年，我就给这本书写了一篇书评，我将它评价为“这是一本关于如何编写测试的书”，它会教你如何给真实的代码写测试。

这本书对于遗留系统的定义在我脑中留下了深刻印象：遗留代码就是没有测试的代码。这个定义简直就是振聋发聩。按照这个标准，很多团队写出来的就是遗留代码，换言之，自己写代码就是在伤害自己。

有了测试防护网，下一个问题就是怎么去替换遗留系统，答案是分成小块，逐步替换。你看到了，这又是任务分解思想在发挥作用。

我在《36 | 为什么总有人觉得 5 万块钱可以做一个淘宝？》中提到，淘宝将系统改造成 Java 系统的升级过程，就是将业务分成若干的小模块，每次只升级一个模块，老模块只维护，不增加新功能，新功能只在新模块开发，新老模块共用数据库。新功能上线，则关闭老模块对应功能，所有功能替换完毕，则老模块下线。

这个道理是普遍适用的，差别只是体现在模块的大小上。如果你的“小模块”是一个系统，那就部署新老两套系统，在前面的流量入口做控制，逐步把流量从老系统转到新系统上去；如果“小模块”只在代码层面，那就要有一段分发的代码，根据参数将流程转到不同的代码上去，然后，根据开发的进展，逐步减少对老代码的调用，一直到完全不依赖于老代码。

这里还有一个小的建议，按照分模块的做法，将新代码放到新模块里，按照新的标准去写新的代码，比如，测试覆盖率要达到 100%，然后，让调用入口的地方依赖于这个新的模块。

最后，有了测试，有了替换方案，但还有一个关键问题，新代码要怎么写？

要回答这个问题，我们必须回到一开始的地方，我们为什么要做这次调整。因为这个系统已经不堪重负了，那我们新做的修改是不是一定能解决这个问题呢？答案是不好说。

很多程序员都会认为别人给留下的代码是烂摊子，但真有一个机会让你重写代码，你怎么保证不把摊子弄烂？这是很多人没有仔细思考过的问题。

如果你不去想这个问题，即便今天你重写了这段代码，明天你又会怨恨写这段代码的人没把这段代码写好，只不过，这个被抱怨的人是你自己而已。

要想代码腐化的速度不那么快，一定要在软件设计上多下功夫。一方面，建立好领域模型，另一方面，寻找行业对于系统构建的最新理解。

关于领域模型的价值，我在专栏前面已经提到过不少次了。有不少行业已经形成了自己在领域模型上的最佳实践，比如，电商领域，你可以作为参考，这样可以节省很多探索的成本。

我们稍微展开说说后面一点，“寻找行业中的最新理解”。简言之，我们需要知道现在行业已经发展到什么水平了。

比如说，今天做一个大访问量的系统，我们要用缓存系统，要用 CDN，而不是把所有流量都直接转给数据库。而这么做的前提是，内存成本已经大幅度降低，缓存系统才成为了标准配置。拜 REST 所赐，行业对于 HTTP 的理解已经大踏步地向前迈进，CDN 才有了巨大的进步空间。

而今天的缓存系统已经不再是简单的大 Map，有一些实现得比较好的缓存系统可以支持很多不同的数据结构，甚至支持复杂的查询。从某种程度上讲，它们已经变成了一个性能更好的“数据库”。

有了这些理解，做技术选型时，你就可以根据自己系统的特点，选择适合的技术，而不是以昨天的技术解决今天的问题，造成的结果就是，代码写出来就是过时的。

前面这个例子用到的是技术选型，关于“最新理解”还有一个角度是，行业对于最佳实践的理解。

其实在这个专栏里，我讲的内容很多都是各种“最佳实践”，比如，要写测试，要有持续集成，要有自动化等等，这些内容看似很简单，但如果你不做，结果就是团队很容易重新陷入泥潭，继续苦苦挣扎。

既然选择重写代码，至少新的代码应该按照“最佳实践”来做，才能够尽可能减缓代码腐化的速度。

总之，改造遗留系统，一个关键点就是，不要回到老路上。

总结时刻

我们把前面学到的各种知识运用到了“改造遗留系统”上。只要产品还在发展，系统改造就是不可避免的。改造遗留系统，前提条件是要弄清楚现状，知道系统为什么要改造，是架构有问题，还是领域模型混乱，只有知道根因，才可能有的放矢地进行改造。

改造遗留系统，我给你几个建议：

构建测试防护网，保证新老模块功能一致；

分成小块，逐步替换；

构建好领域模型；

寻找行业中关于系统构建的最新理解。

如果今天的内容你只能记住一件事，那请记住：小步改造遗留系统，不要回到老路上。

最后，我想请你分享一下，你有哪些改造遗留系统的经验呢？欢迎在留言区分享你的做法。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

40 | 我们应该如何保持竞争力？

在前面两讲，我结合着两个程序员要直接面对的场景，讨论了如何综合运用前面学习到的知识，这一讲的内容可能不涉及到实际的应用场景，但与每个人的发展息息相关。我想谈谈如何走好程序员这条路。

焦虑的程序员

让我们再次用思考框架分析一下问题。首先，现状是什么？关于这个问题，我并不打算讨论个体，因为每个人的情况千差万别，我准备从整体入手。

IT 行业是一个快速发展变化的行业，一方面，我们不断地看到有人快速取得成功，另一方面，我们也听到了许多充满焦虑的声音。获得大的成功总是一个小概率事件，大多数人面对的还是日常的柴米油盐。

我们的焦虑来自于对未来的不确定性，而这种不确定性是一个特定时代加上特定行业的产物。

如果把时间倒回到上个世纪 80 年代之前，虽然当时的生活条件一般，但很少有人会为未来的发展焦虑，因为那时候，人们可以清晰地看到自己未来的人生，尽管那种人生可能是平淡的。

但今天的我们处在一个人类历史上少有的快速发展时代，我们看不清以后的人生，大脑却还停留在上一代人的思维习惯上。

IT 行业在国内的大发展也就最近 20 多年的事，行业里很少有走过完整职业生涯的程序员。也正是因为如此，我们经常会产生了各种焦虑：

我刚刚入行时，有人问，程序员能做到 30 岁吗？

我快 30 岁时，有人问，35 岁还能做程序员吗？

我 35 岁时，讨论变成了 40 岁的程序员该怎么办。

估计等国内有越来越多的程序员走完了整个职业生涯，就会有人关心，程序员退休之后的生活应该是什么样子了。

从长期来看，只要生活中还有需要用自动化解决的问题，程序员这个群体还是很有前景的。但随着时间的推移，程序员这个职业的溢价也会越来越低，单纯凭借身处这个行业就获得好发展的可能性也越来越低，想让自己的职业生涯走得更顺畅，还需要找到更好的目标，不断努力。

成为 T 型人

我们再来回答下一个问题：目标是什么。也许这时候，每个人脑子里想到的职业发展路线都不一样，但我准备用一个统一的目标回答你：成为 T 型人。

什么叫 T 型人？简言之，一专多能。

有了“一专”，“多能”才是有意义的，否则，就是低水平重复，而这正是很多人职业生涯不见起色的真正原因。

这里的“专”不是熟练，而是深入。你可能是个有着 10 年丰富经验的程序员，但实际上只不过是重复了 10 年解决同样难度的问题而已，这根本就不算深入，也就没有做到真正意义上的“一专”。

你会发现很多优秀的人，在很多方面都会很优秀，这是“一专”带来的触类旁通。

当你有了“一专”，拓展“多能”，就会拥有更宽广的职业道路。比如，我拥有了深厚的技术功底，通晓怎么做软件：

如果还能够带着其他人一起做好，就成了技术领导者。

如果能够分享技术的理解，就有机会成为培训师。

如果能够在实战中帮助别人解决问题，就可以成为咨询师。

反过来，当你有了“多能”，也可以拓宽你的视野，帮你认清自己的“一专”怎样更好地发挥价值，而不是狭隘地认为自己有了技术，就已经天下尽在掌握了。视野窄，缺乏大局观，也成为了许多程序员再进一步的阻碍。事实上，这个专栏里的很多内容都是帮你打开“多能”的视角。

也许你会说，我在公司已经独挡一面了，应该算有“一专”了吧？但我想说的是，可能还不够。只做一个公司的专家，受一个公司的波动影响太大，而成为行业的专家，才会降低自己职业生涯的风险。

有时，我在面试时会问候选人这样一个问题：“如果让你在一次技术大会上做分享，你会讲什么呢？”我真正的问题是，以行业标准衡量，你觉得你在哪个方面是专家呢？

大多数人从来没有思考过这个问题，他们只是日常在完成自己的工作，即便在某一方面已经做得很不错了，但依然算不上专家，因为他们缺乏深度思考。

比如，你非常熟悉 Kafka，知道它的各种参数，也读过它的实现原理。但如果我问你，Kafka 为什么要把自己定位成一个分布式流平台，它要想成为一个流平台，还要在哪方面做得更好？你的答案是什么呢？

这其中的差别就是，前面所谓的熟悉，只是熟悉别人的思考结果，而后面则是一个没有现成答案的东西。学习微积分是有难度，但同发明微积分相比，难度根本不在一个层次上。当然，我不是说你要熟悉所有工具的发展过程，而是自己要在一个特定的方面拥有深度的思考。

也许你会说，这个要求实在是太高了吧！没错，这确实是一个很高的要求。但“取法于上，仅得为中；取法于中，故为其下。”

其实，很多人的焦虑就源自目标太低，找不到前进的动力。给自己定下一个可以长期努力的目标，走在职业的道路上才不致于很快丧失动力。

在学习区成长

现在我们来回答第三个问题，怎么达到目标。既然要朝着行业中的专家方向努力，那你就得知道行业中的专家是什么样。我的一个建议是，向行业中的大师学习。

你或许会说，我倒是想向大师学习，但哪有机会啊！好在 IT 行业中的许多人都是愿意分享的，我们可以读到很多大师级程序员分享的内容。

我在入行的时候，有幸读了很多经典之作，比如，出身贝尔实验室的很多大师级程序员的作品，诸如《C 程序设计语言》《程序设计实践》、《Unix 编程环境》等，还有一些像 Eric Raymond 这样沉浸编程几十年的人写出的作品，诸如《Unix 编程艺术》，以及前面提及的 Kent Beck、Martin Fowler 和 Robert Martin 等这些人的作品。

读这些书的一个好处在于，你的视野会打开，不会把目标放在“用别人已经打造好的工具做一个特定的需求”，虽然这可能是你的必经之路，但那只是沿途的风景，而不是目标。

接下来，我们要踏上征程，怎么才能让自己的水平不断提高呢？我的答案是，找一个好问题去解决，解决了一个好的问题能够让你的水平快速得到提升。什么是好问题？就是比你当前能力略高一点的问题，比如：

如果你还什么都不会，那有一份编程的工作就好。

如果你已经能够写好普通的代码，就应该尝试去编写程序库。

如果实现一个具体功能都没问题了，那就去做设计，让程序有更好的组织。

如果你已经能完成一个普通的系统设计，那就应该去设计业务量更大的系统。

为什么要选择比自己水平高一点的问题？这与我们学习成长的方式有关。Noel Tichy 提出了一个“学习区”模型，如下图所示：

最内层是舒适区（Comfort Zone），置身其中会让人感觉良好，但也会因为没有挑战，成长甚微，你可以把它理解成做你最熟悉的事情。

最外层是恐慌区（Panic Zone），这是压力极大的地方，完全超出了你的能力范围，你在其中只会感到无比的焦虑。

中间的是学习区（Learning Zone），事情有难度，又刚好是你努力一下可以完成的，这才是成长最快的区域。

根据这个模型，只有一直身处学习区才能让人得到足够的成长，所以，我们应该既选择比自己能力高一点的问题去解决，不要总做自己习惯的事，没有挑战，也不要好大喜功，一下子把自己的热情全部打散。

在学习区成长，就不要满足于当前已经取得的成绩，那已经成为你的舒适区。因为我们有远大的目标在前面指引，完成日常的工作只不过是个人成长路上的台阶。

也许你会说，我的工作不能给我个人成长所需的机会，怎么办呢？实际上，别人只会关心你是否完成工作，成长是自己的事情，很多机会都要靠自己争取，前面提到的那些具体做法完全是你可以在工作范围内，自己努力的事情。

如果你当前的工作已经不能给你提供足够好的问题，那就去寻找一份更有挑战性的工作。在 IT 行业，跳槽似乎是一件很常见的事，但很多人跳槽的时候，并不是以提升自己为目标的。造成的结果是，不断地做同一个层面的工作，自然也就很难提升自己的水平。

为什么程序员都愿意到大厂工作？因为那里有高水平的人和好的问题。但如果只是到大厂去做低水平的事，那就是浪费时间了。所以，即便你真的想到大厂工作，与谁一起工作，做什么事，远比进入大厂本身要重要得多。

如果你真的能够不断向前进步，迟早会遇到前面已经没有铺就好的道路，这时候，就轮到你创造一个工具给别人去使用了。比如，2012 年，我在项目中受困于集成问题，却找不到一个我想要的、能在单元测试框架里用的模拟服务器，于是，我写了 Moco。

最后，我还想鼓励你分享所得。我在《28 | 结构化：写文档也是一种学习方式》中和你说过，输出是一种将知识连接起来的方式，它会让人摆脱固步自封，也会帮你去创造自己的行业影响力，机会会随着你在行业中的影响力逐渐增多，有了行业影响力，你才有资格成为行业专家。

当你成为了一个行业级别的专家，就可以在这条路上一直走下去，而不必担心自己是不是拼得过年轻人了，因为你也在一直前进！

总结时刻

程序员是一个充满焦虑的群体，焦虑的本质是对未来的不确定。工作在这个时代的程序员是一个特殊的群体，一方面，这个大时代为我们创造了无数的机会，另一方面，因为程序员是一个新的行业，所以，很多人不知道未来是什么样子的，焦虑颇深。

从目前的发展来看，IT 行业依然是一个非常有前景的行业，但想在这条路上走好，需要我们成为 “T ”型人才，也就是“一专多能”。一专多能的前提是“一专”，让自己成为某个方面的专家。这个专家要放在行业的标准去看，这才能降低因为一个公司的波动而造成的影响。

成为行业专家，要向行业的大师学习，给自己定下一个高的目标，然后是脚踏实地，找适合自己的问题去解决，让自己一直在学习区成长。

如果今天的内容你只能记住一件事，那请记住：在学习区工作和成长。

最后，我想请你分享一下，你有哪些保持自己竞争力的心得呢？欢迎在留言区写下你的想法。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

划重点 | “综合运用”主题内容的全盘回顾

又到了我们划重点的时间了，因为篇幅关系，“综合运用”这个模块最为短小精悍。

在这个模块中，我们把前面学到的各种知识综合起来，运用在实际的工作场景中，让你知道这些内容并不是一个个孤立的实践，在实际工作中，唯有将它们结合起来，才能发挥最大功效。

重点复习

在这个模块中，我们学习到了一些新知识。

“学习区”学习模型

舒适区，舒适而缺乏成长。

恐慌区，超出能力范围。

学习区，有难度而可以达成。

在学习区练习才能得到足够的成长。

T 型人才，一专多能

知识的广度。

专业技能的深度。

有“一专”，“多能”才是有意义的。

在这个模块中，我们还了解了一些重要的思路，让我们把工作做得更好。

进入新工作，从全面了解了解开始

业务：做什么。

技术：怎么做。

团队运作：怎么与人协作。

从大到小，由外及内地了解工作。

面对遗留系统，稳扎稳打，小步前行

基础理念

烂代码只是现象，要了解根因。

能重构，先重构，大规模改造是迫不得已的选择。

小步前行。

实际操作

构建测试防护网。

将大系统分解成小模块，逐步替换。

新旧模块并存，由分发模块调度。

建立好领域模型。

寻找行业对于系统构建的最新理解。

程序员的职业发展

程序员的焦虑来自于对未来的不确定性，这种不确定性是一个特定时代加上特定行业的产物。

快速发展的中国经济。

程序员在中国是一个新兴职业。

成为行业专家，制定高目标。

向大师学习，开拓视野。

找到好的问题，和高水平的人一起工作。

实战指南

了解一个项目，从大图景开始。
——《38 | 新入职一家公司，怎么快速进入工作状态？》

小步改造遗留系统，不要回到老路上。
——《39 | 面对遗留系统，你应该这样做》

在学习区工作和成长。
——《40 | 我们应该如何保持竞争力？》

额外收获

在这个模块的最后，针对大家在学习过程中的一些问题，我也进行了回答，帮你梳理出一个思路，更好地理解学到的内容：

推行新观念，找愿意改变的人，做具体的事。

Lead by Example.

外部系统应该用接口隔离，这种做法体现了接口隔离原则（ISP），也是防腐层概念的体现。

外部系统的测试，能模拟的就模拟，能本地的就本地。

留言精选

关于入职一家新公司，怎么快速进入工作状态这个问题，西西弗与卡夫卡 同学分享了他的方法：

有朋友正在转型，从乙方商业化产品的交付经理转向新公司的产品经理。原本得心应手的思维方式和工作习惯，遇到了巨大挑战。以前只需依据已有产品的功能出解决方案，能做就能做，不能实现就是不能实现，到某个时间交付什么功能很明确，考核是以交付签字为准。现在需要面对各方需求，自己想明白用户真正的问题是什么，最终要交付的价值是什么，没有一个实体的谁人来签字，只有不断地迭代。

借鉴领域驱动设计，可以采用以下方法。简单描述的话，是一个点、一个圈再加一个箭头线，是不是有点像丘比特？

一个“点”，指的是用户核心价值。这是最关键的一条，基本上只能靠自己想明白。想，不是闭门造车式的苦思冥想，可以是已有的领域经验，可以从书本中学习，可以是大家的各种吐槽，可以是自己从旁边观察用户的实践，还可以是自己变身为用户的实践。

有些人会纠结“点”想的对不对，迟迟不敢动手。其实一开始想得对不对不是那么重要，关键是要有这“点”，然后快速到市场上验证，根据反馈再调整。

一个“圈”，指的是围绕核心价值划出的范围，即领域驱动设计中的限界上下文。产品经理面临的一个现实是，各种人都会给你提需求，只要他们觉得和你有关，还时不时来问什么时候可以实现。

需求轰炸之下很容易焦虑，不光自己焦虑，所有的利益相关者都会焦虑。依据核心价值，框出需求范围，在和各方交流过程中可以有一种确定性，减少焦虑，利于行动。

大家（不光是研发团队，也包括其他需求方）就能明白，哪些和当前核心价值密切相关，我们优先考虑；哪些与核心价值有关但它不在我们的范围内，属于其他团队，需要他们协助；哪些有关系，但目前没想清楚价值大不大，并且代价可能很高建议先搁置。范围不是一成不变，它随着时间会发生变动，所以我们不要追求固定，只要保证在某个时间段内，大家一致认同即可。

一个“箭头”，指的是实现路径，箭头指向核心目标（核心价值）。目标（核心价值）和范围描绘的是终极，而从现实到终极还有很多路要走，可能的路径还有很多条。我们需要琢磨怎么走更稳当，怎么走代价比较低，路上关键的里程碑是什么。路径对不对是其次，重要的是思考过程，可以把关键点需要交付的价值、需要支持的资源等等梳理清楚。

另外，西西弗与卡夫卡 同学还对于程序员如何保持竞争力的问题给出了非常不错的建议。

补充我的一些做法。工作中不要满足当前需求，要经常从自己上级主管甚至老板角度来审视自己的工作，思考业务的终极目标，持续琢磨扩展边界，挑战工作难度。

平时多看书多思考，除了钻研某个领域，还要多有涉猎，拓展领域，成为终身学习者。

适当运动维持健康，你有更多体力和更强抗压能力的时候，就可以超过不少人。

保持竞争力除了上述之外，要保持乐观，相信大多数事都有解决方法，在多数人都容易放弃的时候，你的坚持，就是竞争力。

对于新入职一家公司的场景，Y024 同学分享了他快速进入工作状态的方法：

1. 我会在权限允许的范围内，时不时的到处翻翻 ftp、内部 wiki 等资源，星星点点构建全貌（业务、技术、团队）。

2. 梳理系统数据流。去年很火的电视剧「大江大河」里，宋运辉初入职场的方式就很值得借鉴：先走通全部流程，有个全貌，利用图书馆、师傅等资源再自己动手各个击破并绘制流程图，最终实践检验认知，以技术说话融入团队。

（他就每天只要天气晴朗，绕着设备上上下下、里里外外地跑。一个星期下来，全部流程走通；两个星期不到，原理搞通，仪表能读，普通故障能应付；第三星期开始，他可以开出维修单，但得给师父过目；第四星期起，谁有事请假他可以顶上，坐到仪表盘前抄表看动态做操作。师父说他学得很快。

第四星期起，没人可以让他顶替时候，他在仪表室后面支起绘图板。先画出工艺流程图，经现场核对无误，又让师父审核后，开始按部就班地根据液体走向，测绘所有设备的零件图、装配图、管段图等。

这工作最先做的时候异常艰难，首先是绘图不熟练，很多小毛病，尤其是遇到非标零件，还得到机修工段测绘，有时一天都绘不成一个小小非标件。如果车间技术档案室有图纸还好，可以对照着翻画，可档案室里的图纸残缺不全，前后混乱，想找资料，先得整理资料。

资料室中年女管理员乐得有个懂事的孩子来帮她整理，索性暗暗配把钥匙给宋运辉，要是她下班不在的时候，让宋运辉自己偷偷进来关上门寻找资料。

机修工段的人本来挺烦这个宋运辉，说他一来维修单子多得像雪片，支得他们团团转，有人还趁宋运辉上班时候冲进控制室指桑骂槐，被寻建祥骂了回去，差点还打起来。但后来集中一段维修高峰后，维修单子又少了下去，上面还表扬跑冒滴漏少很多，一工段和机修工段各加一次月奖，可见设备性能好转。

再以后遇到维修，他们不能确定要用什么零件，打个内线电话给控制室问宋运辉，一问就清楚。双方关系渐渐变得铁起来。基层有时候很简单，只要拿得出技术，别人就服。 ）

另外，Y024 同学还很认真地整理了专栏提到的部分图书：

郑老师拍案惊奇书单及简评，最近各大书店有活动，可以借机囤起来了。

1. 重构
作者: Martin Fowler
https://book.douban.com/subject/1229923/
严格说来，我并没有完整的读完这本书，不过，正如作者自己所说，这样的书原本就不指望能够读完，因为有一大部分其实是参考手册。正是我读过的部分让我知道了重构，让我知道这么做可以把代码写得更好。

2. 敏捷软件开发
作者: Robert C·Martin
https://book.douban.com/subject/1140457/
这是一本名字赶潮流，内容很丰富的书，这本书让我开始理解软件设计，从此不再刻意追求设计模式。

3. 测试驱动开发
作者: Kent Beck
https://book.douban.com/subject/1230036/
读的是英文版，因为当时中文版还没有出版，所以，我不敢说，我通过这本书很好的理解了测试驱动开发，但它却为我打开了一扇门，让我知道了一种更好的工作方式。

4. 修改代码的艺术
作者: Michael Feathers
https://book.douban.com/subject/2248759/
这是一本讲解如何编写测试的书。至于这本书的具体内容，我的评价是实用。如果说不足，那么，这本书缺乏一个列表，就像 Martin Fowler 为《重构》所做的那样，出什么样的问题，应该采用怎样的手法进行处理。

对于如何面对遗留系统， 毅 同学提到：

1. 了解原系统已实现的功能，没有文档就在心中划分好内部功能模块；
2. 各模块的边界及关联，对于业务交叉点先思考通信机制；
3. 看代码，通常是瓶颈优先，业务上是先复杂后简单；
4. 选定切入点；
5. 正式改造时先把原有功能抽象出来使用现有实现，改造的过程完成前不会受影响；
6. 改造完成后切换但新实现进行测试；
7. 稳定后替换旧实现；
8. 重复 4-7。

Wei 同学对于“T 型人”的说法感触很深：

“T 型人”这个太说到点了，到底是做“专”还是做“广”，哪条路线一直是我思考的方向；工作上跟大牛工作过，给我感觉几乎是全能的，我一直都想像他们那样，做一个多面手，但是如何做广，这一直是困扰我的一个问题。

我是 dev 出身，但是现实遇到的问题往往跟数据库，发布的平台相关；这样说下来，各种相关领域，数据库、k8s
、网络协议、DNS ，都需要大量时间去积累；有时候什么都懂一点，反而让自己应该定位什么角色感到迷茫了，掌握的水平不足以让自己去应聘 DBA、Ops，但是只是应聘 dev 似乎又有点“浪费”，跟那些熟悉最新语言 / 框架的对比起来没特出竞争力。

今天学习“T 型人”这个概念，让我好好思考了自己到底应该怎么定位。我首先是一个 developer，这个是根；对语言特性的熟练掌握，各种 best practices，例如课程中提到的 TDD 等应该熟练应用起来；然后在这上面拓展，学习架构知识，多思考对不同系统应该怎么设计，老师提到的 DDD 会认真学习应用；再有软件最终还是给用户使用，而不是单单提交代码。相关的数据库、k8s、监控运用根据实际遇到的问题再学习解决。

最重要的是，在学习区终身学习和工作！

对于如何持续保持竞争力的问题，enjoylearning 同学提到：

程序员如何保持竞争力很重要，在这个年轻人学习能力不断提升的 IT 行业，作为老程序员经验阅历眼光以及技术前沿判断力就显得越来越重要。

说起来这个职业是一个需要终身学习的职业，年龄不重要，能力才重要，是不是让自己永远呆在学习区更重要。

对于技术推广，desmond 同学的理解也很棒：

技术推广，不要先推广最难的部分，先推广能让对方感到最明显好处的部分。取得对方的信任，是友好沟通的基础。

感谢同学们的精彩留言。我们的专栏更新已经进入尾声阶段，后续我会为大家做一些对整个专栏进行全盘复习的内容，敬请期待。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

总复习 | 重新审视“最佳实践”

我承诺的正文内容已经全部交付给你，恭喜你完成了整个专栏的学习！希望通过对这些内容的学习，你已经对“如何做好软件”有了一个全新的认识。

在这个专栏中，我给你讲了很多行业中的最佳实践，比如：测试、持续集成等等，但因为这个专栏叙述方式的关系，一些有关联的实践被放到了不同的模块下讲解。

所以在这一讲中，我们将按照最佳实践的维度重新审视这些内容。我会将这些知识重新串联起来，帮你做一个对专栏的整体复习。

产品

做产品，很多时候是面向不确定性解决问题。目前这方面最好的实践是“精益创业”。对于精益创业的最简单的理解就是“试”。试也有试的方法，精益创业提出了一个“开发（build）- 测量（measure）- 认知（learning）”这样的反馈循环，通过这个循环得到经过验证的认知（Validated Learning）。

既然是对不确定产品特性的尝试，最好的办法就是低成本地试。在精益创业中，最小可行产品（MVP）就是低成本的试法。最小可行产品，就是“刚刚好”满足用户需求的产品。理解这个说法的关键在于用最小的代价，尝试可行的路径。

在产品的打磨过程中，可以采用用户测试的方式，直接观察用户对产品的使用。作为程序员，我们要尽可能吃自家的狗粮，即便你做的产品不是给自己使用的产品，也可以努力走近用户。

精益创业
相关阅读：《06 | 精益创业：产品经理不靠谱，你该怎么办？》

最小可行产品（MVP）
相关阅读：《19 | 如何用最小的代价做产品？》

用户测试、验证产品特性、吃自家狗粮
相关阅读：《26 | 作为程序员，你也应该聆听用户声音 》

需求

当我们确定做一个产品功能时，怎么描述需求也是很重要的。产品列表式的需求描述方式最容易出现问题的地方在于，看不清需求的全貌。

用户故事是一个好的需求描述方式：作为一个什么角色，要做什么样的事，以便达成一种怎样的效果。

在用户故事中，验收标准是非常重要的一环。即便不是采用用户故事描述需求，也依然建议先将验收标准定义清楚。

开发团队对需求的理解普遍偏大，基本上都是一个主题。在开发之前，先将需求拆分成小粒度的。衡量一个用户故事拆分是否恰当，一个标准是 INVEST 原则。有了拆分出来的用户故事，就可以进行估算了，估算的过程也是对需求加深理解的过程，过大的用户故事应该再次拆分。

当我们有了拆分之后的需求，就可以对需求进行进行优先级讨论了。先做重要性高的事，而不是一股脑地去做所有的需求。只有分清了需求的优先级，才能方便地对需求进行管理。

用户故事
相关阅读：《04 | 接到需求任务，你要先做哪件事？ 》

需求的分解与估算
相关阅读：《17 | 程序员也可以“砍”需求吗？》

需求管理、优先级
相关阅读：《18 | 需求管理：太多人给你安排任务，怎么办？》

持续集成

在开发中，写出代码并不是终点，我们要把代码集成起来。集成要经常做，改动量越小，集成就可以做得越频繁，频繁到每次提交都去集成，这就是持续集成。

持续集成发展到今天已经是一套完整的开发实践。想要做好持续集成，你需要记住持续集成的关键是“快速反馈”。

怎样快速得到反馈。

怎样反馈是有效的。

持续集成，可以继续延展，将生产部署也纳入其中，这就是持续交付。如果持续交付，再向前一步，就可以同产品验证结合起来。

持续交付的关键点，是在不同的环境验证发布包和自动化部署。不同的环境组成了持续交付的构建流水线，而自动化部署主要是 DevOps 发挥能力的地方。持续交付的发展，让交付物从一个简单的发布包变成了一个拥有完整环境的 Docker 镜像。

持续集成和持续交付可以将诸多的实践贯穿起来：单元测试、软件设计、任务分解、主分支开发、DevOps 等等。所以，如果一个公司希望做过程改进，持续集成是一个好的出发点。

持续集成发展史
相关阅读：《05 | 持续集成：集成本身就应该是写代码的一个环节》

快速反馈
相关阅读：《24 | 快速反馈：为什么你们公司总是做不好持续集成？》

持续集成，贯穿诸多实践
相关阅读：《答疑解惑 | 持续集成，一条贯穿诸多实践的主线 》

持续交付
相关阅读：《32 | 持续交付：有持续集成就够了吗？》

与产品结合：持续验证
相关阅读：《答疑解惑 | 持续集成、持续交付，然后呢？ 》

测试

测试是一个典型的程序员误区，很多程序员误以为测试只是测试人员的事。理解了软件变更成本，知道了内建质量之后，我们就应该清楚，测试应该体现在全部的开发环节中。这一思想在开发中的体现就是自动化测试。

想要写好自动化测试，需要先理解测试金字塔，不同的测试运行成本不同。为了让软件拥有更多的、覆盖面更广的测试，需要多写单元测试。

编写测试的方式有很多，一种实践是测试驱动开发（TDD）。先写测试，然后写代码，最后重构，这就是 TDD 的节奏：红——绿——重构。测试驱动开发的本质是测试驱动设计，所以，编写可测试的代码是前提。

要想做好 TDD，一个重要的前提是任务分解，分解到非常小的微操作。学会任务分解，是成为优秀程序员的前提条件。

想写好测试，需要懂得好测试是什么样子的，避免测试的坏味道。好测试有一个衡量标准：A-TRIP。

我们不只要写好单元测试，还要站在应用的角度写测试，这就是验收测试。验收测试现在比较成体系的做法是行为驱动开发（BDD），它让你可以用业务的语言描述测试。

单元测试、自动化测试、蛋卷和冰淇淋模型
相关阅读：《12 | 测试也是程序员的事吗？》

测试驱动开发
相关阅读：《13 | 先写测试，就是测试驱动开发吗？》
相关阅读：《14 | 大师级程序员的工作秘笈 》

测试练习
相关阅读：《15 | 一起练习：手把手带你拆任务 》

简单的测试、测试的坏味道、A-TRIP
相关阅读：《16 | 为什么你的测试不够好？ 》

验收测试、写好验收测试用例
相关阅读：《32 | 持续交付：有持续集成就够了吗？》

外部系统测试，用接口隔离
相关阅读：《答疑解惑 | 如何在实际工作中推行新观念？ 》

编码与设计

编码和设计，是软件开发中最重要的一环。在我看来，编码和设计是一体，想清楚才能写出好代码。很多程序员追求写好代码，却没有一个很好的标准去衡量代码的好坏。结合着软件设计的一些理念，我给你一个编写好代码的进步阶梯，希望你能达到用业务语言编写代码的程度。

用业务语言编写代码，需要对软件设计有着良好的理解。提到设计，人们的初步印象是“高内聚低耦合”，但这是一个太过高度抽象的描述。SOLID 原则是一个更具实践性的指导原则，有了原则做指导，就可以更好地理解设计模式了。

有了基础原则，我们会知道将不同的代码划分开，这样就产生了分层。好的分层可以构建出抽象，而其他人就可以在这个抽象上继续发展。对于程序员来说，构建自己的核心抽象是最关键的一步。

目前构建核心抽象最好的方式是领域驱动设计（DDD），它将我们思考的起点拉到了业务层面，通过战略设计将系统按照不同的上下文划分开来，再通过战术设计，指导我们有效地设计一个个的领域模型。

但无论怎样做设计，前提是使用适当的技术解决适当的问题，不要把技术用复杂，把团队带入泥潭。

业务语言写代码
相关阅读：《21 | 你的代码为谁而写？》

架构设计
相关阅读：《34 | 你的代码是怎么变混乱的？ 》

分层、抽象
相关阅读：《35 | 总是在说 MVC 分层架构，但你真的理解分层吗？》

业务与技术
相关阅读：《36 | 为什么总有人觉得 5 万块钱可以做一个淘宝？ 》

微服务
相关阅读：《37 | 先做好 DDD 再谈微服务吧，那只是一种部署形式 》

项目准备

从头开始一个项目时，一个好的实践就是把一切都准备好。迭代 0 就是这样一个把迭代准备好的实践，从需求到技术，做好充分的准备工作再开启项目，你会显得从容不迫。在技术方面，迭代 0 最重要的准备工作就是构建脚本，它是后续很多工作的基础，比如，持续集成。

迭代 0，做基础的准备
相关阅读：《10 | 迭代 0: 启动开发之前，你应该准备什么？》

构建脚本，让项目一开始就自动化
相关阅读：《30 | 一个好的项目自动化应该是什么样子的？ 》

其余的最佳实践

除了几个花大篇幅介绍的最佳实践，我们还提到了很多不同的最佳实践。

DoD

完成的定义（DoD），是一个确保合作各方理解一致的实践。它是一个清单，由一个个检查项组成，每个检查项都是实际可检查的。有了 DoD，做事就只有两种状态：完成和未完成。

完成的定义，DOD
相关阅读：《03 | DoD 价值：你完成了工作，为什么他们还不满意？》

站会

站会，一种轻量级的会议形式，用来同步每天发生的事情。一般来说，只说三件事：昨天做了什么，今天打算做什么，遇到了什么问题。

站会
相关阅读：《22 | 轻量级沟通：你总是在开会吗？ 》

看板

看板，一种项目管理工具， 将正在进行的工作可视化。通过看板，可以发现团队正在进行工作的很多问题。看板有实体和电子之分，可以根据自己的项目特点进行选择。

看板
相关阅读：《23 | 可视化：一种更为直观的沟通方式 》

回顾会议

回顾会议，是一种复盘实践，让团队成员对一个周期内发生的事情进行回顾。回顾会议一般分为讲事实、找重点和制定行动项三个部分。但在开始回顾之前，会先进行安全检查，确保每个人都能放心大胆地说真话。

回顾会议
相关阅读：《25 | 开发中的问题一再出现，应该怎么办？ 》

回顾会议中的安全检查
相关阅读：《答疑解惑 | 持续集成，一条贯穿诸多实践的主线 》

重构

重构，是程序员的基本功，把调整代码的动作分解成若干可以单独进行的“重构”小动作，一步步完成。重构的前提是识别代码的坏味道。保证代码行为不变，需要有测试配合，而重构的方向是，重构成模式（Refactoring to Patterns）。重构的过程和编写代码的过程最好结伴而行，最佳实践就是测试驱动开发。

重构
相关阅读：《加餐 | 你真的了解重构吗？》

在测试驱动开发中重构
相关阅读：《13 | 先写测试，就是测试驱动开发吗？》

分支开发

分支开发模型，是每个团队都要面临的问题。行业中有两种常见的分支模型，一种是基于主干的开发模型，一种是分支开发模型。分支开发符合直觉，却不是最佳实践。主分支开发模型是与其他实践配合最好的模式，但也需要开发者有着良好的开发习惯。如果并行开发多个功能，可以考虑 Feature Toggle 和 Branch by Abstraction。

分支开发
相关阅读：《14 | 大师级程序员的工作秘笈 》

Feature Toggle 和 Branch by Abstraction
相关阅读：《答疑解惑 | 如何分解一个你不了解的技术任务？ 》

Fail Fast

Fail Fast 是一个重要的编程原则：遇到问题，尽早报错。不要以构建健壮系统为由，兼容很多奇怪的问题，使得 Bug 得以藏身。

Fail Fast
相关阅读：《27 | 尽早暴露问题： 为什么被指责的总是你？ 》

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

总复习 | 重新来“看书”

我们继续复习，在上一讲中，我从最佳实践的角度带领大家回顾了专栏里的一些内容。这一讲，我们换个复习的角度。在专栏进行的过程中，有一些同学注意到我引用了大量的书籍，提出让我把这些书做一个整理。

Wei 同学提到：

有一个小建议： 在每一个主题模块的小结中，把文章中提到的书籍做一个书单方便读者。

刘晓林 同学提到：

郑老师在专栏中推荐了很多非常好的书籍作为参考，可否考虑在某一期中，将这些参考书籍整理成一个书单，按照专栏的主题做个小分类，然后每本书简单点评两句作为领读内容。希望在专栏的结束语之前可以看到这个书单。

Y024 同学甚至在留言中帮我总结了一个小清单，而有人也在豆瓣上做出了一个豆列，罗列了专栏中提到的一些书。

在今天这一讲中，我就站在“看书”的视角，带着你进行一次复习。这些书大多是在我个人成长过程中，给我留下深刻印象的。

我希望你在结束这个专栏学习之后，开启的是另外一段学习历程，用这些书提升自己的水平，夯实自己的基础知识。学习了这个专栏之后，你拥有了一个新的知识结构，再来看这些书就会有一种全新的体验。

此外，在这次的内容中，我会提到几本专栏中没有提到的书，算是给你在学习路上的一个补充。我还制作了一个豆列，方便你去找到这些书。

编码实践

如果你想详细学习如何写好代码，我推荐你去读 Robert Martin 的《代码整洁之道》（Clean Code），这本书几乎覆盖了如何把代码写好的方方面面。

《实现模式》是一本关于如何写好代码的书，更具体一点是，编写别人能够理解的代码。它的作者 Kent Beck 是许多软件开发实践的开创者。但 Kent Beck 的写作能力一般，他的很多作品被埋没了。只有细细品味，才能体会到 Kent Beck 深厚的功力。

我提升自己编码水平的理解是从《程序设计实践》（The Practice of Programming）这本书开始的，这本书的作者是 Brian Kernighan 和 Rob Pike，这两个人都出身于大名鼎鼎的贝尔实验室，参与过 Unix 的开发。

如果你想从日常开发中提升自己的效率，可以读一下《卓有成效的程序员》。假如你不曾思考过这个问题，这本书会让看到一些不同的工作方式，我也给这本书写过一篇书评。不过，这本书里的技巧太具体了，所以，有一些已经有些过时了。

设计

SOLID 原则是一种面向对象软件设计原则。早在 1995 年，Robert Martin 就提出了这些设计原则的雏形，然后在他的《敏捷软件开发：原则、实践与模式》这本书中，比较完整地阐述了这五个原则，后来，他有把这些原则进一步整理，成了今天的 “SOLID”。有了设计原则做基础，这本书后面讲了设计模式，理解起来就容易多了。虽然书名是关于敏捷的，但这是一本讲设计的书。

设计和架构有什么区别？2017 年，Robert Martin 出版了《架构整洁之道》（Clean Architecture），他在其中告诉我们，二者没有区别。所以，这也是一本关于设计的书，给出了 Robert Martin 对设计的最新理解。你可以把它看成《敏捷软件开发：原则、实践与模式》的修订版。

《设计模式》不推荐阅读，它是设计模式的开山之作，但它的起点是 Erich Gamma 的博士论文，其写作风格偏向学术，而且中文版翻译得也很一般。这里将它罗列出来只是因为其历史重要性。如果你想学习设计模式，现在有一些更容易入门的书，比如《Head First 设计模式》。

Martin Fowler 的《企业应用架构模式》将软件开发当时常见的解决方案汇集成模式，今天看来很多模式已经习以为常，但当年出场可是技惊四座的。从这本书的名字你不难看出，它出版的年代是企业级开发盛行的年代。Martin Fowler 一直认为这本书没有写完，希望能够继续更新，但不知道何时能看到这本书的新版。

《Unix 编程艺术》也是一本讲软件设计的书，只不过，它选择的切入点是 Unix 中的设计，从中你可以学到“只做一件事，把它做好”、“文本化”等编程理念，有助于你改善日常的工作。这样的书，也就只有 Eric Raymond 这样沉浸编程几十年的人才能写出来。

工程实践

Kent Beck 有一本知名的软件工程之作《解析极限编程》（Extreme Programming Explained），它介绍了一种软件开发方法：极限编程。但更重要的是，今天很多主流的软件开发最佳实践都是从这里出来的。这本书可以理解成诸多最佳工程实践的总纲。

Martin Fowler 在 1999 年写下软件行业的名著《重构：改善既有代码的设计》（Refactoring: Improving the Design of Existing Code），把重构这个小圈子实践带到了大众视野。2018 年底，Martin Fowler 时隔近 20 年后，又写出了《重构》第二版。把他对这些年行业发展的新理解融入到重构实践中。重构应该有个目标，这个目标就是“重构成模式”，而这也是一本专门的书：《重构与模式》（Refactoring to Patterns）。

《测试驱动开发》是 Kent Beck 为世人展示 TDD 做法的一本书。它好的地方需要自己体会，Kent Beck 并没有显式的讲出来，比如：任务分解。

Jez Humble 和 Dave Farley 的《持续交付》（Continuous Delivery）让持续集成再进一步，将生产环境纳入了考量。乔梁，他是《持续交付》这本书的中文版译者，而且在这本书出版近十年后，他自己写了《持续交付 2.0》，把自己多年来关于持续交付的新理解整理了进去。

说到遗留代码和测试，我推荐一本经典的书：Michael Feathers 的《修改代码的艺术》（Working Effectively with Legacy Code），从它的英文名中，你就不难发现，它就是一本关于遗留代码的书。如果你打算处理遗留代码，也建议你读读这本书。这本书我也写过书评，你可以了解一下我对它看法。

领域驱动设计

Eric Evans 2003 年写了《领域驱动设计》，向行业介绍一下 DDD 这套方法论，立即在行业中引起广泛的关注。但实话说，Eric 在知识传播上的能力着实一般，这本关于 DDD 的开山之作，其写作质量却难以恭维，想要通过它去学好 DDD，是非常困难的。所以，在国外的技术社区中，有很多人是通过各种交流讨论逐渐认识到 DDD 的价值所在，而在国内 ，DDD 几乎没怎么掀起波澜。

2013 年，在 Eric Evans 出版《领域驱动设计》十年之后，DDD 已经不再是当年吴下阿蒙，有了自己一套比较完整的体系。Vaughn Vernon 将十年的精华重新整理，写了一本《实现领域驱动设计》，普通技术人员终于有机会看明白 DDD 到底好在哪里了。所以，你会发现，最近几年，国内的技术社区开始出现了大量关于 DDD 的讨论。

因为《实现领域驱动设计》实在太厚，Vaughn Vernon 又出手写了一本精华本《领域驱动设计精粹》，让人可以快速上手 DDD，这本书也是我向其他人推荐学习 DDD 的首选。

产品与需求

精益创业是 Eric Ries 最早总结出来的。他在很多地方分享他的理念，不断提炼，最终在 2011 年写成一本同名的书：《精益创业》。如果说精益创业是理论，《精益创业实战》这本书则给了你一个操作流程。

Mike Cohn 是敏捷理念的一个重要传播者，我们在讲测试金字塔时，提到了他的著作《Scrum 敏捷软件开发》（Succeeding with Agile）。敏捷开发有两大流派：一派是工程实践，另一派是管理实践。如果你对 Scrum 这类管理实践感兴趣，可以读一下这本书。

如果你对用户故事这个话题感兴趣，推荐阅读 Mike Cohn 的两本书《用户故事与敏捷方法》（User Stories Applied）和《敏捷软件开发实践 估算与计划》（Agile Estimating and Planning）。

开发文化

软件行业里有一本名著叫《人月神话》，这算是软件开发领域第一本反思之作。今天，我们讨论的很多词汇都出自这本书，比如，没有银弹、焦油坑等等。虽然这本书出版于 1975 年，但其中提到的问题，依然困扰着今天的程序员。

开源概念的提出者 Eric Raymond，他的《大教堂与集市》推开了开源大门。今天开源软件已经成为程序员日常工作的一部分，但如果没有 Eric Raymond 这些人的努力，我们还必须与复杂的企业级软件搏斗。了解一下开源的历程，可以帮助你更好地理解今天的幸福。

程序员应该如何做，Robert Martin 也写了一本书《程序员的职业素养》（Clean Coder），其中对大多数程序员最重要的一点建议是，说“不”。

软件开发拾遗

高德纳的《计算机程序设计艺术》肯定是一套程序员都知道，但没几个人读完的书。算法的讲解经过几十年已经有了很好的发展，如果学算法，肯定有更好的选择。如果你想看图灵奖获得者如何从根上思考问题，不妨找来这套书来翻翻。

《快速软件开发》（Rapid Development），不推荐阅读。在这本书中，作者首次提出了解决集成问题的优秀实践：Daily Build，每日构建。通过这个名字，我们便不难看出它的集成策略，即每天集成一次。它其中很多实践在当时是先进的，但今天看来有些落伍了。如果你只想从中收获一些理念性的东西，可以去读读。

《C 程序设计语言》、《Unix 编程环境》等出自贝尔实验室大师级程序员之手，他们的书都值得一读，其中的内容今天看来可能有些过时，但他们解决问题的方式和手法却值得慢慢品味。

我在讲淘宝技术变迁时，提到了《淘宝技术这十年》，这本书算不上经典，但可以当做休闲读物。

技术之外

管理大师彼得·德鲁克有一本经典著作《卓有成效的管理者》，虽然标题上带着管理者几个字，但在我看来，这是一本告诉我们如何工作的书，每个人都可以读一下。

尤瓦尔·赫拉利的《人类简史》或《未来简史》，是我第一次学到“大历史观”这个说法，历史不再是一个个单独的历史事件，而是一个有内在逻辑的发展脉络。

《从一到无穷大》是一本著名科普著作，它向我们介绍了 20 世纪以来的科学进展。作者乔治·伽莫夫既是热宇宙大爆炸模型的提出者，也是生物学上最早提出“遗传密码”模型的人。虽然这本书出版自 1947 年，但以现在社会的整体科学素养，还是有必要读读这本书的。

史蒂芬·柯维（Stephen Richards Covey）的《高效能人士的七个习惯》，其中的理念我在专栏两个不同的地方提到过，一个是讲以终为始时，那段关于智力创造的论述，另一个是讲优先级时提到的艾森豪威尔矩阵。这本书值得每个人阅读，很多程序员欠缺的就是这些观念性的东西。

很多程序员都是科幻小说迷，编程和科幻，这两个都是需要想象力的领域。刘慈欣的《三体》，不说它给 IT 行业带来的丰富的词汇表吧，作为科幻小说来说，它就是一流的，值得阅读。它会让你仰望星空，打开思维。如果你对科幻小说有兴趣，推荐阅读阿西莫夫的《银河帝国》系列，这是科幻小说界的扛鼎之作，你会看到，一部出版于 1942 年的书里就有大数据的身影。

对于程序员来说，最好的工作状态就是进入心流，它会让你忘我工作。如果你对心流的概念感兴趣，可以去读米哈里·契克森米哈赖的著作《心流》，这位作者就是心流概念的提出者。

好，今天的复习就到这里，你有哪些经典的书可以推荐给这个专栏的同学呢？欢迎在留言区写下分享你的想法。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

答疑解惑 | 如何在实际工作中推行新观念？

在整个专栏的最后一个大模块"综合运用"中，我们把前面学到的各种原则和知识穿插在一起应用在了不同的场景中。在这个模块的答疑中，我们也综合汇总一次，把整个专栏中出现的一些有趣却还没有来得及讨论的问题放在一起。

问题 1：想要推行 DDD，阻力很大怎么办？

段启超 同学提到

想在公司内推行 DDD，阻力真的很大，首先是很多人对 DDD 没概念，需要一定的学习成本，二是团队间相互隔离，沟通成本很高，起码的通用语言都很难达成。
——《37 | 先做好 DDD 再谈微服务吧，那只是一种部署形式》

段启超同学提到的这个问题是一个非常典型的问题，而且，这个问题并不仅仅局限于 DDD。你在一个地方看到了一些好东西：技术、实践，或是想法，然后想把它运用在自己的项目中，希望项目越做越好，越来越顺利。但在实际情况中，想在一个组织内推广一些不一样的东西，都会面临层层阻力。

我在《40 | 我们应该如何保持竞争力？》中提到了一个学习模型，你只要在学习区不断地练习新技能，很快就可以超越同侪。其中的原因是，大部分人只习惯待在舒适区，在舒适区的人能力上的进步非常有限。也因为在舒适区实在太舒适了，走出舒适区会让人产生焦虑，所以，人的内心是惧怕改变的。

你有良好的愿望，驱动你自己去改变是一件可控的事，有愿意和你一起改变的人是一件幸运的事，但你指望所有人一下子和你走上同一条道路，这是一件几乎不可能的事，即便你是很高层的领导，让所有人与你保持一致也不现实。

我曾经在一个大公司做过敏捷咨询，这还是由他们顶层领导推动的敏捷转型，但依然是困难重重。那些习惯于待在自己舒适区的人总会找到各种神奇的理由告诉你，他们的情况有多么特殊，这些最佳实践在他们那里是不适用的。

我们放弃了吗？并没有。我们的做法是，找一个团队来做试点。

换句话说，我们找到了几个愿意改变的人，把这些最佳实践应用在他们的项目上。在这种情况下，大家的目标是一致的，就是希望让这个项目得到改善。所以，大家自然会想尽一切办法，克服遇到的困难。比如，我们当时的切入点是持续集成。

他们的代码都在老旧的 ClearCase 上，每个人修改文件要先去竞争文件锁，特别不利于小步提交，所以，我们推动着将 ClearCase 改成了稍微进步一点的 Subversion。（好吧，你能听出来这是一个有些年头的故事。）

代码是用 C 语言编写的，在他们的代码规模下，编译时间会很长。于是，我们决定搭建一个分布式构建系统，这需要有很多台电脑。不过，他们的硬件是严格管控的，申请电脑是很困难的，虽然花了很大的力气，但最终我们做到了。

以往团队都是几天甚至几周才提交一次代码，我们先将代码提交的要求限定在每人每天至少提交一次，为此，我们专门坐下来与团队成员一起分解任务，将他们理解的大任务拆分成一个一个的小任务。

……

想做事，只需要一个理由就够了，不想做，理由有一万个。劝那些不想改变的人改变是异常耗时而且收效甚微。最好的办法是，找到愿意和你一起改变的人，做一件具体的事。

我们并没有劝说谁去听从我们的想法，只是在一个一个地解决问题。我们花了很长时间，最终建立起了持续集成，看到大屏幕上的绿色标识，我颇为感动。原本只需要一两天搭建的持续集成，在一个复杂组织中，它要花费那么长时间，这也是我从未经历过的。

当我们把这件事做成之后，其他团队看到了效果，开始纷纷效仿。于是，原本复杂的各种规定也开始纷纷松绑，比如，他们再也不需要为申请电脑发愁了。至于之前质疑我们的人，因为看到了成效，他们的关注点就成了怎么把事能做成。

后来我听说，他们在组织内部专门建立了一个持续集成中心，为各个团队提供了公共的构建资源，提升了整体的效率。

Linus Torvalds 曾经说过：“Talk is cheap. Show me the code. ”讲道理很容易，但也难以让人真正的信服。同样，做事很难，但成果摆在那里，让人不得不信服。

在英文中对这种行为有一个说法叫“Lead by Example”，通常用来形容团队领导以身作则的行事风格。当你寻求改变时，无论你的角色是什么，你都需要扮演好领导者的角色，“Lead by Example”送给你！

问题 2：测试怎么写？

andyXH 同学提到

目前对于 TDD 还是处于理解状态，不知道如何真正的在项目工程中使用。因为项目工程往往还有很多其他调用，如 rpc，数据库服务，第三方服务，不知道在这个过程如何处理。期待老师在之后文章中讲解。
——《13 | 先写测试，就是测试驱动开发吗？》

梦倚栏杆 同学提到

从数据库或者第三方 api 查询类内容需要写测试吗？这种测试怎么写呢？如果不需要写，会发现大量展示类系统不需要写测试了，感觉怪怪的。
——《16 | 为什么你的测试不够好？》

闷骚程序员 同学提到

假设我要测试的函数是一个关于 tcp 的网络发送函数，我想问一下，老师在写类似这样功能的单元测试是怎么实现的？
——《39 | 面对遗留系统，你应该这样做》

TimFruit 同学提到

问个问题，一般 web 服务依赖数据库，这部分如何做好单元测试？如果去掉数据库，很难测试相应的 sql 语句。
——《39 | 面对遗留系统，你应该这样做》

大家看到了，这是一类非常典型的问题。一般来说，如果写的测试是一些业务逻辑的测试，大多数人还知道怎么测，一旦涉及到外部系统、数据库，很多人就不知道该怎么办了。

我们先来回答一个问题，你要测外部系统的什么？

你当然会说，我的整个系统都依赖于外部系统，没有了它，我的系统根本运行不起来，不能完成工作啊！但是，我的问题是你要测的是什么？

我知道很多人一想到外部系统，第一反应是：“我的整段代码都是依赖于外部系统的，因为外部系统不好测，所以，我这段代码都没法测了。”如果你是这样想的，说明你的代码将对外部系统的依赖在业务代码中散播开了，这是一种严重的耦合。外部系统对你来说，应该只是一个接口。

我在《13 | 先写测试，就是测试驱动开发吗？》中说过，想写好测试，先要站在可测试的角度思考。假设我同意你关于外部系统不好测的观点，那应该做的是尽量把能测的部分测好。将对外部系统的依赖控制在一个小的范围内。

一个好的做法就是设计一个接口，让业务代码依赖于这个接口，而第三方依赖都放在这个接口的一个具体实现中。我在《34 | 你的代码是怎么变混乱的？》中提到了 SOLID 原则，这种做法就是 接口隔离原则（ISP）的体现。

如果你能够站在系统集成的角度思考，这个部分就是系统与系统之间的集成点。我在《37 | 先做好 DDD 再谈微服务吧，那只是一种部署形式》提到了 DDD。在 DDD 的战略设计中，有一个概念叫上下文映射图（Context Map），在不同上下文中集成最常见的一种模式是防腐层（Anti-Corruption Layer，ACL）。

很多系统在实现时就是缺少了防腐层，造成的结果就是系统耦合极其严重。因为外部服务的任何修改都会造成自己的代码跟着大幅度变动，更极端的情况是，我见过一个网关系统在自己的业务逻辑中直接依赖于第三方服务传过来的 JSON 对象，造成内存资源的极大浪费，网关本身极其不稳定。

至此，你知道了，如果有任何外部系统，都要设计防腐层，用接口做隔离。这样，才能保证你的业务代码是可测的。如果外部系统真的不好测，这种做法将大幅度降低不可测的比例，尽可能提高测试覆盖率。

我们前面的假设是，外部系统不好测，但真的不好测吗？

作为 Moco 这个模拟服务器的作者，我肯定是不会同意这个说法。如果你的系统依赖的外部系统是最常见的 REST 服务，那 Moco 就是给这种场景准备的。我给你看一个最简单的例子，这是 Moco 中最简单的用法：

@Test

public void should_response_as_expected() throws Exception {

    HttpServer server = httpServer(12306);

    server.response("foo");

    running(server, new Runnable() {

        @Override

        public void run() throws IOException {

            Content content = Request.Get("http://localhost:12306").execute().returnContent();

            assertThat(content.asString(), is("foo"));

        }

    });

}

在这个例子里，你设置外部服务的行为，让它按照你的需求返回特定的内容，然后，运行你的服务去访问这个外部服务，它和你访问真实服务效果是一样的。而且，通过 Moco，你还可以模拟出一些真实服务不可能给你做出的效果，比如，连接超时。

这里给出的是一个用 Java 编写的例子。如果你采用的是其他语言，也可以使用 Moco 的 Standalone 模式，用 JSON 配置出一个模拟服务器。

对于数据库的测试，如果你采用的是 Spring Framework，它就提供了一套完整的方案，比如：你可以在运行测试时插入一些数据，然后，在测试执行完毕之后，回滚回去，保证测试的可重复性。

事实上，它对测试的支持已经非常强大了，远不止于数据库。如果你采用的是 Spring Boot，对测试的支持就更加完整了，但基础还是 Spring Framework 提供的。如果用到真实的数据库，最好是一套独立的本地数据库，保证环境的可控。

对于外部服务的测试，简言之，能模拟的就模拟，能本地的就本地。如果你的服务没有现成的工具支持，也许就是一个打造新工具的好时机。

总结一下。关于外部系统的测试，你可以先通过接口隔离开来，然后，通过模拟服务或本地可控的方式进行测试。

好，今天的答疑就到这里，你对这些问题有什么看法呢？欢迎在留言区写下你的想法。

感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给你的朋友。

结束语 | 少做事，才能更有效地工作

在这个专栏里，我讲过很多东西，几乎涉及到软件开发的方方面面，但有一个重要的方面，我却从来没有说过，那就是算法。

因为我一直把它当做不言而喻的基本功，认为每个程序员都应该掌握。在我们专栏的结束语中，我就用这个没有涉及过的话题来开篇吧！

算法的差异

排序算法是每个程序员都会学到的内容，大家对各种算法也是如数家珍：插入排序、冒泡排序、归并排序、堆排序、快速排序等等。我们也知道各个算法的复杂度，比如，插入排序是 O(n^2），快速排序平均情况下是 O(nlogn）等等。

你有没有想过一个问题，不同算法的复杂度本质差别到底是什么呢？我们就以插入排序和快速排序为例，为什么快速排序要比插入排序快呢？

我不打算做算法分析，直接公布答案：因为做比较的次数少。为什么同样的排序，比较次数会有差异呢？因为插入排序每次循环只关注当前的目标，循环之间没有关系，而快速排序在做不同划分时，上一次的结果对下一次有助力，因此它省下了不少的比较次数。

明白了这个道理，再来看所谓的算法优化，其实就是尽可能利用已知的信息，少做不必要的事。

再来看一个常见的面试题，给你一堆数，找出前 100 个。很多人直觉就会想到排序，然后选出前 100 个。这种做法固然可行，但一定是做多了，因为这里需要的是找出前 100 个数，而不是要 100 个有序的数字，更不是要所有的数都有序。

说到这里，你就知道了，只要把数据划分开就好，并不需要排序，如果划分点不是第 100 个元素，就向着 100 所在的方向继续划分就好。

计算机是最擅长处理繁琐重复工作的，即便如此，我们依然要做算法优化，原因是当数据规模大到一定程度时，不同复杂度的算法差别就非常明显了。算法没用好，计算机硬件再好，也是徒劳的。

有一则《计算机程序设计艺术》作者高德纳（Donald Knuth）的轶事，他年轻时参加算法大赛，用最差的系统击败了诸多对手，拿到算法执行效率的冠军，凭借的就是其强大的算法优化功力。

对于计算机，算法尚且如此重要，我们面对工作时何尝不是如此呢！

有效工作

《10x 程序员工作法》，也许有的同学最初看到这个标题就急急加入了，以为会从这个专栏中学习到一些“以一抵十”的编程技法，对不起，我彻底让你失望了。我非但没讲太多编程的技法，甚至还从各种角度劝你少写代码：无论是向产品经理提问题，还是让你在前面多考虑设计。

难道不是做得越多才越高效吗？

插入排序并不会因为干的活多，就比快速排序得到更高的评价，因为它们比的是谁排得快。工作效率高，不是因为代码写得多，而是有效工作做得多。

如果 CPU 都被无效指令占据了，哪有时间执行有效指令呢？即使你很忙碌，但工作进展依然是收效甚微，因为无效工作占据了你太多的大脑，让你不能聚焦在正经事上，当然就是效率不高了。

其实，这个专栏的内容在我脑子里已经盘旋很多年了。不过，即便在专栏筹备期，我已经备了很多篇稿子之后，我依然没有找到一个准确的说法能够描绘内心的想法。

我想过“程序员的职业素养”，但似乎这会让专栏朝着职场行动指南的方向努力；我想过“高效工作”，但实际上我也不打算讨论那些工作技巧。直到上线日期临近，我的编辑实在受不了我的拖延，坐下来与我交流了很久，我才终于找到了内心的那个词：有效。

我在这个专栏真正探讨的主题是，有效工作。

有效工作，需要我们把力量聚焦到正确的地方，做本质复杂度（Essential Complexity）的事情，少做无意义的事情。

我曾经在一个大公司做咨询，按照他们的统计，线上 60% 的代码从来没有运行过。我们都知道，一多半的代码增加的可不只是一多半的工作量，团队可能需要的是几倍甚至几十倍的心力去维护它。

当然，有效工作最终没有成为这个专栏的名字，而用了更有个性的《10x 程序员工作法》。这个名字也不错，因为在我看来，很多程序员做的是负功，比如，写那 60% 代码的程序员。只要能做到有效工作，效率自然会高出业界平均水平很多。

怎么才能有效工作呢？我在专栏中已经给你讲了很多，小结一下就是：

拓展自己的上下文，看到真正的目标，更好地对准靶子，比如，多了解用户，才不至于做错了方向；站在公司的层面上，才知道哪个任务优先级更高；站在行业的角度，而不局限于只在公司内成为高手，等等。

去掉不必要的内容，减少浪费，比如，花时间分析需求，不做非必要的功能；花时间做好领域设计，别围着特定技术打转；花时间做好自动化，把精力集中在编码上，等等。

要想有效工作，有两点非常重要。一方面，意识上要注意自己工作中无效的部分。这就像一个开关，拨过去就好了。所以，读这个专栏有人常有恍然大悟的感觉，也有人觉得很简单。

很多时候，你只是不知道，就像我在专栏中提到，要问产品经理问题，这是很多人没想过的。每篇文章后面的那一句总结，就是这样的开关，拨过去就好。

另一方面，要构建自己关于软件开发的知识体系，这是要花时间积累的。在这个专栏中，我给你讲了很多最佳实践，就是让你知道，在某些方面，有人已经做得很好了，花时间学习，比自己从头摸索好很多。

这就像所有的数学公式一样，理论上你都可以自行推导，但肯定不如从教科书上学得快。

藏经阁目录

虽然我讲了这么多内容，但实际上，因为篇幅的关系，这只是冰山一角。其实，我给你讲的这部分内容并不是具体的知识，而是告诉了你哪些东西要去学习，给了你一张学习地图，把各种知识贯串了起来。

我曾与朋友打趣道，我的专栏实际上是藏经阁的目录，真正的经书还要等你自己去参悟。只不过，有一个人把这些经书之间的知识连接给你补齐了。这些连接恰恰是在学习相关内容时，让我苦思冥想许久的。

大约一年前（2018 年 4 月），极客时间编辑找到我，问我是否有兴趣在极客时间开个专栏，作为“得到”重度用户的我，一直对知识服务很感兴趣。有这样的机会让我体验，我当然想试试，甚至最初给自己定下了写 100 篇的宏伟计划。

真正开始写，我才知道，在繁忙的日常工作之余，坚持写作还是一件很有挑战的事，今天看来，100 篇的目标显得那么无知无畏。

不过，也正是因为压缩到一半左右的篇幅，在专栏后面的部分，我才极大地提高了知识密度，比如，微服务和 DDD，这两个可以分别写成一个系列内容的话题，我用一篇文章就将其精华和知识脉络提炼呈现了出来。

因为我想尽我所能，帮助大家构建起一个软件开发的知识体系，让你在未来遇到问题时，知道可以在哪个方面进一步加强。希望这个专栏真的起到帮你理清思路，答疑解惑的作用。

还记得我在开篇词中的最后一段话吗？

也许在这个专栏的最后，你发现自己并不认同我的原则，却能够用自己的原则来与我探讨，那么，恭喜你，因为那是最美妙的事情！

不知道你是否形成了自己的原则呢？欢迎与大家分享。因为它代表着你已经形成了自己的知识体系。与我讲了些什么相比，你学到了什么才是一件更重要的事。

希望在学习了这个专栏之后，你可以用自己的工作原则做更多本质复杂度的事情，减少无意义的时间消耗。

其实，这个专栏的最大收益人是我自己，感谢这次的专栏之旅，我终于强行治疗了我的拖延症，把自己对于有效工作的思考完整地整理了出来，那些在脑子里模糊的印象现在终于有了一个完整的体系。这个体系就是我在专栏里提到的工作原则，现在我可以更好地表达自己的想法了。

不过，这个专栏于我而言也是有遗憾的。因为我想表达的内容很多，给大家打开更多大门的同时，也给很多同学留下了更多的疑问。

有些同学期待在某个方面再深入细节地讲一下，比如，DDD，那可是值得再写一个专栏的主题。限于这个专栏的主题和篇幅关系，我没办法深入展开，只能对大家说声抱歉了。

如果以后有机会，我会再来与你分享我对软件开发的理解，这次的《10x 程序员工作法》之旅就暂告一段落了！

再见！