读书笔记:Google软件工程
Google软件工程,作者: [美]提图斯·温特斯 / [美]汤姆·曼什雷克 / [美]海勒姆·赖特
目录
序 1
前言 3
软件组织在设计、架构和编写代码时应该牢记的三个基本原则:
时间与变化:代码在其生命周期内需要如何适应。
规模与增长:组织在发展过程中要如何适应。
权衡与成本:组织如何从时间、变化、规模和增长中所学到的经验来做出决策。
第一部分 理论
第1 章 什么是软件工程 13
无论是出于技术理由还是业务理由,你都有能力对任何有价值的变化做出反应,那么你的项目是可持续的。当你无法对底层技术或产品方向的变化做出反应时,你不得不顶着巨大的风险,把希望寄托于这类升级变更永远也不会变得那么至关重要。对于短期项目来说,这可能是个安全的选择。但对于长期项目就不是了。
时间与变化 15
海勒姆定律18
当有一个API有足够多的用户时,在约定中你所承诺的已不重要:所有在你系统里面被观察到的行为都会被一些用户所依赖。
案例:哈希排序 18
为什么目标不是“没有变化”呢 20
规模与效率 21
这本书的大部分内容都集中在生产这样一台机器的组织其所面临的规模扩大带来的复杂性,以及保持这台机器持续运行流程所面临的复杂性。
如果测试集群的计算成本呈超线性增长,每个季度人均消耗更多的计算资源,那你将走上一条不可持续的道路。
软件组织最宝贵的资产代码库本身也需要扩展。如果你的构建系统或版本控制系统随着时间以超线性方式扩展(可能是由于变更日志历史记录的增加)。
阻碍规模化的政策 22
很容易发现那些扩展不佳的政策。这项工作是否随着代码库的增长而扩大?如果没有机制来自动化优化,我们就有规模化的问题。
1 常用的弃用方法是规模化问题的一个很好的例子。“我们将在8月15日删除旧的插件,请转换到新的插件”。(第15章讨论)
2 可扩展的方式解决这些问题意味着改变我们弃用的方式:团队不必把迁移工作推给相关依赖的团队,而是利用规模化的能力在团队内部解决这个问题。
3 “搅动规则(churn rule)”,这一政策可扩展性很好。由一个特定的专家组来执行大规模变更,要比要求每个团队自己来做变更会更具有可扩展性。
4 代码分支方式是另一个规模化问题的政策示例。将大型功能合并到主干(trunk)上时,会破坏产品的稳定性。(第16章讨论)
促进规模化的政策 24
什么样的政策会带来更好的成本效应?
专业知识和共享的交流论坛在组织规模化上提供了巨大的价值。友好且乐于助人的Java专家愿意回答问题,那么很快就会使这一百个工程师写出更好的Java代码。(第3章讨论)
案例:编译器升级 24
如何评估你的代码库是否与该变更兼容?
在我们意识到编译器升级任务是痛苦的之后,我们开始关注技术和组织变革,以克服规模化问题,并将规模化转化为优势:自动化,整合/一致性(低层次 变化限制在一定的问题范围内),专业知识(这样少数人就能做得更多)。
基础设施的变更频率越高,变更就越容易做。都希望代码库以后的升级都比第一次升级更容易。
影响代码库灵活性的因素:
- 专业知识:
- 稳定性:因为更频繁的升级版本,不同版本间变化较少。
- 一致性:频繁升级,使得没通过升级的代码更少。
- 熟悉:执行升级过程中发现冗余并尝试自动化。
- 政策:碧昂丝原则
根本的教训不是关于编译器升级的频率或难度,而是一旦我们意识到编译器升级任务是必要的,我们就找到了方法来确保用固定数量的工程师来执行这些任务。
停滞不前的确是一种选择,但往往不是一种明智的选择。
左移思想 26
在开发人员工作流早期发现问题通常会降低成本。
在开发作业流时间线(评审,测试,提交,部署),将问题检测移到更早的“左边”会比等到更晚再检测成本更低。
权衡与成本 28
目标:人们强烈反感“我说了算”,我们希望看到“我不同意你的标准/评估,但我了解你是如何得出这个结论的”。每件事都需要有一个理由。
“成本”大致可转化为所做的努力,因素:
- 财务成本(如金钱)
- 资源成本(如CPU时间)
- 人员成本(如工程工作)
- 交易成本(如采取行动的成本)
- 机会成本(如不采取行动的成本)
- 社会成本(如对整个社会产生什么影响)
原因:社会成本尤其容易被忽视。谷歌拥有数十亿用户的产品。微小差池都会被放大,往往会损害一些被忽视的群体。我们在做出产品和技术决策时,好的和坏的方面都要考虑到。(第4章讨论)
案例:白板笔 29
你的思路有多少次因丢失的白板笔而中断?为了一个不到一美元的东西。
结论:对此我们做了一个明确的权衡:为优化无障碍的头脑风暴比防范拿着一堆白板笔离开的人要重要得多。
“谷歌是一种数据驱动的文化”,事实上,这是一种简化:即使没有数据,也可能有证据,先例和论证。做一个好的工程决策就是权衡所有可用的输入。用尽各种方法来衡量成估计真正的潜在成本之后。
方法:一个工程组的决策应该归结为以下几点:
- 我们这样做是因为我们必须(法律要求,客户要求)。
- 我们这样做是因为根据目前的证据,这是我们当时能看到的最佳选择(由适当的决策者决定)。
决策投入 30
案例:分布式构建 30
案例:时间与规模的博弈 32
数据驱动的决策 32
致力于数据驱动文化,有一个不为人知的好处,那就是:它将承认错误的能力和必要性结合在了一起。
软件工程VS 编程 33
小结 34
本章要点 34
随着时间的推移,数据驱动意味着当数据改变时(或当假设被消除时)需要改变方向。错误或修订计划是不可避免的。
第二部分 文化
第2 章 如何更好地参与团队合作 37
隐藏代码 37
问题:不安全感是人性的一部分,没有人喜欢被批评,尤其是害怕自己正在做的事情被别人批评。
天才神话 38
隐藏有害 40
回答:软件开发工作中的协同和评审非常重要。
及早检测 41
巴士系数 41
小步快跑 42
程序员在紧凑的反馈环中效率是最高的:写一个新函数,编译。
“开放式办公模式”极细微的谈话内容都会变成公开的,最后大家只好闭嘴不谈,久而久之,开放式办公室最终也会变得和私人办公室一样的糟糕。
拒绝隐藏 44
一切为了团队 44
你能说出多少个被广泛使用且成功的软件是由一个人写成的?(有些人可能会说“LaTex”,但它很难被广泛使用。)
社交的三大支柱 45
这三项原则是所有良性互动和合作的基础:
1 谦虚
2 尊重
3 信任
三大支柱的重要性 46
毕竟处理社交问题确实复杂:只要是和人有关的事情都会很麻烦,不可预期。
不要低估社交的力量,人际关系总是比项目更持久。
谦虚、尊重和信任 46
只有当一个概念在白板上确实没办法被所有同行推翻时,才会开始去做早期的原型。
无指责的回顾文化 50
一份好的回顾分析文档应包括以下内容:
- 事件的简短摘要。
- 事件时间表,从发现到调查解决。
- 导致该事件的主要原因。
- 影响和破坏评估。
- 可立即修复问题的行动事项。
- 防止事件再次发生的行动事项。
- 经验教训。
接受影响
工程学本质上就是关于权衡取舍的
职业政客们显然对某一个问题有错误的认识或表现得很无知,他们也以从不承认错误或无知而臭名昭著。这种行为之所以存在,主要是因为政客们经常受到对手的抨击。
谷歌范儿(Googley) 52
小结 53
本章要点 53
如果你想在一个团队或大型组织中有效工作,就要了解自己和他人喜欢的工作方式。
第3 章 知识共享 55
学习的挑战 55
缺乏心理安全感:在一种人们害怕在别人面前冒险或犯错误而担心受到惩罚的环境下,人们往往表现为一种恐惧文化或避免透明度的倾向。
信息孤岛:知识碎片化,组织内每个部门之间彼此不通信,也不使用共享资源。
单点故障SPOF:当关键信息只能从单个人处获得时,就会出现瓶颈。
我们很容易陷入一种习惯,就是找最有能力处理这件事情的人来解决。这种方法短期来看效率很高,解决问题很快,但是长远来看,代价很大。解决关键问题的方法掌握在个别人手中。其他人也没有机会去学习问题的处理方式,知识掌握不均衡,有的人拥有全部知识,而有的人一无所知。
闹鬼墓地:人们避免接触或更改代码的原因是他们害怕出错。闹鬼墓地的特点就是人们因为恐惧和迷信而避免采取行动。
知识共享的哲学 57
书面知识有规模化优势,可以让更多人获得这些知识,但通过访谈目标人群获取他们脑袋中的知识也非常有必要。因为专家有能力综合归纳他们脑中的知识体系,他们可以评估哪些信息适用于哪种使用场景,确定文档之间的相关性,并知道哪里可以找到这些知识。
设定基调:心理安全 58
导师制 58
导师不是Noogler所在团队的成员、经理或技术主管,导师的职责明确包括:回答问题和帮助Noogler快速成长。
大型群体的心理安全 59
群体交互反模式:
不要假装惊讶(“什么?!真不敢相信你竟然不知道什么是堆栈!”)
假装惊讶是心理安全的障碍,而且会让团队成员害怕承认自己缺乏知识。
不要故弄玄虚
卖弄学问的纠正,往往是哗众取宠,而不是提供精确答案。
不要乱出主意
打断既有讨论,提供意见,但不对谈话内容负责。
不断充实知识 60
提问 60
理解上下文61
Chesterson's Fence围栏原则:在去除或改变某事物之前,首先了解它为什么会在那里。
扩大提问渠道:向社区提问 62
帮你自己一个忙:当你从一对一的讨论中学到一些东西时,把它写下来。
群聊 62
群聊往往要么是面向主题的,要么是面向团队的。主题驱动的群聊通常是开放的,这样任何人都可以顺便来问一个问题。
邮件列表 63
在公共邮件列表上提问和在群聊中提问非常类似:一个问题会传送到很多可能回答它的人那里,任何跟随列表的人都能从中学习。
YAQS:问答平台 63
分享你的知识:你总有可以教别人的地方 64
Office Hours 64
技术讲座与课程 64
文档 65
更新文档
在首次学习某件事情时,最好先看看现有文档和培训材料如何改进。你也许忘记了“快速入门”文档中哪些是难点。那么在你刚接触一个文档时,如果你在文档中发现错误或遗漏,请马上进行修复!
在谷歌,工程师们感觉有权更新文档,而不管谁拥有文档的所有权。我们经常这么做,即使只是修复类似错别字这样的小事情。随着g3doc的引入,文档会留下可审计的变更历史轨迹。
文档推广
鼓励工程师记录他们的工作可能是困难的。编写文档需要花费时间和精力,而这些工作所带来的好处不是立竿见影的,只是为了其他门提供方便。
假设团队成员总是要求你帮助调试某些类型的生产故障,方法是将团队成员引导向文档,并且在需要时再提供实际帮助。
代码 67
组织知识发展 68
尊重
少数几个人的不良行为破坏社区,在这样的环境中,新手会到别处去提问,而潜在的新专家则停止尝试去回复那些提问,失去成长的空间。
培养知识分享的文化 68
对“天才混蛋”的宽容是有害的:虽然在较高层次上需要某种程度的技术领导,但并非所有领导力都针对技术问题,领导者们要提高周围员工的素质,改善团队的心理安全。
激励与奖赏
组织常常会犯一个错误是反而积极奖励那些不遵循价值观的行为。
通过明确定义出期望的行为来鼓励工程师分享知识。
Julia是邮件列表的头号贡献者,经常在邮件列表中回复读者的问题,让读者从中受益,因此Ravi送给了Julia一份同行奖金。它能够产生重要而强大的基层效应。
建立规范的信息源 70
一个对指南相当熟悉的专家只需发送一个链接给一个工程师同事,专家无需给他亲自解释整个公司的做法,从而节省了时间。
让信息流动73
可读性:通过代码评审规范化指导 74
什么是可读性流程 74
为什么需要可读性流程 75
小结 78
本章要点 78
第4 章 平等工程 79
人类的偏见 80
理解多样性的必要性 82
建立多元文化能力 82
使多样性具有可操作性 84
拒绝单一的方式 85
挑战既定流程 86
价值观与成果 87
保持好奇心,向前推进 88
小结 88
本章要点 89
第5 章 团队领导的艺术 91
经理和技术主管(或两者兼任) 91
工程经理 92
技术主管 92
技术主管经理 92
非职权影响力
当你需要让组织之外的人去做你认为需要做的事情时,情况就不同了。这种“非职权影响力”是你可以培养的最强大的领导特质之一。
从个人贡献者到领导者 93
如果产品想要有所进展,无论是否被正式任命,总要有个人站出来把握方向。假如你恰好是那种有动力且没有耐心的类型,很可能这个人就是你。
唯一需要担心的是……嗯,一切 94
谷歌会要求员工在一段时间内完成高于当前水平的工作(即在当前水平上“超过预期”),然后再晋升员工,从而避免员工上升到他不能胜任的地位。
如果一个工程师能够编写大量优秀代码,却完全没有管理人员或领导团队的愿望,强迫他们担任管理角色,你就失去了一个优秀的工程师,得到了一个差劲的经理。
仆人式领导95
管理者们似乎染上了一种疾病:他会忘记他原来的管理者对他做过的所有糟糕的事情,而突然开始用同样的方法来“管理”自己的下属。
工程经理 96
“经理”是一个令人厌恶的词 96
经理们对待员工的方式经常就像骡夫对待他们的骡子一样:他们用胡萝卜引导他们前进,当这不起作用时,就用大棒鞭策他们。
如今的工程经理 96
反模式 98
反模式:雇用平庸的人 98
反模式:忽视低绩效员工 99
忽视低绩效员工,不仅阻碍新的高绩效员工加入团队,还会导致现有高绩效员工的离职。你最终会得到一支表现欠佳的团队,因为这些人根本不会自愿离开团队。
反模式:忽视“人”的问题 100
反模式:做老好人 101
反模式:打破招聘门槛 102
当你试图快速招聘时,一种常见的做法是,一个团队需要招聘5名工程师,因此它会对一堆申请进行筛选,面试40或50人,然后选出最好的5名候选人,不管他们是否符合招聘标准。这是建立一个平庸团队的最快方法之一。
与不得不和一个原来就不该被聘用的员工打交道的成本相比,招聘的成本都显得微不足道。
反模式:像对待孩子一样对待你的团队 102
积极的模式 103
抛弃“自我”意识 103
成为一名禅师 104
你越是处在链条的前端,就可以越快地让你下面的齿轮旋转,无论你是否有意为之。
当一个团队成员向你征求意见时,你通常会跳进解决方案模式,但这是你最不应该去的地方。寻求建议的人通常不想让你解决他们的问题,而是想你帮助他们解决问题,最简单的方法就是问这个人问题。
成为催化剂106
努力建立团队共识是一种非正式领导者经常使用的领导技能,因为这是一种你可以在没有任何实际权力的情况下领导的方式。如果你有权力,你可以指挥和支配方向,但总的来说,这不如建立共识有效。
移除障碍 106
你并不需要知道所有答案,但你通常可以帮助找到能消除障碍的人。在许多情况下,认识正确的人比知道正确的答案更有价值。
成为老师和导师 107
一开始教人和给他们一个自学的机会可能非常困难,但这是有效领导的重要组成部分。
成为一名导师并不需要很多正规的教育或准备。你需要三样东西:团队流程和系统的经验,向其他人解释事情的能力,以及评估学员需要多少帮助的能力。
设定清晰的目标 107
坦诚 108
追踪幸福感109
出乎意料的问题 110
其他提示和技巧 111
担任领导职位时其他提示和技巧:
- 委派工作,也要亲自动手。或者你组建了一个新的团队,那么要赢得团队的尊重并跟上他们的步伐,最简单的方法之一就是亲自动手,通常是承担一项别人都不想做的繁重任务。
- 寻找替代者。记住,有些人喜欢只做高效的个人贡献者,这是可以的。
- 保护团队远离混乱。混乱一直存在,但我的前任经理却保护我的团队不受影响。
对待人像植物一样 113
内在激励和外在激励 114
小结 115
一个有效的经理所带来的最重要的技能是社交技能。
本章要点 115
第6 章 大规模团队领导力 117
为更大的团体服务,你开始意识到你以前的工程专业知识与你的工作正变得越来越不相关。
直到有一天你发现,作为领导者,你实际上比作为个人贡献者有更大的影响力。
如何成为一个优秀的领导者?三个总是,总是在做决策,总是不在场,总是在扩展。
总是在做决策 118
你所做的大多数决定都是关于找到正确的折中方案。
飞机的故事 118
这种权衡是显而易见的(“如果我们投入到这个项目上,就会拖延另一个项目......”)
不管是一个团队还是更大的组织的领导者,你的工作是引导人们去解决那些因素,模糊不清的问题。所谓模糊不清,是指这个问题没有明显的解决方案,甚至可能无法解决。
如果写代码类似于砍树,那么作为领导者,你的工作就是“透过树木见森林”,找到一条穿过森林的可行路径,引导工程师们前往那些重要的树木。
识别盲点 119
识别关键的权衡 120
通常没有神奇的“银弹”解决方案,一个答案在任何情况下都永远有效,这是不可能的。
决策,然后迭代 120
总是不在场 122
你的使命:建立一个“自驱”的团队123
划分问题空间 123
子问题授权给领导者
在你执行任务之前,问自己一个问题:我真的是唯一可以完成这项工作的人吗?
你亲自动手可能是最有效率的,但是那样你就无法培养你的领导者,你不是在建立一个自立的组织。
我能做什么是我的团队中其他人做不了的事?
“向上管理”也很重要,要确保你的管理链了解你的团队正在做什么,并和整个公司保持连接。
你不断绘制森林的地图,然后把砍树的工作分配给其他人。
谨慎地锚定一个团队的身份
将团队身份固定到特定的解决方案(“我们是管理Git仓库的团队”)团队很可能会“严防死守”,捍卫其解决方案,并抵制变化。如果团队变成了负责一个问题(例如,“我们是向公司提供版本控制的团队”),那么随着时间的推移,团队就可以腾出手去尝试不同的解决方案。
总是在扩展 126
成功的循环127
成功的螺旋是一个难题,这是很难管理的,但它是扩展一个团队的团队(team of tems)的主要范式。
重要VS 紧急 128
当你进入领导岗位时,你可能会注意到,你的主要工作方式变得越来越难以预测,更多的是在救火,也就是说,你的工作变得不那么“主动”,而是更加“被动”了。
如何才能迫使你主要在重要事情上工作的关键方法:
- 授权。许多紧急事情都可以授权给你的组织中的其他领导者。
- 安排专门的时间。定期抽出两个小时或更长时间安静地坐着,只做重要但不紧急的事情。
学会放弃 130
如果你能识别出最关键的东西,那么你就应该扔掉其他的80%。
你只需要相信,低于你前20%门槛的任务,要么会被妥善处理要么会得到适当的升级。
保护你的精力 131
- 享受真正的假期。如果你查看工资邮件或聊天记录,就毁了这次“充电”。只有当你真正懂得如何切断其他人的联系时,你的假期才会充满乐趣。
- 让切断联系变得轻而易举。
- 白天小憩。花10分钟在室外散步。像这样的短暂休息只是一次小小的充电。
- 给自己一天心理健康假。如果你处于糟糕的情绪中,就转身回家,宣布请一天病假,当天什么也别做比造成主动的伤害好。
小结 133
本章要点 133
第7 章 度量工程生产力 135
为什么要度量工程生产力 135
随着组织规模的线性增长,沟通成本呈几何级数上升。
鉴别:它值得度量吗 137
根据目标和信号来选择有意义的指标 141
我们使用目标/信号/指标(Goals/Signals/Metrics)框架来指导指标创建。
- 目标:期望达成的最终结果。
- 信号:用来判断我们是否已经得到了最终结果的东西。
- 指标:信号的代替。
目标 142
生产力分为五个核心要素(QUANTS)。
- 代码质量(Quality of the code)
- 工程师的专注力(Attention from engineers)
- 认知复杂度(Intellectual complexity)
- 节奏和速度(Tempo and velocity)
- 满意度(Satisfaction)
信号 144
指标 145
使用数据验证指标 145
采取行动并跟踪结果 150
小结 150
本章要点 150
第三部分 流程
第8 章 风格指南与规则 155
规则就是法律。指南提供了建议和最佳做法。
为什么要有规则 156
创建规则 157
指导原则 157
保持一致性
谷歌员工的工卡任何一个办公地点的门禁都能识别;任何谷歌设备都能连上WIFI。
风格指南 165
修改规则 168
流程 169
风格仲裁者170
例外情况 170
指南 171
应用规则 173
规则当它们被强制执行时,就会产生更大的价值。规则可以通过教学和培训这种社会性方式实施,也可以通过工具的技术手段实施。
自动化的规则执行确保了规则不会随着时间的推移或组织规模的扩大而丢失或遗忘。新人加入时,他们可能还不知道所有的规则。
错误检查工具 174
我们使用像clang-tidy(https://oreil.ly/dDHtl)(适用于C++)和Error Prone(https://oreil.ly/d7wkw)(适用于java)这样的自动化强制执行规则。
代码格式化工具 175
C++使用clang-format(https://oreil.ly/AbP3F);python使用yapf(https://github.com/google/yapf);go使用(https://golang.org/cmd/gofmt);Dart使用dartfmt;BUILD文件使用buildifier(https://oreil.ly/-wyGx)
小结 177
本章要点 177
第9 章 代码评审 179
代码评审流程 180
谷歌如何进行代码评审 181
代码评审的好处 184
代码评审流程和认真对待代码评审的文化提供了以下好处:
- 检查代码正确性
- 确保代码变更能够被其他工程师理解
- 增强整个代码库的一致性
- 心理上促进团队的责任感
- 知识共享
- 提供代码评审本身的历史记录
代码正确性185
代码理解 187
代码一致性187
心理和文化方面的好处 188
知识共享 189
代码评审最佳实践 190
礼貌而专业190
小的变更 191
清晰的变更描述 193
评审者数量最少化 193
尽可能的自动化 194
代码评审类型 194
绿地代码评审 194
行为变更、改进和优化 195
缺陷修复和回滚 195
重构和大规模变更 196
小结 197
本章要点 197
第10 章 文档 199
大多数工程师在编写,使用和维护代码过程中,有一个共同抱怨就是文档质量的问题,“这种方法的副作用是什么?”,“在步骤3后发现一个错误。”,“这个文档是最新的吗?”每个软件工程师在他们的职业生涯中都抱怨过文档的质量、数量或甚至缺乏文档,谷歌的工程师也不例外。
能够让工程师们更容易地编写 出高质量文档的关键点是引入与组织规模相匹配的流程和工具,并能嵌入现有的工作流中。
每个人都认识到需要付出更多的努力来改进文档质量,但是组织上还没有承认文档的关键作用。
什么是文档 200
为什么需要文档 200
但是由于文档的受益者是下游用户,所以通常不会给文档作者带来直接的好处。不像测试,可以迅速的让程序员看到好处。
在文档上的投资会随着时间的推移而产生回报。毕竟,你仅仅写一篇文档,但它却会被阅读成百上千次。
像代码一样对待文档 202
文档常常与代码紧密耦合,因此应该尽可能将其视为代码。
- 置于源代码管理系统之下。
- 进行变更评审(并随文档中所记录的源代码进行变更)。
- 如果可能的话,要对准确性、有效期等方面进行度量。(目前还没有工具可以做到这一点。)
案例研究:谷歌的Wiki
由于没有真正的文件所有者,许多文件都过时了,无人更新和维护。没有新增文档的流程,重复文档和文档集问题开始出现。
最有能力修复文档的人通常不需要在编写文档后查阅它们。
改善这种状况的方法,是将重要的文档迁移到跟踪代码变更的源代码管理系统下。
引入Markdown(一种常见的文档格式化语言)对文档质量也是有帮助的,因为它使工程师容易理解和如何编辑文档。
了解文档的读者 204
工程师在编写文档时,会犯一个错误,那就是:假设他们自己必须是优秀的文档编写者。按照这个标准,很少有软件工程师会去写文档。
读者的类型205
几乎所有的文档作者都要花时间去做这些事情。“如果我有更多时间,我会给你写一封更短的信。”保持文档的简短和清晰。
文档类型 206
参考文档 207
设计文档 210
新手教程 210
编写新手教程的最佳时间(如果还没有新手教程的话)是第一次加入团队的时候(这也是在现有教程中寻找bug的最佳时机)。
概念文档 212
着陆页面 213
文档评审 214
如果想要“测试”一个文档,通常也会邀请其他人帮助评审。
文档的哲学 216
WHO,WHAT,WHEN,WHERE 和WHY 216
- WHO之前讨论过,其实就是文档的受众。
- WHAT即文档的写作目的。
- WHEN即文档是何时创建、评审或更新的。
- WHERE信息通常也是暗含在文档中的,决定文档应该位于何处。
- WHY即设置该文档的原因。
开头,中间和结尾 217
优秀文档的要素 217
一篇文档的好坏通常从三个方面来判断:完整性、准确性和清晰性。
丢弃文档 218
什么时候需要技术文档工程师 219
小结 219
文档必然是主观的,文档的质量不是由作者来度量,而是由读者来度量,而且不是同步发生的。
本章要点 220
第11 章 测试概述 221
为什么要写测试 222
“Google Web Server”的故事 223
当今开发速度下的测试 224
编写,运行,响应 224
测试代码的好处 227
设计测试套件 228
测试粒度 229
测试范围 233
碧昂丝规则235
碧昂丝规则(https://oreil.ly/X7-z):if you love you can put a ring on it. 如果你喜欢它,你就应该测试它。
代码覆盖率236
谷歌规模下的测试 237
谷歌的大部分代码都保存在一个巨大的单一代码库(https://oreil.ly/qSihi)中。
另一个让谷歌有点不同的地方是,几乎没有团队使用代码分支。所有的软件构建都是使用已经在测试基础设施中验证过的最后一个提交的变更来执行。
大型测试套件的陷阱 238
谷歌测试的历史 239
入职培训课240
测试认证 241
“马桶测试” 241
如今的测试文化 242
自动化测试的局限性 243
小结 244
本章要点 244
第12 章 单元测试 245
可维护性的重要性 246
防止脆弱的测试 247
努力做到不更改测试 247
通过公共API 进行测试 248
测试状态,而不是交互 252
编写清晰的测试 253
使测试完整简洁 254
测试行为,而不是方法 255
行为驱动的测试往往比面向方法的测试更清晰,原因有三。首先,它们读起来更像自然语言,让它们自然地被理解,而无需费力去分析。其次,它们更清楚地表达了因果关系(https://oreil.ly/dAd3k)。最后,每个测试都是简短和描述性的。
将测试结构化以便强调行为
每个行为都有三个部分:“Given”定义系统的预设条件,“When”定义要在系统上执行的操作,“Then”验证结果。
一些框架比如Cucumber(https://Cucumber.io)和Spock(http://spockframework.org),直接固定了given/when/then的结构。
以行为为测试命名
测试命名非常重要,它通常是测试失败报告中第一个或唯一可见的标记,因此当测试被破坏时,它是你交流问题的最佳机会。这也是表达测试意图的最直接的方式。
不要把逻辑放进测试 260
编写清晰的失败信息 261
更好的失败信息可以清楚地区分实际状态和预期状态
Expected an account in state CLOSED, but got account: <{name:"my-account", state:"OPEN"}
测试与代码共享:DAMP,而不是DRY 263
共享值 265
共享设置 267
共享helper 和验证 269
定义测试基础设施 270
小结 270
本章要点 270
第13 章 测试替身 273
测试替身的使用通常被称为模拟(mocking)
测试替身对软件开发的影响 274
谷歌的测试替身 274
基本概念 275
测试替身的示例 275
缝(Seams) 276
模拟(Mocking)框架 277
大多数编程语言都有模拟框架。在谷歌,Java使用的是Mockito, C++使用的是Googletest的googlemock组件(https://github.com/google/gooletest),Python使用的是unittest.mock(https://oreil.ly/clzvH)
使用测试替身的技术 279
伪造(Faking) 279
打桩(Stubbing) 279
打桩(https://oreil.ly/gmShS)是将行为赋给一个函数的过程。该函数本身并没有行为,你可以确切地为该函数指定要返回的值(即打桩返回值)。
// 传入由模拟框架创建的测试替身
AccessManager accessManager = new AccessManager(mockAuthorizationService);
// 如果返回值是null,则该用户ID没有访问权限。
when(mockAuthorizationService.lookupUser(USER_ID)).thenReturn(null);
交互测试 280
实际实现 281
在测试中使用与生产环境中相同的代码时,具有更高的保真度。
实际实现比隔离更好 281
如何决定何时使用实际实现 283
伪造(Faking) 285
为什么伪实现(Fakes)如此重要286
什么时候写伪实现 286
伪实现的保真度 287
伪实现应该被测试 288
如果没有伪实现怎么办 288
打桩 289
过度使用打桩的危险性 289
何时使用打桩合适 291
交互测试 292
状态测试优于交互测试 292
何时使用交互测试合适 293
交互测试的最佳实践 294
小结 296
本章要点 296
第14 章 较大型的测试 299
什么是较大型的测试 299
保真度 300
单元测试中的常见问题 301
为什么不要有较大型的测试 303
谷歌的较大型的测试 304
较大型的测试和时间 305
谷歌规模下的较大型的测试 306
大型测试的结构 308
被测系统(SUT) 309
测试数据 314
验证 315
较大型的测试的类型 316
一个或多个交互二进制文件的功能测试 316
浏览器和设备测试 317
性能、负载和压力测试 317
部署配置测试 318
探索性测试318
A/B 差异回归测试 319
用户验收测试(UAT) 321
探针和金丝雀分析 321
灾难恢复与混沌工程 322
用户评估 324
大型测试和开发者工作流 325
编写大型测试 325
运行大型测试 326
大型测试的所有权 329
小结 330
本章要点 330
第15 章 弃用 331
我们将有序迁移旧系统并最终将其移除的过程称之为弃用。
相比编程来说,弃用与软件工程学科契合更紧密,因为它需要考虑如何随着时间的推移来管理系统。
为什么弃用 332
为什么弃用很难 333
将弃用融入设计 335
弃用的类型 336
我们将这个连续的过程分为两个独立的领域:建议性和强制性。
建议性弃用336
强制性弃用337
强制弃用工作的最佳扩展方式是将迁移的专业知识局部化到一个专家团队中,通常是负责完全移除旧系统的团队。这个团队有动机帮助其他人从过时的系统中迁移,并且可以总结经验和开发一些工具,然后可以在整个组织中使用。
为了使强制性弃用能真正奏效,其时间表需要具有强制执行机制。没有这种魄力,客户团队很容易忽视弃用的工作,并转向其他更紧迫或更感兴趣的工作中。
弃用警告 338
这些警告会随着时间不断累积,很快就多到让系统用户不知所措,以至于他们完全忽略它们。在医学中,这种现象被称为“警告疲劳(https://oreil.ly/uUUef)”。
发出给用户的任何弃用警告都需要具有两个属性:可操作性和相关性。
弃用流程的管理 339
流程负责人340
里程碑 340
虽然最终目标可能是启动整个系统,但是增量里程碑有助于给团队一种进步感,并让他们不必等到全部开发流程结束后才感到为公司创造了价值。
弃用的工具341
小结 342
本章要点 343
第四部分 工具
第16 章 版本控制与分支管理 347
什么是版本控制 348
为什么版本控制很重要 349
集中式版本控制系统VS 分布式版本控制系统 351
真实来源 354
版本控制VS 依赖管理 356
依赖管理则更具挑战性,因为依赖管理主要关注由其他组织管理和控制的项目。它是在更高的层次上,而这也意味着你无法完全的控制。
分支管理 356
分支等同于在制品 357
开发分支 358
合并一个大型的开发分支意味着测试的一次执行将同时检查多个代码变更,当失败时,更难以区隔开。
我们只怎么对开发分支上瘾的
随着组织规模的扩大,开发分支的数量也在增长,更多的工作精力被放在协调分支合并策略上了。
所有合并和重新测试的工作都是纯粹的开销。替代方案需要一种不同的范式:基于主干的开发,严重依赖测试和CI,保持构建为绿色(指成功),并在运行时禁用未完成/未测试的特性。
最后,将会有一个唯一的版本用于发布:只有单一的真实来源,正式确定“应该包含什么和不应该包含什么”的“左移”方法。
发布分支 359
与开发分支相比,发布分支通常是无害的:问题不在于“拉分支”这项技术成本,而是分支的使用方法。开发分支和发布分支之间的主要区别在于预期的最终状态:开发分支期望合并回主干,甚至有可能被另一个团队进一步分支;而一个发布分支最终会被抛弃。
谷歌的版本控制 360
单一版本 361
场景:多个可用版本 362
“单一版本”规则 363
这个看似简单的源代码控制和分支管理规则的内涵:
决不能让开发人员选择“我应该依赖这个组件的哪个版本?”
(几乎)没有长周期的分支 363
发布分支呢365
单一代码仓(Monorepos) 365
细粒度存储库在规模(Git在有几百万的提交后经常有性能问题,并且当存储包括大型二进制工件时,克隆的速度会很慢)和存储(VCS元数据会累积,特别是在版本控制系统中有二进制工件时)方面更容易处理。
版本控制的未来 367
小结 369
本章要点 370
第17 章 代码搜索 371
Code Search 的用户界面 372
如何使用Code Search 373
在哪里 373
做什么 374
如何用 374
为什么 375
谁以及何时375
为什么需要一个单独的Web 工具 375
规模 375
无需设置即可浏览全局代码 376
专业化 377
与其他开发工具集成 377
开放API 379
规模对设计的影响 379
搜索查询延迟 380
索引延迟 381
谷歌的实现 382
搜索索引 382
排序 383
权衡 387
完整性:代码库的Head 387
完整性:所有结果VS 最相关的结果 387
完整性:Head VS 分支 VS 所有历史 VS 工作空间 388
表达性:Token VS 子串 VS 正则表达式 389
小结 390
本章要点 391
第18 章 构建工具与构建哲学 393
2015年,谷歌最终开源了Bazel(https://bazel.build)。
构建系统的目的 393
所有构建系统都有一个直接的目的:它们将工程师编写的源代码转换成可由机器读取的可执行二进制文件。
没有构建系统会发生什么 395
但是我只需要一个编译器 395
用shell 脚本来拯救 396
脚本负责按照正确的顺序构建。但很快就会遇到更多的问题:
- 冗长繁琐。
- 速度很慢。
- 依赖管理,环境变量设置的一致性。
现代构建系统 397
一切都是为了依赖 397
管理自己的代码相当简单,但是管理它的依赖项就困难得多。各种各样的依赖项:有时依赖一个制品(例如,“我需要计算机视觉库的最新版本来构建我的代码”)。
基于任务的构建系统 398
基于制品的构建系统 402
分布式构建408
时间,规模,权衡 412
处理模块和依赖 413
使用细粒度依赖与1:1:1 规则 413
最小化模块可见性 414
管理依赖 414
模块需要能够相互引用。将代码库分解成细粒度模块的缺点是,你需要管理这些模块之间的依赖关系(尽管工具可以帮助自动化这一点)。
单一版本规则。我们在内部代码库中对所有第三方依赖项执行严格的单一版本规则(https://oreil.ly/OFa9V)。
允许多个版本的最大问题是菱形依赖问题。假设目标A依赖于目标B和外部库的v1版本。如果目标B稍后被重构以添加对同一外部库的v2版本的依赖,则目标A将被破坏。
外部依赖的安全性和可靠性。通过要求在源代码中指定每个第三方制品的哈希,如果制品被篡改,则会导致构建失败,这样做开销很小,就可以完全避免安全性问题。
小结 419
本章要点 420
第19 章 Critique:谷歌的代码评审工具 421
代码评审工具的原则 421
代码评审流程 423
通知 424
第一步:创建一个变更 425
差异比较 425
分析结果 426
紧密的工具集成 428
第二步:请求评审 429
第三步和第四步:理解和评论变更 430
评论 430
了解变更状态 432
第五步:批准变更(评价变更) 434
第六步:提交变更 435
提交后:跟踪历史 436
小结 437
本章要点 438
第20 章 静态分析 439
有效静态分析的特点 440
可扩展性 440
易用性 440
让静态分析发挥作用的关键经验 441
关注开发者的体验 441
让静态分析成为核心开发者工作流的一部分 442
赋予用户贡献的权力 442
Tricorder: 谷歌的静态分析平台 443
集成的工具444
集成反馈渠道 445
建议的修复446
按项目定制447
预提交 448
编译器集成448
编辑和浏览代码时的分析 449
小结 450
本章要点 450
第21 章 依赖管理 451
为什么依赖管理这么难 453
冲突的需求和菱形依赖 453
菱形依赖问题:如果liba依赖libbase新版本,而libb依赖旧版本。
引入依赖 455
兼容性承诺455
引入时的注意事项 457
谷歌如何处理引入的依赖 459
不更新依赖并不一定是明智之举:外部的变更可能已经对CVEs(常见漏洞和暴露)进行了优化或添加了重要的新功能。
从理论上讲,依赖管理 460
没有变化(又名静态依赖模型) 461
语义化版本号 462
SemVer是当前非常普遍的做法,它使用三个十进制分隔的整数表示某些依赖(尤其是库)的版本号的普遍做法,如1.1.4.在最常用的约定中,三个组件编号分别表示主版本、次版本和补丁版本,这意味着更改主版本号表示对现有API的更改,可能会破坏现有的使用,而更改次版本号表示纯添加的功能,不应破坏现有的使用,变更补丁版本号则表示不影响API的实现细节和缺陷修复,这些缺陷修复被视为风险非常低。
绑定分发模式 463
Live at Head 464
Live at Head是明确试图从依赖管理问题中把时间和选择这两个因素拿掉:总是依赖于当前版本,并且不要以你的被依赖者难以适应的方式改变任何东西。对于必须进行变更的情况,只有在更新了下游依赖或提供自动化工具来执行更新之后,才应该进行这种中断。在开源生态系统中,这种责任的转变在最初很难激发:将所有下游客户的测试和变更的负担放在API提供者身上,是对API提供者责任的重大修订。
SemVer 的局限性465
SemVer 可能过度约束 466
SemVer 可能过度承诺 467
SemVer的补丁版本只是更改实现细节,是“安全的”变更,这与谷歌使用海勒姆定律的经验是格格不入的,“当用户数量足够时,你系统的每一个可观察的行为都会被某人所依赖”。
动机 468
最小版本选择 469
那么,SemVer 有效吗 470
资源无限的依赖管理 471
导出依赖 473
像“将某个库开源吧”这样无害的、充满希望的慈善行为也可能会成为组织的损失。首先,如果执行不当或维护不当,它最终可能会拖累组织的声誉。正如Apache社区所说,我们应该优先考虑“社区而不是代码”。如果你提供了优秀的代码,但是却是一个差劲的社区成员,那么仍然会对你的组织和更广泛的社区有害。其次,如果无法保持同步,一个善意的发布可能会成为工程效率的负担。假以时日,所有的分叉(forks)都会变得昂贵。
与外界共享代码,无论是作为开源代码发布还是作为闭源代码发布,都不是一个简单的慈善问题(在OSS情况下)或商业机会(在闭源情况下)。在不同的组织,不同的优先级中,你无法监控的依赖用户,最终都将对该代码施加某种形式的海勒姆定律惯性。
小结 477
本章要点 477
- 提供依赖并非免费的:“把它扔到墙外并忘记”的做法可能会损害你的声誉,并出现兼容性挑战。而稳定地支持它会限制你的选择,并使内部使用变得悲观。缺乏稳定的支持会失去商誉,或者使你面临重要的外部团体的风险,这些团体通过海勒姆定律依赖某些东西,并打乱你的“不稳定支持”计划。
第22 章 大规模变更 479
什么是大规模变更 480
谁来处理LSC 481
原子变更的障碍 483
技术限制 483
合并冲突 483
“没有闹鬼的墓地” 484
异构性 484
测试 485
代码评审 487
LSC 的基础设施 489
政策与文化489
代码库分析490
变更管理 491
测试 491
语言支持 491
LSC 流程 493
授权 493
变更创建 494
切片与提交494
清理 498
小结 498
本章要点 498
第23 章 持续集成 499
CI 的概念 501
快速反馈循环 501
金丝雀部署也会导致问题,特别是当同时部署多个版本时,部署之间的兼容性问题。这有时被称为版本偏移(version skew),它是指一种分布式系统的状态,其中包含多个不兼容版本的代码、数据和配置。
自动化 503
在Head上集成最新代码变更。Head是我们的单一代码仓中最新的版本代码,在其他工作流中,这也被称为master,mainline或trunk。相应地,在head集成代码也称为基于主干的开发。
持续测试 505
哪些测试应该在预提交时运行?我们的经验法则是:快速且可靠的。在提交后,你要能接受测试运行的时间更长,以及一些不稳定性,只要你有适当的机制来处理它。
通常将预提交测试仅限于那些发生变更的项目。
CI 的挑战 511
封闭测试 512
谷歌的CI 515
CI 案例研究:Google Takeout 518
但是我无力做CI 524
小结 525
本章要点 525
第24 章 持续交付 527
一个组织的速度是与其他参与者竞争、维护产品和服务质量或适应新规则的能力的一个 关键因素。
持续交付在谷歌的习语 528
自动化。减少或消除频繁发布的重复开销。
数据驱动决策。对健康指标进行A/B测试以确保质量。
分阶段发布。在 发布给所有人之前,向少数用户发布变更。
首先,似乎频繁地发布软件新版本有风险。随着用户群的增长,你可能会担心出现在测试中没有发现的 缺陷,愤怒的用户会强烈反对,并且在产品中可能有太多的新代码,以至于无法进行详尽的测试。但这正是CD能起作用的地方。理想情况下,一个版本和下一个版本之间的变更很少,因此解决问题非常简单。
在这一过程中,团队逐步 完善“在任何时间点可部署”的准备度,而不必真的执行发布。
速度是一项团队运动:如何将部署分解为可管理的单元 529
隔离评估变更:特性开关 530
如果语言允许的话,当关闭某个特性开关后,构建工具就可以在构建中删除该特性。例如,一个已经提供给客户的稳定特性可以在开发构建和发布构建中同时启用;而正在开发的特性可能只为开发阶段启用。
力求敏捷:建立发布火车 531
发布并不是凭空发生的,有可靠的发布可以使相关的因素更容易同步。
没有一个二进制是完美的 531
赶上你的发布期限 532
第二种是,如果你错过了发布火车,它便不会等你。在发布时间的某个时候,你必须坚定地拒绝开发者和他们的新功能。
质量与聚焦用户:只发布有用的功能 533
左移:更早地做出数据驱动的决策 534
一些更大的应用程序也会用A/B测试来测试它们的部署。这意味着发送两个版本的产品:一个是期望的更新版本,另一个对照的基线版本,它是一个“安慰剂”(旧版本会再次发布)。
改变团队文化:建立发布规则 535
如果能做到频繁发布,那么某个特性错过一次发布所带来的痛苦,要比让一次发布中所有特性都延迟带来的痛苦小很多,更别说是当一个还没有完全准备好的特性被匆忙打包,让用户所感受到痛苦了。
小结 536
本章要点 537
第25 章 计算即服务 539
驯服计算环境 540
将琐事自动化 540
容器化与多租户 542
总结 545
为托管计算编写软件 545
为失效设计架构 545
批处理VS 服务 547
管理状态 549
连接到服务550
对于可变的请求来说,处理重复的请求是很棘手的。你需要保证的是幂等性。即发出两次请求的结果与发出一次请求的结果相同。一个有助于处理幂等性的有用工具是 客户端分配的标识符:如果你正在创建某种东西(例如,将比萨饼送到特定地址的订单),客户端会为订单分配一些标识符;如果已经记录了具有该标识符的订单,服务器会假定这是一个重复请求并报告成功(可能同时验证订单的参数是否匹配)。
一次性代码551
CaaS 随时间和规模的演化 552
抽象容器 552
一个服务统御余众 555
提交的配置557
选择计算服务 558
集中化与定制化 559
抽象层次:Serverless 561
公有云VS 私有云 565
小结 566
本章要点 567
后记 569