Google 软件工程
1. 什么是软件工程
软件工程就是随着时间而不断集成的编程。
海勒姆定律(Hyrum’s Law):当一个 API 有足够多的用户时,在约定中你所承诺的已不重要。所有在你系统里面被观察到的行为,都会被一些用户所依赖。
当我们想一想以 “现在可工作” 和 “一直可工作” 这两种不同的思想编写代码的区别,我们就会理出一些清晰的关系。
如果将代码视为具有高度可变生存期需求的工件,我们可以对编程风格进行分类:
- 那些依赖其他脆弱且为发布特性的代码可能被描述为 “黑客化” 或 “巧妙”
- 而遵循最佳实践并为未来规划的代码,更可能被描述为 “干净的” 和 “可维护的”
两者都有其目的,但你选择哪一个关键取决于所讨论代码的预期寿命。
我们习惯于说,如果 “巧妙” 是一种赞扬,那就是编程,如果 “巧妙” 是一种指责,那就是软件工程。
对于大多数项目,在足够长的时间内,项目的所有内容都有可能会变化。
大多数项目都有更多的潜在技术转移风险。
当你能够安全的做出任何变更,并且在代码库的生命周期内都可以这样做,那你组织的代码库是具有可持续性的。
如果变更的成本过高,它将可能被推迟了。
如果成本随时间呈超线性增长,那么运营显然是不可扩展的。
最终时间会掌控一切,并且会出现一些意想不到的事情,使你必须改变。
只要通过一点实践,可以很容易的发现那些扩展属性不佳的政策。
最常见的情况是,我们可以想一下,如果组织中的工程师人数扩大 10 倍,或是 100 倍,工程师的工作状态会是怎样的。
当我们的组织规模是现在的 10 倍时,我们的工程师的产出也会增加 10 倍?我们的工程师必须完成的工作量随着组织规模的增长而增长吗?
这项工作是否随着代码库的增长而扩大?如果这两个都是真的,我们是否有机制来自动化或优化一些工作?如果没有,我们就有规模化方面的问题。
2012 年,我们尝试使用 “减少搅动” 的规则来阻止这种情况,即基础设施团队必须自己完成将内部用户迁移到新版本的工作,或者以向后兼容的方式进行适当的更新。
我们称之为 “搅动规则”(churn rule),这一政策的可扩展性很好。
那些使用了依赖的项目,不再仅仅为了跟上最新版本而投入过多的维护精力。
我们还学习到,有一个特定的专家组来执行大规模变更,要比要求每个团队自己来做这种变更会更具有可扩展性。
因为专家们花一些时间,并深入的了解整个问题,然后将该专业知识应用到每个子问题上。
如果我们强迫所有使用该依赖的团队响应这种版本升级,就意味着每个受影响的团队在紧急解决完这个升级问题之后,立刻又会丢掉那些对于他们来说没有什么用的特定知识。
所以,专家组政策更具可扩展性。
我们最喜欢的内部政策之一是,让负责基础设置的团队能够安全的进行基础设施的变更。
如果一个产品由于基础设施的变更而出现中断或其他问题,但是在我们的持续集成(CI)系统中的自动化测试用例并没有发现这个问题,那么就不应该由负责基础设施的团队承担责任。
更通俗的说,那就是,如果你喜欢它,你应该对它进行 CI 测试,我们称之为 “碧昂丝规则”。
如果没有这一点,基础设施团队的工程师可能需要跟踪每个受影响的团队,并询问他们如何才能进行他们的测试。
当有 100 个工程师的时候,我们可以做到,如果超过这个数,我们肯定就负担不起了。
我们发现,专业知识和共享的交流论坛在组织规模化上提供了巨大的价值。
当工程师在共享论坛上讨论和回答问题时,知识往往会传播开来,新专家不断增加。
如果有 100 个工程师在写 Java,并且一个友好且乐于助人的 Java 专家愿意回答问题,那么很快就会使这 100 个工程师写出更好的 Java 代码。
知识是具有病毒性的,而且专家是载体,对于扫除工程师常见的绊脚石的价值有很多值得说的。
基础设施的变更频率越高,变更就越容易做。
我们发现的一个广泛的事实是,在开发人员工作流的早期发现问题,通常会降低成本。
在谷歌内部,人们强烈反感 “我说了算”。重要的是,任何话题都要有一个决策者,当决策错误时需要有明确的升级路径,但目标是要 “达成共识”,而不是 “一致同意”。
我们希望能看到一些这样的例子 “我不同意你的标准/评估,但我了解你是如何得出这个结论的”。
所有这一切的内在含义是,每件事都需要有一个理由,“就是这样,没有理由”、“我说了算”、“因为其他人都是这样做的” 都是错误决定潜伏之处。
只要这样做事有效的,当我们面对两个工程选项的总成本而需要做出决策时,就应该给出相应的解释。
在像软件工程这样的高创意、高收益的领域,财务成本通常不是限制因素 —— “人” 才是。
让工程师保持快了、专注和投入所带来的效率很容易影响其他因素,原因很简单,因为专注和生产力是如此多变,想象一下 10%~20% 的差异。
谷歌是一种数据驱动的文化,事实上,这是一种简化。
即使没有数据,也可能有证据、先例 和 论证。
做一个好的工程决策就是衡量所有可用的输入,并就权衡做出明智的决定。
Jevons 悖论:随着资源使用效率的提高,对资源的消耗可能会增加。
分布式构建系统所节省的成本,远远超过了系统本身开发和维护的成本。
为狭窄的问题空间定制的解决方案,可能会胜过需要处理所有可能性的通用解决方案。
无论我们讨论的是 微服务、内存缓存、压缩程序 还是软件生态系统中的任何其他内容,通常来说,通过 分叉/重新实现 公共代码,并为你的窄域定制它,可以更轻松的添加新功能,或更确定的进行优化。
也许更重要的是,分叉会使得你与底层依赖的变更隔离开来,由另一个团队或第三方提供商所带来的这些变更将不会对你有任何限制。
你可以自己控制如何以及何时对变更做出反应。
另一方面,如果每个开发人员都讲软件项目中使用的所有东西分叉,而不是重用现有的东西,那么除了可持续性之外,可扩展性也受到影响。
如果你的项目寿命短,分叉的风险就小。
我们坚信数据可以支撑决策,但我们也要认识到数据会随着时间的推移而变化,新的数据可能会出现。
这意味着,从本质上讲,在所讨论的系统的生命周期内,需要不时的重新审视决策。
对于长期的项目,在做出初步决策后,能够改变方向通常是至关重要的。
而且重要的是,这意味着决策者需要有承认错误的权利,与通常的本能相反,承认错误的领导人得到更多的尊重,而不是更少。
以证据为导向,但也要意识到,无法度量的东西可能仍然有价值。
如果你是一个领导者,那是你应该做的:运用判断力做决断很重要。
我们的观点并不是说 软件工程 更优越,只是它们(跟编程相比)代表了两个不同的问题领域,具有不同的约束、价值和最佳实践。
相反,指出这种差异的价值在于认识到有些工具在一个领域很好,但在另一个领域却不然。
我们认为区分相关但不同的术语 “编程” 和 “软件工程” 是很重要的,这种区别很大程度上源于时间的推移对代码的管理、时间对规模的影响以及面对这些想法的决策。
编程是产生代码的直接行为,软件工程是一组必要的政策、实践和工具,使得代码在跨团队协作和使用时更有效。
谷歌一直在努力建立一个可持续的代码库和文化。
- “软件工程” 在维度上与 “编程” 不同:编程是关于代码的产生。软件工程扩展了这一点,包括在代码的生命周期内对其进行维护。
- 短生命周期代码和长生命周期代码的生命周期之间至少差 十万倍,假设相同的最佳实践在这一范围的两端具有普适性是愚蠢的。
- 当在代码的预期寿命中,我们能够对依赖关系、技术或产品需求的变化做出响应时,软件是可持续的。我们可以选择不改变,但我们需要具备响应变化的能力。
- 就人力投入而言,组织必须重复执行的每项任务,都应具有可扩展性(线性的或更好)。政策是使流程具有可扩展性的极好工具。
- 数据驱动是一个好的开始,但实际上,大多数决策都是基于数据、假设、先例 和论据 的混合。当客观数据占了这些输入的大部分时,这是最好的,但很少能全部都是客观数据。
- 随着时间的推移,数据驱动意味着当数据改变时(或当假设被消除时),需要改变方向。错误或修订计划是不可避免的。
2. 如何更好的参与团队合作
我认为一个高效、成功的软件工程师需要把主要时间和精力花在编写好代码上,而不是整天都在与人沟通问题。
软件开发是一个需要协同作战的工作,团队是软件开发工作的基本组织。
要想在工程团队或任何其他创造性的合作中取得成功,团队中的每个人都需基于谦逊、尊重和信任的核心原则重新调整自己的行为。
人们害怕别人看到并评判他们正在进行的工作。
从某种意义上说,不安全感是人性的一部分,没有人喜欢被批评,尤其是害怕自己正在做的事情被别人批评。
天才神话是一种趋势,人类喜欢把团队的成功归于一个人或某位领导者。
许多工程师的内心深处都希望自己被看做是天才。
天才仍然会犯错误,拥有杰出的创意和精湛的编程技巧并不能保证你的软件会大受欢迎。
天才绝不是混蛋的借口,不论天才与否,社交能力差的人往往都是糟糕的队友。
特别是在谷歌这样的公司,决定你职业生涯的其实是与人协同的工作能力。
当我知道别人正在审视我之前所做的工作时,会感到强烈的不安全感,就好像他们马上就会来批判我是个白痴似的。
这是大部分程序员都会有的感觉,自然的反应是隐藏起来,工作,工作,工作,然后完善,完善,完善,确信没人会看到他们的错误为止,等到工作完成时,才有机会让自己的杰作公之于众。
躲起来,直到代码变得完美为止。
另一个隐藏工作的动机是担心别的程序员会窃取自己的想法并执行,保守秘密,就能把控这个想法在自己手中。
软件开发工作是需要高度专注和独处时间的智力工作,但不代表把所有时间都花在独自工作上。
软件开发工作的协同和评审非常重要,可以避免个人独自工作所带来的不必要的麻烦,也可避免个人独自工作时自以为是的潜力增长假象。
如果你把自己所谓的伟大想法隐藏在内心深处,并在实施之前拒绝与任何人沟通或展示,那就是在冒险。
很可能你在早期就犯了一些基本的设计错误。
这就是为什么人们在跳入深水之前先把脚浸在水中的原因:你需要确定你正在做正确的事情,你正在以正确的方式做以前从未做过的事情。
早期失误的概率很高,越早征求反馈,就越能降低这种风险。
记住这句经过无数人验证的至理名言:早失败,快失败,常失败。
早期的分享不仅仅是防止个人失误,而且还可以让你的想法得到验证,更重要的是还可以提升项目的巴士系数。
巴士系数,指多少关键开发者被巴士撞了会让项目停摆。
如果你是唯一一个了解原型代码工作原理的人,你可能会享受到很好的工作保障。
但是如果你被公交车撞了,这个项目就完蛋了。
如果有同事和你一起工作,你的巴士系数就翻了一番。
如果你有一个小团队来共同设计和开发原型,巴士系数会变得更高,当有团队成员离开时,项目也不会停摆。
在软件开发的过程中,我们必须确保每个责任领域除了有一个主要和次要的责任人以外,至少有良好的文档,这有助于项目成功和提升巴士系数。
希望大多数工程师都能认识到,与其成为一个失败项目的关键部分,不如成为一个成功项目的一部分。
除了巴士系数,还有总的工作进展问题。
工程师有时候总喜欢单干,但忽略了一个问题,单干不仅会增加工作强度,其实比想象中的速度慢很多。
与他人一起工作可以更快的获取集体智慧。
一般来说,程序员在紧凑的反馈中效率是最高的:写一个新函数,编译;添加一个测试用例,编译;重构一些代码,编译。
这样,在代码生成后,我们就能够尽快发现并修复错误。
这可以帮助我们时刻保证编写的代码是高质量的,而且确保软件正在向正确的方向一点一滴的有所进展。
目前 DevOps 理念明确指出了这些帮助提升技术生产力的目标,尽早获得反馈 ,尽早进行测试,尽早考虑安全和生产环境。
我们发现问题的时间越早,修复问题的成本就越低。
足够多的眼睛就可以让所有问题浮现,足够多的眼睛可以确保项目一直在正确的轨道上开展。
一直隐藏在洞穴默默工作的人们,当他们清醒过来时才发现,尽管他们最初的设想已经完成了,但是世界已经改变,他们正在做的事情已经变得不再重要了。
你可以利用大量的不受干扰的时间聚焦做某一件事情,但如果你用这些时间来做错误的事情,那就是在浪费时间。
我们还是认为工程师需要拿出持续不间断的时间专注于编写代码,但是同时也需要他们与团队进行连接。
如何找到正确的平衡点是一门艺术。
独自工作比他人一起工作本身更具有危险性。
即使你害怕别人偷走你的想法或担心自己不够聪明,你更应该担心的是可能会浪费大量的时间在错误的事情上。
在编程领域,孤独的工匠是极其稀有的,即使他们确实存在,他们也不会在真空环境中实现超人的成就,他们之所以可以改变世界,一定是灵感火花加上英雄团队努力的结果。
任何可以改变世界或让用户为之兴奋的软件都不是通过个人偷偷躲起来工作而完成的。
你需要跟其他人一起工作,分享愿景,安排工作分工,向别人学习,组建一支优秀的团队。
高效运作的团队是成功的关键。你应该为此而努力追求。
3. 知识共享
一个组织往往比某个网上的人更了解组织自身的问题,而且,它也应该能够回答它自己的大部分问题。
为了达到这一点,你既需要知道哪些专家能够解决这些问题,也需要知道如何将他们的知识分发出去的机制。
在组织内共享专业知识并非易事,没有强大的学习文化,就会遇到各种各样的挑战。
如果专家总是自己做每件事,不花时间通过指导或知识文档来培养新的专家,问题会变得很严重。
这种情况下,知识和责任将持续积累在那些已经拥有专业技能的人身上,而新的团队成员或新手只能靠自己一点一滴的积累来提升。
软件工程的核心是人,代码是人的重要产出,但只是构建产品过程中的一小部分。
代码不会从无到有,人的专业技能也不能凭空就拥有。
每个人在成为专家之前都是新手,一个组织的成功取决于人员的成长和投资。
文档化的知识可以更好的在团队甚至整个组织内部继承和流动。
但是,尽管书面文档比面对面交流的知识传播规模更大,但也要考虑一些权衡利弊。
比如书面文档可能更通用,但不适合个别学习者的场景,而且还有维护成本,需随时保持信息的相关性和动态更新。
部落知识就是存在于单个团队成员脑袋中但未文档化的知识。
如果我们可以面对面访谈这些专家,记录并维护这些知识,就可以让任何查阅到这些文档的人获取这些知识。
书面知识有规模化优势,可以让更多人获得这些知识,但通过访谈目标人群获取他们脑袋中的知识也非常有必要。
因为专家有能力综合归纳他们脑中广袤的知识体系,他们可以评估哪些信息适用于哪种使用场景,确定文档之间的关联性,并知道在哪里可以找到这些知识。
如果他们不知道在哪里找到答案,但至少知道谁曾经做过类似的事情。
部落知识与文档知识相辅相成,即使是拥有完美文档的完美专家团队,也需要与其他团队进行沟通、协调,并随时调整学习策略。
组织内部的培训需聚焦员工的学习和成长,以建立稳定的专家队伍。
要学习,首先你必须承认有些事情不明白。我们应该欢迎这样的诚实,而不是惩罚。
谷歌做的相当好,但有时也会有一些工程师,不愿意承认他们不理解某些事情。
群体交互模式:
- 不要假装惊讶:会让团队成员害怕承认自己缺乏知识
- 不要故弄玄虚:要提供精确的答案,不要卖弄学问
- 不要乱出主意:要读谈话的内容负责
- 不要带有偏见
作为领导者,要树立这种风范:不要错误的把 “年长” 和 “无所不知” 等同起来。
事实上,你知道的越多,就越是知道更多自己不知道的事情。
公开提问或承认自己在哪方面的知识是空白的,是非常好的行为,这样做对团队中的其他成员也会起到示范作用,让他们觉得这样做没有问题。
回答问题时,耐心和善意会培养出一种让人们寻求帮助时感觉安全的环境。
学习不仅仅是理解新事物,它还包括理解现有设计和实现背后的决策缘由。
切斯特森围栏(Chesterson's Fence)原则:在去除或改变某事物之前,首先了解它为什么会在那里。
如果你不知道它的用途,我肯定不会让你把它拆了。你去查查它的用途,之后我可能会允许你拆掉它。
谷歌历来有举办内外部技术讲座与课程的文化。
确保文档具有反馈机制,如果读者没有一个简单直接的方式来指出文档已经过时,或不准确,他们很可能会因为麻烦而不愿意告诉任何人。
如果这样的话,下一个新手还会遇到同样的问题。
如果大家觉得有人会注意到并考虑他们的建议,他们就会更愿意做更新。
在谷歌,工程师们可以直接从文档本身提交一个文档缺陷。
鼓励工程师记录他们的工作可能是困难的,编写文档需要花费时间和精力,而这些工作所带来的好处不是立竿见影的,只是为其他人提供了方便。
但对于这个组织来说是件好事,因为多数人可以从少数人的时间投资中获益。
但如果没有好的激励,这种鼓励行为很难继续。
规范的信息源是集中的,全公司范围的信息库,它提供了一种标准化的知识传播方法。
谷歌会在全公司范围内给所有工程师发送时事资讯,包括工程新闻(EngNews)、隐私/安全新闻(Ownd)和谷歌热点。
4. 平等工程
将软件工程组织本身打造成符合我们产品目标人群的结构。
仅有计算机科学学位的程序员团队,不足以实现包容且平等的工程。
一个杰出的工程师的标志之一,就是能够理解产品如何使不同群体的人获利或者受损。工程师应该有技术才能,但他们也应该有敏锐的洞察力,知道何时该做什么,何时不该做什么。这种洞察力是一种能够识别和拒绝可能导致不良结果的特性或产品的能力。这是一个崇高而又困难的目标,因为在成为高绩效工程师的道路上充斥着大量的利己主义思想。
在成为一名杰出的工程师的过程中,必须了解在不造成伤害的情况下行使权利所需的内在责任,这一点至关重要。
无论是开发软件,还是软件组织的发展,都需要团队的共同努力。随着软件组织的扩大,它必须对其用户群做出响应,并进行充分的设计。在当今互联的计算世界中,用户群涉及本地和世界各地的每个人。我们必须付出更多的努力,使设计软件的开发团队和他们所生产的产品,都能反映出一种价值观,即很多个多样且包容的用户群。而且如果一个工程组织想要扩大规模,它不能忽视弱势群体;这些代表弱势群体的工程师不仅增强了组织本身,他们还为软件的设计和实现提供了独特且必要的视角,这对整个世界都是真正有用的。
5. 团队领导的艺术
经理(Manager)是管人的,而技术主管(Tech Lead)则是管技术的。
没有船长的船只不过是一个漂浮的候船室,除非有人抓住方向舵并启动引擎,否则它只会随波逐流。一个软件就像那艘船,如果没有人驾驶它,你会被一群工程师浪费宝贵的时间,只能坐等奇迹发生(或者更糟的是,仍然在编写你不需要的代码)。
工程经理负责团队中每个人的绩效,生产力和幸福感,包括他们的技术主管,同时确保他们负责的产品满足业务需求。
一个团队的技术主管(TL)经常向该团队的经理汇报,负责产品的技术方面,包括技术决策和选择,架构,优先级,速度和项目管理。
在小型和新生的团队中,工程经理需要强大的技术技能集,默认情况下通常有一个 TLM:一个能够同时兼顾团队人员和技术需求的人。
在谷歌,大型,成熟的团队通常会有一堆搭档领导者,一个 TL 和一个工程经理作为合作伙伴一起工作。理论上说,要同时做好这两项工作而不完全筋疲力尽是非常困难的,所以最好有两位专家专注于各自的角色。
人们普遍认为,你可以给那些向你汇报工作的人安排工作,但当你需要让组织之外的人去做你认为需要做的事情时,情况就不同了。这种 “非职权影响力” 是你可以培养的最强大的领导特质之一。
如果你的产品想要有所进展(无论向哪个方向发展),那么,无论是否被正式任命,总要有个人站出来把握它的方向。假如你恰好是那种有动力且没有耐心的类型,很可能这个人就是是你。你可能会发现,自己已经卷入了帮助团队解决冲突,做出决策和协调人员的工作中。这种情况经常发生,而且经常是在无意中发生的。也许你从未想过成为一个 “领导者”,但不管怎样,它还是发生了。有些人将这种情况称为 “经理人炎症”。
彼得原理:在一个等级制度中,每个员工趋向于上升到他所不能胜任的地位。
大多数人都可能遇到过一个不能胜任自己工作的经理,或者是非常不善于管理人的经理。我们知道甚至有些人只在差劲的经理手下工作过。
管理者们似乎染上了一种疾病,那就是,他会忘记他原来的管理者对他做过的所有糟糕的事情,而突然开始用同样的方法来 “管理” 自己的下属。如果不及时治疗,这种疾病会杀死整个团队。
当我第一次称为谷歌的经理时,我从当时的工程总监 Steve Vinter 那里得到了最好的建议。他说,最重要的是,要克制住管理的冲动。
新上任的管理者最强烈的愿望之一就是积极的 “管理” 他们的员工,因为这是管理者应该做的,对吧?但这通常会产生严重的后果。
治疗 “管理” 疾病的方法是,充分运用 “仆人式领导”,这是一种很好的说法,作为一个领导者,你能做到最重要的事情就是为你的团队服务,就像一个管家照顾一个家庭的健康和幸福一样。作为一个仆人式的领导者,你应该努力营造一种 谦虚 和 信任 的氛围。这可能意味着消除团队成员自己无法消除的官僚障碍,帮助团队达成共识,甚至在团队工作到很晚时为他们买晚餐。仆人式领导者为他们的团队清理障碍,铺平道路,并在必要时提出建议,除此之外还不怕做一些脏活,累活。仆人式领导所做的唯一管理就是 “管理团队的技术和社交健康”,尽管纯粹关注团队的技术健康可能是诱人的,但团队的社交健康同样重要(但往往你难以管理)。
传统的经理担心如何完成任务,而伟大的经理关心的是要完成什么任务(并相信他们的团队能想出如何完成任务)。
我把 Jerry 当成人一样对待,他总是把工作做完,我从来不必担心他是否在办公桌旁,因为他不需要保姆来监督他完成工作。如果你的员工对他们的工作如此不感兴趣,以至于他们实际上需要传统的经理保姆来说服他们工作,那才是你真正的问题。
扬善于公庭,规过于私室。
你应该努力雇佣比你更聪明,可以取代你的人。这可能很困难,因为这些人经常会挑战你(另外,当你犯错时,他们可能还会怼你)。当然,这些人也会让你惊讶,干出很多伟大的事情。他们将能更好的驱动自己,有些人也会渴望领导团队。你不应该把这看作是企图篡夺你的权力。相反,你应该把它看成是一个机会,让你有精力去领导一个更大的团队,寻找新的机会,甚至可以去度个假,而不用担心每天都要检查团队是否完成了工作。这也是一个学习和成长的好机会,当周围都是比你更聪明的人的时候,你更容易扩展自己的专业知识。
人的方面是编写软件最具挑战性的部分,但与人打交道最困难的部分是处理不符合期望的人。有时,人们会因为时间不够长或不够努力而不符合期望,但最困难的情况是,无论它们工作多久或多么努力,都没有能力完成工作。
事实上,团队非常清楚谁是低绩效的员工,因为团队不得不拖着他们前行。忽视低绩效员工,不仅阻碍新的高绩效员工加入团队,还会导致现有高绩效员工的离职。你最终会得到一支表现欠佳的团队,因为这些人根本不会自愿离开团队。最后,你把低绩效员工留在团队里,也不会给他们带来任何益处;在你的团队中表现不好的人,实际上在其他团队很可能会表现不错。
尽快与低绩效员工沟通的好处是,你可以很好的帮助他们。如果你立即与一个低绩效员工沟通,你经常会发现他们只需要一些鼓励或指导就可以进入更高的生产力状态。如果你等太久才去与一个低绩效员工沟通,他们与团队的关系就会变得很糟糕,你也会非常沮丧。到这时候,你可能已经无法帮助他们了。
如何有效的指导低绩效员工呢?最好的类比是,想象你正在帮助一个一瘸一拐的人再次学会走路,然后慢跑,然后和其他人一起跑步。它几乎总是需要一些临时的微观管理,当然仍然需要谦虚,尊重和信任的态度,尤其是尊重。设定一个具体的时间框架(比如说两个月),以及你希望他们在这段时间内实现的某些具体的目标。把这些目标定得小,渐进且可衡量,这样就有机会取得很多小的成功。每周与团队成员会面,检查进展情况,并确保围绕每个即将到来的里程碑,设定明确的期望,以便很容易衡量成功与否。
一个经理对他们的团队有两个主要的关注领域:社交和技术。在谷歌,经理们在技术方面更强是很常见的,因为大多数经理都是从技术岗位晋升的(他们的主要工作目标是解决技术问题),他们往往会忽视人的问题。
如果你因为不信任而对团队进行微观管理,那其实是你招聘的失败,招聘一些连你自己都不信任的人。
在任何团队中,自尊心太强的人都是很难处理的,尤其是团队的领导者。相反,你应该努力培养一种强大的团队自我意识和认同感。
如果你能够很肯定那些在战壕里工作的人比你更了解他们工作的细节,那么这意味着,你会认同,你可能是推动团队达成共识并帮助确定方向的人,但如何实现目标的具体细节,最好由生产产品的人来决定。这不仅使他们有更强的主人翁意识,而且使他们对产品的成功(或失败)有更强的责任感。
大多数刚开始担任领导职务的人都觉得自己肩负着巨大的责任,要做好每一件事,了解每一件事,掌握所有的答案。我们可以向你保证,你不可能把每件事都做好,也不可能知道所有的答案,如果你表现得像这样,你很快就会失去团队的尊重。
人们对那些在犯错时道歉的领导人有着极大的尊重,而且与普遍的看法相反,道歉并不会让你变得脆弱。事实上,当你道歉时,人们通常会尊重你,因为道歉告诉人们你头脑冷静,善于评估情况,并且谦虚。
当你领导的团队更大时,调节你的反应和保持冷静,就变得更加重要,因为你的团队会(有意或无意的)从你身上寻找行动和应对周围发生的任何事情的线索。
形象一点来说,将公司的组织结构图视为一个齿轮链,每个贡献者都是一个一端只有几颗齿的小齿轮,而他们上面的每个经理都是另一个齿轮,最后 CEO 是一个有几百颗齿的最大齿轮。这意味着,每当一个 “经理齿轮”(可能有几十个齿)转一圈,“个人齿轮” 就转两三圈。**而 CEO 的一个小动作就能让倒霉的员工,在六七个齿轮的链条末端,疯狂的旋转!**你越是处在链条的前端,就可以越快的让你下面的齿轮旋转,无论你是否有意为之。
领导者总是在聚光灯下。这意味着如果你处于一个公开的领导地位,你总是被监视着,不仅是在主持会议或演讲时,甚至只是坐在办公桌前回复电子邮件时。你的同僚在观察你的肢体语言,你对闲聊的反应以及你吃午饭时的举动。他们读到的是自信还是恐惧?作为一个领导者,你的工作是激励,但激励是一项 7✖️24 小时全天候的工作。你对任何事情的明显态度,无论多么琐碎,都会在不知不觉中被注意到,并传染给你的团队。
另外一个禅宗管理技巧:提问。当一个团队成员向你征求意见时,这通常是相当令人兴奋的,因为你终于有机会解决一些问题了。这正是你在担任领导职位前多年所做的,所以你通常会跳进解决方案模式,但这是你最不该去的地方。寻求建议的人通常不想让你解决他们的问题,而是想让你帮助他们解决问题,最简单方法就是问这个人问题。你可以表现出一些谦虚,尊重和信任,试着通过提炼和探索问题来帮助人们自己解决问题。这通常会引导员工找到答案,它也会成为员工自己的答案。不管你是否有答案,使用这种技巧几乎总是会给员工留下你有答案的印象。
在许多情况下,认识正确的人比知道正确的答案更有价值。你并不需要知道所有的答案,但你通常可以帮助找到消除障碍的人。
如果你想让团队朝一个方向快速前进,你需要确保每个团队成员都理解并同意这个方向是什么。如果你有明确的目标,你需要设定明确的优先级,并在时机到来时帮助团队,令其知道应该如何做出权衡。
设定一个明确的目标,并让你的团队朝着同一个方向推动产品,最简单的方法是为团队创建一个简洁的使命声明。在你帮助团队确定了方向和目标之后,你可以退一步,给团队更多的自主权,定期检查以确保每个人都在正确的轨道上。这不仅可以使你腾出时间来处理其他领导任务,还可以极大的提高团队的效率。
我不会对你撒谎,但是当我不能告诉你一些事情,或者我真的不知道的时候,我会告诉你。如果一个团队成员找你谈一些你不能分享的事情,你可以告诉他们你知道答案,但是不能随便说。更常见的是当一个团队成员问你一些你不知道答案的问题时,你可以告诉那个人你不知道。
当你提供直接的反馈或批评时,你的表达方式是确保信息被听到而不被曲解的关键所在。如果你让接受者处于守势,他们不会考虑如何改变,而是考虑如何与你争辩,向你表明你错了。
作为一个领导者,有一种方法可以让你的团队长期保持高效(以及团队稳定),那就是花些时间衡量他们的幸福感。追踪团队幸福感的一个简单方法是,在每次一对一沟通结束时问你的团队成员 “你需要什么吗”,这个简单的问题是一个很好的总结方式,可以确保每个团队成员都拥有他们所需的东西。如果你每次一对一的时候都问这个问题,你会发现最终团队会记住这一点,有时甚至会给你一份清单,上面列出了让每个人的工作都更好的事情。
追踪团队成员幸福感的一个重要部分是追踪他们的职业发展。
在未来五年里,通常每个人都想做几件事:升职,学习新东西,从事一些重要的工作,以及与聪明人一起共事。不管他们是否用语言表达,大多数人都在思考这个问题。如果你想成为一个有效的领导者,就应该考虑如何帮助实现所有这些事情,并让团队知道你正在思考这个问题。
作为领导者,你的工作是确定谁需要什么,然后给他们,你的团队需要不同程度的激励和方向。为了让所有团队成员都得到他们需要的东西,你需要激励那些墨守成规的人,并为那些分心或不确定该做什么的人提供更强有力的方向。当然,也有些人 “飘忽不定”,既需要激励,也需要方向。因此,有了这种激励和方向的结合,就能让团队快乐并富有成效。
Dan Pink 在《驱动力》一书中解释说,让人们成为最快乐,最有成效的人的方法不是外在的激励他们(例如,向他们仍一堆现金),相反,你需要努力增加他们的内在动机。
你可以通过给人们三样东西来增加内在动机:自主,专精 和 目的。
当一个人有能力独立行动,而不需要别人对他进行微观管理时,他就拥有自主权。有了自主权的员工,你可能会给他们提供需要开发产品的自主方向,但让他们自己决定如何实现。这不仅有助于激励他们,因为他们与产品的关系更密切,还因为这让他们对产品有更强的主人翁精神。他们对产品成功越有自主权,他们就越希望看到产品成功。
专精意味着需要给别人机会来提升现有技能并学习新技能。提供足够的机会让员工专精,不仅有助于激励他们,还能让他们随着时间推移变得更好,从而形成更强的团队。
6. 大规模团队领导力
你会为失去这些细节而悲伤,你开始意识到你以前的工程专业知识与你的工作变得越来越不相关。相反,这时你的效率比以往任何时候都更依赖于你的技术直觉和激励工程师的能力。
三个 “总是” 的领导力:
- 总是在做决策(Always Be Deciding)
- 总是不在场(Always Be Leaving)
- 总是在扩展(Always Be Scaling)
管理一个团队的团队,意味着在更高层次做出更多的决策。你的工作更多的是关于高层战略,而不是任何解决任何具体的工程任务。在这个层次上,你所做的大多数决定都是关于找到正确的折中方案。
作为一个领导者,你需要决定你的团队每周应该做什么。在最更层面上,不管是一个团队还是更大的组织的领导者,你的工作是引导人们去解决那些困难,模糊不清的问题。所谓模糊不清,是指这个问题没有明显的解决方案,甚至可能无法解决。无论哪种方式,都需要对问题进行探索,引导,并努力使之处于可控状态(希望如此)。如果写代码类似于砍树,那么作为领导者,你的工作就是 “透过树木见森林”,找到一条穿过森林的可行路径,引导工程师们前往那些重要的树木。这一过程有三个主要步骤:首先,你需要识别盲点;其次,你需要识别权衡;然后,你需要决策并迭代解决方案。
你可以用这些信息为当前这个月作出最佳决策。下个月,你可能需要再次重新评估和重新平衡这些利弊,这是一个迭代过程。这就是我们所说的 “总是在决策” 的意思。
这里有一个风险。如果你没有将巩固你做流程设计成适合迭代进行的,那么,你的团队可能会陷入寻找完美解决方案的陷阱,这可能导致 “分析瘫痪”。你需要让你的团队适应迭代。
你的工作不仅仅是解决一个模糊不清的问题,同时更是让你的组织在没有你在场的情况下自行去解决它。如果你能做到这点,它将使你有更多的精力去面对一个新的问题(或新的组织),从而留下了一条让团队自组织自管理的成功之路。当然,这里的反模式是你将自己设置为单点故障的一种情况。
作为试金石,你团队正在处理一个难题,目前进展良好。现在想象一下,你,领导者,突然消失了。你的团队会继续前进吗?它会继续取得成功吗?
想想你上次休假,至少花了一个星期时间。你是否一直在检查你的工作邮件?问问你自己为什么。如果你安心休假,天会塌下来吗?如果是这样,你很有可能让自己成为了一个单点故障。你需要解决这个问题。
你的使命是,建立一个 “自驱” 的团队。成为一个成功的领导者意味着要去建立一个能够自己解决困难问题的组织。这个组织需要一组强大的领导者,健康的工程流程,以及一种积极的,自我延伸的文化。
构建这种自信的群体有三个主要部分:划分问题空间,授权子问题,根据需要进行迭代。
具有挑战性的问题通常由困难的子问题组成。如果你领导一个团队的团队,一个明显的选择就是让每一个团队负责其中的一个子问题。然而,风险在于子问题会随着时间而改变,而僵化的团队边界将无法注意到货适应这一事实。如果你有能力,请考虑一个更松散的组织结构,其中子团队可以改变规模,个人可以在子团队之间迁移,分配给子团队的问题可以随着时间的推移而变化。这需要在 “过于僵化” 和 “过于模糊” 之间把握好分寸。一方面,你希望你的团队有一个清晰的问题感,目标感和稳定的成就感。另一方面,人们也需要自由度去调整方向,并通过尝试新事物来应对变化的环境。
作为一个领导者,你的盘子里总是摆满了需要完成的重要任务。这些任务中的大多数对你来说都相当容易。假设你正在辛勤的处理你的收件箱,回复问题,然后决定留出 20 分钟的时间来解决一个长期困扰你的问题。但在你执行任务之前,请稍微停下来思考一下。问自己这样一个问题:我真的是唯一可以完成这项工作的人吗?
当然,你亲自动手可能是最有效率的,但是那样你就无法培养你的领导者,你不是在建立一个自立的组织。除非任务确实是时间紧迫火烧眉毛,否则就忍耐一下,把工作交给别人去做吧,大概你认识的某个人可以完成任务,单可能需要花更长的时间才能完成。如果需要的话,请指导他们的工作。你需要为你手下的领导者创造成长的机会;他们需要学会 “突破自我”,并自己完成这项工作,这样你就不会一直处在关键路径上了。
作为领导者的领导者,你需要牢记自己的目标。如果你发现自己陷入各种琐事之中,那你就是在给组织帮倒忙。当你每天开始工作时,都问自己一个关键的问题:我能做什么事我团队中其他人做不了的事?
有很多好的答案。例如,你可以保护你的团队免受组织政策的影响;你可以给予他们鼓励;你可以确保每个人都能和睦相处,从而营造一种谦虚,信任和尊重的文化。“向上管理” 也很重要,要确保你的管理链了解你的团队正在做什么,并和整个公司保持连接。
你正在构建一个蓝图,说明如何解决模糊不清的问题,以及你的组织如何随着时间推移管理这个问题。你不断的绘制森林的地图,然后把砍树的工作分配给其他人。
让我们假设你现在已经达成了目标,你已经建立了一个自运转的机器。你不再是个单点故障了。恭喜你!那么现在你该做什么了?
“现在做什么呢?”,简单的答案是指挥这台机器,并保持它的健康。但除非出现危机,否则你应该只是点到为止。
一个深思熟虑的调整可以产生巨大的影响。我们在管理人员的时候可以使用这种方法。请想象我们的团队像一个大飞艇一样飞行,缓慢而坚定的朝某个方向前进。我们花一周的时间仔细观察和倾听,而不是微观管理和尝试不断修正航向。周末的时候,我们在飞艇的某个精确的位置上做了一个小的粉笔记号,然后用了很小的力,在这个关键位置上 “轻轻一拍”,便调整了航向。
好的管理就是这样:95% 的观察和倾听,5% 在正确的地方进行关键的调整。
一个常见的错误是让一个团队负责特定的产品,而不是一个一般性的问题。产品是问题的解决方案。解决方案的寿命可能很短,并且产品可以被更好的解决方案替代。不过,一个问题如果选的好的话能够经久不衰。将团队身份固定到特定的解决方案,会随着时间的推移导致各种焦虑。团队会盲目的固执己见,因为解决方案已经成为了团队身份和自我价值的一部分。如果团队变成了负责一个问题,那么随着时间的推移,团队就可以腾出手去尝试不同的解决方案。
作为领导者,你最宝贵的资源是有限的时间,注意力和精力。如果你在积极构建团队职责和权力的过程中,没有学会保持头脑清醒,那么扩大规模注定要失败。
请记住,作为领导者,你的工作就是做那些只有你能做的事情。
周末不是假期,至少花三天时间 “忘记” 你的工作,至少花一周时间来彻底恢复精力。只有当你真正懂得如何切断与他人的联系时,你的假期才会充满乐趣。
有时候,无缘无故,你就过了糟糕的一天。你可能睡的很好,吃的很好,做了运动,但你仍然处于糟糕的情绪中。如果你是一位领导者,这是一件可怕的事情。你的坏情绪会给周围的人定下基调,并导致糟糕的决定。如果你发现自己正处于这种情况,就转身回家,宣布请一天病假。当天什么也别做,总比造成主动伤害好。
7. 度量工程生产力
在软件工程领域,谷歌发现当公司规模扩张时,拥有一支专注于工程生产力的专家团队本身就是非常有价值和重要的,并且能够利用来自这样一个团队的深刻见解,从而使公司收益。
随着组织规模的线性增长,沟通成本呈几何级数上升。为了扩大业务范围添加更多的人是很有必要的,但是,当增加额外的人员时,沟通成本并不是线性增长的。
因此,将无法根据工程组织的规模按比例线性的扩展业务范围。
不过,还有另一种方法可以解决我们的业务扩展问题:我们可以让每个人更有生产力。如果我们能够提高组织中每个工程师的个人工作效率,我们就能扩大业务范围,而不必相应的增加沟通开销。
谷歌的新业务迅速成长,这意味着要学会如何提高我们工程师的工作效率。
要做到这一点,我们需要了解是什么让他们高效,识别在我们的工程过程中低效的地方,并修复识别出的问题。
然后,我们将根据需要重复这个循环,进行持续改进。
通过这样做,我们将能够随着对业务需求的增加而扩展我们的工作组织。
然而,这种改进周期也需要人力资源。如果你的工程组织每年要花费 50 个工程师来理解和修复生产力存在的问题,那么每年提高相当于 10 个工程师的生产力,这种改进就是不值得的。因此,我们的目标不仅仅是提高软件工程的生产力,而且是高效的做到这一点。
在我们决定如何度量工程师的生产力之前,我们需要知道什么时候一个指标是值得度量的。
度量本身很昂贵:它需要人们度量工作流程,分析结果,并将结果发布给公司的其他部门。
此外,度量过程本身可能很繁重,并减缓了工程组织的其他工作。
在谷歌,我们提出了一系列问题来帮助团队确定是否值得一开始就度量生产力。
我们首先要求人们以具体的形式来描述他们想度量什么。
我们发现,人们越能具体的描述这个问题,他们就越有可能从这个过程中受益。
然后我们请他们考虑以下各方面的问题:
- 你期待什么结果,为什么
- 如果数据支持的预期结果,将采取什么行动
- 如果我们得到一个负面的结果,会采取适当的行动吗
- 谁将决定对结果采取行动,他们将何时采取行动
通过询问这些问题,我们发现,在很多情况下,做度量时不值得的,这很好!
有很好的理由不去度量工具或流程对生产力的影响。
- 现在你无力变更 流程/工具
- 任何结果不就都会因为其他因素而无效
- 结果将仅用作虚荣心指标,以支持你无论如何都要做的事情
- 仅有的度量指标不够精确,不足以度量问题,而且可能会与其他因素混淆
当成功度量软件过程时,并不是着手证明一个假设正确或不正确;成功意味着,给利益相关人决策所需的数据。
如果该利益相关人不使用数据,那么项目常常是失败的。只有当基于度量结果能够做出具体的决策时,我们才应该去度量软件的过程。
在我们决定度量一个软件过程之后,我们需要确定使用什么度量指标。显然代码行数不合适。
在谷歌,我们使用 **目标/信号/指标 框架(GSM Goals/Signals/Metrics)**来指导指标创建。
- 目标:期望达成的最终结果
- 信号:用来判断我们是否已经得到了最终结果的东西。信号是我们想要度量的东西,但是它们本身很可能无法直接度量
- 指标:信号的代理。这是我们事实上可以度量的东西。
路灯效应:如果你只去有路灯照亮的地方找你想找的东西,那你可能找错了地方。
如果只使用那些我们易于理解且易于度量的指标,而不管这些指标是否适合我们的需求时,就会出现这种情况。
GSM 迫使我们思考哪些指标真正能帮助我们实现目标,而不是简单的考虑我们有什么现成的指标。
对于每个度量指标,我们应该能够追溯到它要充当代理的信号,以及它试图度量的目标。
这样可以确保我们知道我们要度量哪些指标以及为什么要度量它们。
8. 风格指南与规则
制定规则的目的是鼓励好的行为和劝阻坏的行为。不同组织对 “好” 和 “坏” 的理解不同,这取决于组织关心什么。
好和坏并没有一个普世的标准;好与坏是主观的,是根据需要而量身定制的。
随着一个组织的发展,既定规则和指导方针形成了编码的通用词汇表。
共同的词汇表使工程师能够集中精力于他们的代码需要表达什么,而不是如何表达。
通过塑造这些词汇表,工程师们往往会自觉地,甚至是下意识的去做 “好的事情”。
因此,规则给了我们广泛的杠杆作用,将共同的发展模式推向期望的方向。
在定义一套规则时,关键问题不是 “我们应该拥有哪些规则?”,我们要问的问题是,“我们要达成什么目标?”。
当我们关注规则所服务的目标时,识别哪些规则支持这个目标,可以更容易的提取出一组有用的规则。
我们规则的目标是管理好开发环境的复杂性,保持代码库的可管理性,同时保证工程师高效的工作。
我们的另一个原则是为代码的读者优化,而不是为作者优化。
考虑到时间的推移,我们的代码被阅读的频率远远高于它被修改的频率。
我们宁愿编写代码时令人厌烦,也不愿它难以阅读。
我们看重 “简单读” 而不是 “简单写”。
如果工程师必须反复的为变量和类型输入可能更长但更具有描述性的名称时,虽然前期成本会更高,但是我们选择为所有未来的读者提供的可读性而付出这样的成本。
作为重要的一部分,我们还要求工程师在代码中留下明确的预期行为证据。
我们希望读者在阅读代码时,能够清楚的了解代码在做什么。
大多数风格指南包含关于注释的要求,注释也是为了支持读者快速理解代码的目标而设计的。
文档注释(加在给定文件、类或函数前面的块注释)描述了随后代码的设计或意图。
实现注释(在代码本身中散布的注释)证明或强调不明显的选择,解释一些棘手的问题,并强调代码的重要部分。
我们有涵盖这两种注释的风格指南规则,要求工程师必须提供其他工程师在阅读代码时可能需要的解释。
一致性使任何工程师即使进入代码库中不熟悉的部分,也能相当迅速的开始工作。
一个本地项目可以有其独特的个性,但其工具是相同的,技术是相同的,库也是相同的,而且都可以很好的工作。
一致性带来的好处远远大于我们失去的创作自由。
代码也是如此,保持一致性有时可能会感到受到限制,但这意味着更多的工程师用更少的精力完成更多的工作。
当代码库的风格和规范在内部保持一致时,编写代码工程师和其他阅读代码的人,可以更关注代码要完成什么,而不是代码的呈现方式。
在很大程度上,这种一致性可以让专家通过代码组合来降低认知成本。
当我们用相同的接口解决问题,并以一致的方式呈现代码时,专家们更容易浏览这些代码,专注于重要的内容,并理解它在做什么。
一致性还使得模块化代码和发现重复变得更容易。
一致性是对规模化的有力支持,工具是组织扩展的关键,代码的一致性使得构建那些能够理解、编辑和生成代码的工具变得更加容易。
如果每个人都有少量不同的代码,那么依赖于一致性的工具的全部好处就无法应用。
当每个人都使用相同的组件,每个人的代码都遵循相同更多结构和组织规则时,我们就可以投资到统一的工具建设上,这些工具为我们的很多维护任务建立起自动化的能力。如果每个团队需要分别投资同一工具的定制版本,根据他们独特的环境进行定制,我们将失去这种优势。
一致性也有助于扩展组织的人力,随着一个组织的增长,在代码库上工作的工程师数量也在增加。
尽可能的保持每个人正在编写的代码的一致性,可以更好的现在项目之间流动,最大限度的减少工程师切换团队的学习成本,增强组织在人员需求波动时,灵活调整和适应的能力。
我们主张一致性的层次结构:“保持一致性” 从局部开始,即给定文件内的规范先于给定团队的规范,然后是大型项目的规范,最后是整个代码库的规范。
鉴于我们追求的软件长生命周期和规模化的能力,我们认识到事情需要改变,因此我们创建了一个更新规则的流程。
9. 代码评审
代码评审是一个由作者以外的人评审代码的流程,通常在将代码引入代码库之前进行。
在谷歌,基本上每个变更在提交之前都要经过评审,每个工程师都要负责发起评审和评审变更。
代码评审通常需要一个流程,以及支持该流程的工具。
重要的是要记住(并接受)代码本身是一种负担。这可能是一个必要的负担,但就其本身而言,代码对于某个人来说知识一个维护任务。
很像飞机携带的燃料,它有重量,当然,它是飞机飞行所必需的。
当然,新特性通常是必需的,但是在开发代码之前,应该首先注意确保任何新特性都是必要的。
重复的代码不仅浪费了精力,实际上它比没有代码花费更多的时间。当代码库中有重复代码时,变更的执行通常需要更多的工作量。
10. 文档
由于文档的受益者是下游用户,所以通常不会给文档作者带来直接的好处。
文档需要更多的前期投入,而且需要很久以后才能为作者带来明显的收益。
但是,就像在测试上的投资一样,在文档上的投资会随着时间的推移而产生回报。
毕竟,你仅仅写一篇文档,但它会被阅读成百上千次,它的最初成本被摊销给所有未来的读者。
文档会随着时间推移而扩展,而且对于组织中的其他人员来说,扩展文档也是至关重要的。
它可以帮助回答以下问题:
- 为什么要做出这些设计决定?
- 为什么我们要用这种方式实现这段代码?
- 两年后,如果你查看自己的代码,为什么我要以这种方式实现此代码?
有些工程师觉得自己不是个能干的作家。但是,你并不需要精通英语,就能写出可操作的文档。
你只需要走出一点自我,试着从读者角度来看事情。
文档被看做是额外的负担(需要另外去维护的别的事情),而不是使现有代码的维护变得更加容易的东西。
文档会减少其他用户提的问题,这对于编写文档的人来说,节约了不少时间。
如果你必须多次解释某件事,那么写文档这个过程是有意义的。
与测试非常相似,你编写一篇好的文档所付出的努力会让你在一生中获得数倍的收益。
文档会随着时间的推移变得至关重要,并且会随着组织规模的扩大,对特别关键的代码会带来巨大的好处。
工程师越是把文档作为软件开发的必要任务之一,他们就越是不会埋怨写作带来的前期成本和投入,因为好的文档可以让他们获得更多的长期利益。
工程师们在编写文档时所犯的最重要的错误之一,就是只为自己写文档。
这样做是很自然的,而为自己写文档也并非没有价值。
相反,在开始写作之前,你应该确定文档的目标读者。可以尝试先确定一个初级读者,并为这个读者写作。
只需要用读者期待的表达方式和风格写就可以了,只要你能读,你就能写。
记住,读者站在你曾经站过的地方,但不具备你的新领域知识。
所以,你不需要成为一个伟大的作家,只需要让想你一样的人和你现在一样熟悉这个领域。
在某些情况下,不同的读者需要不同的写作风格,但在大多数情况下,诀窍是以一种尽可能广泛的适用于不同受众群体的方式来写作。
通常情况下,当你需要向专家和新手解释一个复杂的主题时,为那些有领域知识的专家们写作,也许会让你走捷径,但是你会把新手搞糊涂,而向新手详细解释每件事,无疑会惹恼专家。
写这样的文档需要平衡,也没有什么灵丹妙药,然而,我们发现,做适当的平衡有助于你保持文档的简短。
保持足够的描述信息,向不熟悉这个主题的人解释复杂的主题,但不要忽视或惹恼专家类读者。
大多数技术文档都回答了一个 “How” 的问题。这个怎么用?我如何编写这个 API?如何设置这个服务器?
因此,软件工程师倾向于直接跳到文档的 “How” 部分,而忽略与之相关的其他问题:Who,What,When,Where,Why。
- Who:文档的受众
- What:文档的协作目的
- When:文档何时创建、评审或更新的
- Where:文档应该位于何处
- Why:设置该文档的原因
11. 测试概述
很长一段时间依赖,软件测试都处在一种状态,需要大量的手动测试,并且容易出错。
然而,自本世纪初依赖,软件行业的测试方法已经发生了巨大的变化,以应对现代软件系统的规模和复杂性。
发展的核心是开发人员驱动的自动化测试实践。
自动化测试可以防止缺陷逃逸,这些缺陷可能会影响到用户。
在开发周期中,一个缺陷被捕获的时间越晚,它的成本就越高,在许多情况下,这个成本呈指数增长。
然而,“捕获缺陷” 只是做自动化测试的一部分动机。另一个同样重要的原因是支持变更的能力。
无论你是添加新功能、进行保持代码健康的重构,还是进行更大的重新设计,自动化测试都可以快速发现错误,这使得有信心的更改软件成为可能。
迭代速度更快的公司,可以更快的适应不断变化的技术、市场条件和客户口味。
如果有一个健壮的测试实践,那就不必害怕变化,可以更好的拥抱变化,因为有基础的质量保障。
越想快的改变系统,就越需要一种快速的方法来测试它们。
编写测试的行为也改进了系统的设计,作为代码的第一个客户,测试可以告诉你关于设计选择的很多信息。
系统与数据库的耦合是否过紧?API 是否支持所需的用例?系统能处理所有的边界用例吗?
编写自动化测试会迫使你在开发周期的早期面对这些问题。
这样做通常会使得软件更加模块化,且有更大的灵活性。
测试的价值来自工程师对它们的新人,如果测试成为生产力的障碍,不断的带来琐事和不确定性,工程师们就会失去信任并开始寻找解决办法。
一个糟糕的测试套件可能比没有测试套件更麻烦。
除了使公司能够快速开发出优秀的产品外,测试对于确保我们生活中重要产品和服务的安全也变得至关重要。
在谷歌,我们已经确定测试并不是事后才考虑的事情,专注于质量和测试是我们工作的一部分。
我们已经认识到,未能将质量融入我们的产品和服务,将不可避免的导致糟糕的结果,有时这是很痛苦的。
因此,我们将测试作为我们工程文化的核心。
不能仅仅依靠程序员的能力来避免产品缺陷,即使每个工程师只是偶尔编写出缺陷,但是当有足够的人在同一个项目上工作之后,你也会被不断增长的缺陷列表所淹没。
假设有一个 100 人的团队,工程师们都非常优秀,每个月每人只写出一个缺陷。
总的来说,这群令人惊叹的工程师每个工作日仍然会产生 5 个新的缺陷。
更糟糕的是,在一个复杂的系统中,修复一个缺陷常常会导致另一个缺陷,因为工程师慢慢的习惯已知的缺陷,并围绕它们写代码。
最好的团队会想方设法将其成员的集体智慧转化成对整个团队的利益,这正是自动化测试的作用。
团队中的工程师编写测试后,它将被添加到其他人可用的公共资源池中。
团队中的其他每个人都可以运行测试,并在检测到问题时受益。
在指导新员工时,我们经常被问到,哪些行为或属性实际上需要测试?直截了当的答案是:测试所有不想被破坏的东西。
换言之,如果想确信某个系统展现出某个特定的行为,那么唯一能使用的方法就是未这个特定行为编写一个自动化的测试。
这包括所有常见的容易出问题的地方,如性能、行为正确性、可访问性和安全性。
碧昂丝规则:如果你喜欢它,你就应该测试它。
碧昂丝规则经常被负责在整个代码库中进行变更的基础设施团队所引用。
如果不相关的基础设施变更通过了所有测试,则产品团队需要修复它,并添加额外的测试。
代码覆盖率是衡量功能代码行被测试的比例。
代码覆盖率通常被视为了解测试质量的黄金标准指标,这有点不幸。
有可能出现这样的情况,那就是用一些测试来运行很多行代码,而从不检查每一行是否做了任何有用的事情。
这是因为代码覆盖率只衡量被调用的代码行,并不是调用的结果。
度量测试套件质量的一个更好的方法是考虑被测试的行为。
请尝试回答 “我们有足够的测试吗?”
代码测试覆盖率可以提供对未测试代码的一些洞察,但它不能代替对系统的测试情况的批判性思考。
我们代码库的开放性鼓励集体所有权,让每个人都对代码库负责。
这种开放性的一个好处是,你能够直接修复你所用到的产品或服务中的缺陷(当然,要经过批准),而不是抱怨它。
这也意味着许多人会对别人拥有的代码的一部分进行变更。
随着代码库的增长,将不可避免的需要对现有代码进行变更。
如果现有代码写的不好,自动化测试会使这些变更变得更困难。
脆弱测试(那些过分指定预期结果或依赖于那些复杂却被广泛使用的样板文件的测试),实际上会阻碍变更。
因为,即使做了不相关的变更,这些写得不好的测试也有可能会失败。
如果你曾经对一个特性做过 5 行修改,却发现了几十个无关的、被破坏的测试,那么你已经感受到了脆弱测试的冲突。
随着时间的推移,这种冲突会使团队不再为保持代码库健康而进行重构。
脆弱测试中一些最糟糕的情况来自对模拟(Mock)对象的误用。
谷歌的代码库遭受过严重的模拟(Mock)框架的滥用,它导致一些工程师宣布 “不再使用模拟(Mock)”。
虽然这是一个强有力的声明,但是理解模拟(Mock)对象的局限性,可以帮助你避免误用它们。
测试套件运行的速度越慢,被执行的频率就会越低,而且它提供的好处也就越少。
如果不能保持测试套件的确定性和运行快速性,那么它将成为提高生产力的障碍。
马桶测试:有人开玩笑的提议在厕所张贴传单,厕所是每个人每天都必须至少去一次的地方,不管是不是开玩笑,这个想法很容易做到。
自动化测试并不适合所有的测试任务。例如,测试搜索结果的质量通常需要人工判断。
通过使用自动化测试来涵盖人们已经充分理解的行为,使人工测试人员能够花费大量的精力和定性的工作,来专注于产品中他们能够为之提供最大价值的部分。
采用开发人员驱动的自动化测试是谷歌最具变更性的软件工程实践之一。
它使我们能够用更大的团队构建更大的系统,比我们想象的要快。
它帮助我们跟上技术变革的步伐。
12. 单元测试
粒度:测试所消耗的资源和允许它做的事情。
范围:测试打算验证多少代码。
我们使用 “单元测试” 一词来指范围相对狭窄的测试,例如,单个类或方法的测试。
除了防止缺陷,测试最重要的目的是提高工程师的工作效率。
与范围更广的测试相比,单元测试具有许多特性,这些特性使它们成为优化生产力的一种极好的方法。
谷歌编写的大多数测试都是单元测试,根据经验,我们鼓励工程师按照 80% 的单元测试和 20% 的范围更广的测试的比例来写测试。
因为单元测试在工程师的生活中,占据了如此重要的部分,所以谷歌非常注重测试的可维护性。
可维护的测试是指 “可工作” 的测试,在编写它们之后,工程师不需要再考虑它们,直至它们运行失败。
而这些失败表明出现了真正的有明确原因的缺陷。
糟糕的测试必须在签入之前修复,以免给未来的工程师带来拖累。
- 测试不能是脆弱的:它们是对一个无害的、不相关的、没有引入真正缺陷的变更做出响应。
- 测试必须要清晰:它们失败之后,很难确定出了什么问题,如何修复,以及这些测试最初的职责是什么。
脆弱的测试是指在对生产代码进行不相关的变更,而又不会引入任何真正的缺陷时,但却令其失败的那些测试。
工程师必须诊断和修复此类测试,作为其工作的一部分。
最理想的测试是不变的,在编写之后,除非被测系统的需求发生变化,否则它永远不需要改变。
有两种方法可以验证被测系统是否按预期运行。
通过状态测试,可以观察系统本身,以查看调用后的系统表现。
通过交互测试,可以检查系统是否对其协作者执行了预期的操作序列,协作者又是如何响应调用该系统的。
许多测试是状态验证和交互验证的组合。
交互测试往往比状态测试更脆弱,同样的原因是测试私有方法,比测试公共方法更脆弱。
交互测试检查系统是通过什么样的调用过程得到结果的,而通常你应该只关心结果是什么。
有问题的交互测试最常见的原因是过度依赖模拟(Mocking)框架。
这些框架让测试替身的创建变得很容易,而这些测试替身用来记录和验证针对它们的每个调用,并在测试中,这些测试替身代替了实际对象。
这种策略直接导致脆弱的交互测试,因此,只要真实对象是快速和确定的,我们更倾向于使用真实对象而不是模拟对象。
随着时间的推移,测试清晰度变得更重要。测试通常会比编写测试的工程师存在更长久,并且随着系统的演进,对系统的需求和理解也会发生微妙的变化。
一个失败的测试,完全有可能是由一个已经不在团队中的工程师在多年前编写的,无法确定其目的或如何修复它。
这与不清晰的生产代码形成了鲜明的对比,对于不清晰的生产代码,可以花足够的时间通过查看调用它的代码和删除它时哪里被破坏了,来确定其用途。
但是对于一个不清楚的测试,你可能永远不会理解它的目的,因为删除测试除了(可能)在测试覆盖率中产生一块空白之外,不会产生任何影响。
在最坏的情况下,当工程师们不知道如何修复这些模糊的测试时,这些测试就会被删除。
移除这样的测试不仅会在测试覆盖率中引入一块空白,而且还表明测试在它存在的整个时期(可能是几年)一直没有提供任何价值。
为了使测试套件随着时间的推移能扩展并且有用,该套件中的每个单独的测试都尽可能清晰是很重要的。
帮助测试实现清晰性的两个高层属性是完整性和简洁性。
当一个测试的主体包含了读者为了理解它是如何得到结果而需要的所有信息时,这个测试是完整的。
当测试不包含其他干扰或不相关的信息时,它是简洁的。
一个测试的主体应该包含理解它所需的所有信息,而不包含任何无关的或分散注意力的信息。
与其为每个方法编写测试,不如为每个行为编写测试。
行为是系统在特定状态下如何响应一系列输入的保障。
行为通常可以用 given/when/then 来表示,given 银行账户为空,when 试图从中取款,then 交易被拒绝。
方法和行为之间的映射是多对多的,大多数复杂的方法实现多个行为,有些行为依赖于多个方法的交互。
将测试视为行为而不是方法相耦合会显著影响它们的结构。记住,每个行为都有三个部分:given 定义系统的预设条件,when 定义要在系统上执行的操作,then 验证结果。
当结构明确的时候,测试是最清晰的。一些框架,比如 cucumber 和 spock 直接固定了 given/when/then 的结构。
其他语言可以使用空格和可选注释使结构突出。
测试命名应该概括它正在测试的行为,一个好名字描述了系统正在进行的操作和预期的结果。
如果你陷入困境的话,一个很好的技巧就是尝试用单词 should 来开始测试命名。
如果需要再测试名称中使用 and 一词,那么很有可能你实际上在测试多个行为,并且应该编写多个测试。
不要把逻辑放进测试,测试代码并不需要再被测试,如果你觉得需要再编写一个测试,用来验证你写好的测试的话,那么一定是出了问题。
编写清晰的失败信息,清晰性的最后一个方面与编写测试无关,而是与工程师在测试失败时看到的东西有关。
一个好的失败消息包含与测试名称几乎相同的信息,它应该清楚的表示期望的结果、实际的结果和任何相关的参数。
DRY(不要重复自己),这种方法使得代码变更更容易,因为工程师只需要更新一段代码,而不需要跟踪多个引用。
这种方式的缺点是,它会使代码变得不清晰,要求读者追踪引用链来理解代码的作用。
在正常的生产代码中,这一缺点通常是为了使代码更易于变更和使用,而付出一个小代价。
但是这种 成本/收益 分析在测试代码的上下文有点不同,好的测试是为了稳定而设计的,事实上,当被测试的系统发生变化时,你通常希望一些测试会失败。
所以 DRY 在测试代码方面没有那么多好处,同时,对于测试来说,复杂性的代价更大,生产代码有测试套件的好处是,可以确保它在变得复杂时继续工作,而测试必须独立进行,如果它们不是自明正确的,就有可能出现错误。
如果测试变得太复杂,以至于我们感觉自己需要对测试进行测试,以确保它们正常工作,那么就会出现问题。
测试代码不应该完全是 DRY,而应该努力保持 DAMP,也就是说 “描述性和有意义的短语”(Descriptive and Meaningful Phrases)。
单元测试是最强大的工具之一,作为软件工程师,我们必须确保我们的系统在面对未预料到的变化时,能够长期工作。
但是,作用越大,责任也就越大,不细心的使用单元测试,可能会导致需要更多投入来维护系统,并且在对系统进行变更时需要更大的投入,但实际上却并没有提高我们对该系统的信心。
13. 测试替身
单元测试是保持开发人员工作效率,和减少代码缺陷的关键工具。
尽管对于简单的代码来说,它们很容易编写,但是随着代码变得越来越复杂,编写它们也变得困难。
测试替身是一个对象或函数,它可以在测试中代表那个真实的实现,类似于特技替身演员替代电影演员的情况。
过度使用模拟框架(mocking framework)会带来危险,许多工程师避免使用模拟框架,转而编写更真实的测试。
如果可以为代码编写单元测试,那么我们称代码是可测试的。
缝(seam)是一种通过允许使用测试替身来实现代码可测试性的方法,它可以为被测系统使用不同的依赖项,来替代在生产环境中所使用的依赖项。
依赖注入是引入缝的常用技术。简言之,当类使用依赖注入时,它所用的任何类(即类的依赖项)都讲传递给它,而不是直接实例化,从而使这些依赖项能够在测试中被替换。
如果是动态语言,那么可以动态替换单个函数或对象方法。
依赖注入在这些语言中不那么重要。因为动态语言的这种能力让在测试中使用依赖项的实际实现成为可能,同时只要重写不适合测试的依赖函数或方法即可。
编写可测试代码需要预先投资,它在代码库生命周期的早期尤为重要,因为越晚考虑可测试性,可测试性应用于代码库就越困难。
不考虑测试的代码通常需要重构或重写,然后才能添加适当的测试。
模拟(mocking)框架是一个软件库,它使在测试中创建测试替身更加容易;
它允许使用一个模拟对象,来替换真实对象,模拟对象是一个在测试中内联指定其行为的测试替身。
模拟框架的使用减少了样板代码,因为不必每次需要测试替身时,都定义一个新类。
尽管模拟框架有助于更容易的使用测试替身,但是在使用时也需要特别注意,因为他们的过度使用通常会使代码库更难维护。
使用测试替身有三种主要技术:
- 伪造(faking):伪实现是 API 的轻量级实现,其行为与实际实现类似,但不适合生产环境,例如,内存数据库。
- 打桩(stubbing):打桩是将行为赋给一个函数的过程,该函数本身并没有行为,你可以确切的为该函数指定要返回的值(即打桩返回值)。
- 交互测试:是一种在不实际调用某函数具体实现的前提下,验证如何调用该函数的方法。(主要验证的场景,例如,函数没有实际被调用,调用次数过多,或者使用了错误的参数调用)
过度使用模拟框架有一种倾向,即用与真实实现不同步的重复代码污染测试,从而使重构变得困难。
倾向于实际实现的测试被称为 经典测试,还有一种称为 模拟测试 的测试风格,在这种风格中,首选的是使用模拟框架,而不是实际实现。
如果单元测试过多的依赖于测试替身,工程师可能需要运行集成测试或手动验证其功能是否按预期工作,以便获得相同的可信度。
14. 较大型的测试
15. 弃用
所有系统都会老化。旧系统需要持续的维护、深奥的专业知识,并且由于旧系统与周围生态系统逐渐分化,它会需要更多的工作量来维护。
因此,对于过时的系统,通常最好是将精力放在关闭它,而不是无限期的维护,让它们与替代它们的新系统并存。
我们将有序迁移旧系统,并最终将其移除的过程称之为弃用。
相比编程来说,弃用与软件工程学科契合更紧密,因为它需要考虑如何随着时间的推移来管理系统。
对于长期运行的软件生态系统,计划和执行正确的弃用计划,并通过消除随时间推移而积累在系统中的冗余和复杂性,从而降低资源成本并提高速度。
相反,错误的弃用系统可能造成的成本比置之不理更高。
虽然弃用系统需要付出额外的工作,但可以在软件系统设计时,就计划好未来的弃用,以便等到真正需要弃用时跟狗容易的进行移除。
我们对弃用的讨论从以下基本前提开始,代码是负债,而不是资产。
代码是要消耗成本的,其中创建系统所产生的的成本只是一部分,更多的成本消耗是随着系统在其整个生命周期的发展中,所产生的维护成本。
这些持续的维护成本,例如保持系统运行所需的运维资源,或随着周围生态系统的发展而需要不断更新其代码,这就意味着有必要对是否应该维持原有的系统还是弃用之间进行权衡。
弃用最适用于一个明显过时的系统,该系统有一个成型的替代产品已经存在,并且可以提供相同的功能。
从长远来看,我们发现拥有多个执行相同功能的系统会阻碍新系统的开发,因为它一直有可能被期望与旧系统有兼容性。
移除旧系统需要花费精力,但是这些换来的回报是更换的新系统可以更快的发展。
要谨慎的选择弃用项目,然后坚持完成它们也是很重要的。
在谷歌内部,我们发现迁移到全新系统的成本非常高,而且成本经常被低估。
通过就地重构的方式来逐步完成弃用工作,可以使现有系统保持运行,同时更容易为用户提供价值。
将弃用的概念纳入系统设计的阶段,在软件工程中可能是比较超前的,但是,在其他工程学科中却很常见。
以核电厂的设计为例,它是一个极其复杂的工程,在核电站的设计中就必须考虑使用寿命结束后的退役,甚至要为此预留经费。
可是,软件系统对这方面的考量少之又少,许多软件工程师对构建和启动新系统更感兴趣,而不是维护现有系统的任务。
我们鼓励谷歌的工程团队提出以下几个问题:
- 消费者从该产品迁移到其他替代产品有多容易?
- 如何逐步更换部分系统?
我们需要指出的是,是否长期支持某个项目,是公司在构建该项目的初期就要做的决定。
当一个软件系统存在以后,能做的就是维护该软件,谨慎的选择弃用该软件,或者在某些外部事件导致它崩溃时让它停止运行。
简而言之,你的公司如果没有做好准备支持该项目的整个生命周期,那就不要开始这个项目。
16. 版本控制与分支管理
由于 DevOps 而流行起来的 “基于主干的开发”(一个存储库,没有开发分支),其实是一种有利于规模化的策略方法。
与任何流程一样,版本控制也会带来一些开销,必须有人配置和管理你的版本控制系统,每个开发人员都必须使用它。
但别搞错了,这些事情的成本几乎总是很低。
任何事情都可以考虑不同的方案,甚至是一些基本的东西,如版本控制。
分支等同于在制品
我们认为,将开发分支作为实现产品质量稳定的手段在本质上是错误的。
同一个变更集最终会被合并到主干上,而小的合并要比大的合并更容易。与批量处理不相关的变更,然后再一起合并的方式相比,由编写这些变更的工程师自己进行合并会更容易。
如果只要一个工程师参与,那么更容易确定是谁的变更导致了回归问题。
合并一个大型的开发分支意味着测试的一次执行将同时检查多个代码变更,当失败时,更难以区隔开。
对问题进行分类和根本原因分析是困难的,修复它更困难。
基于主干的开发,严重依赖测试和 CI,保持构建为绿色(指成功),并在运行时禁用未完成/未测试的特性。
每个人都自动负责提交到主干,并与主干同步;不需要 “合并策略” 会议,也没有大型/昂贵的合并。
而且,不会有关于应该使用哪个版本的库的激烈讨论——因为只有一个版本。
如果产品发布之间的时间间隔,超过几个小时,可以创建一个发布分支,让这个分支只包含为了产品发布而构建的代码,这么做可能是个明智的做法。
如果从产品实际发布出去之时,到下一个发布周期之前,这段时间发现了任何关键缺陷,那么可以从主干挑选(cherry-pick)出用于修复这个缺陷的代码,合并到发布分支上。
与开发分支相比,发布分支通常是无害的,问题不在于 “拉分支” 这项技术成本,而是分支的使用方法。
开发分支和发布分支之间的主要区别在于预期的最终状态:开发分支预期合并会主干,甚至有可能被另一个团队进一步分支;而一个发布分支最终会被抛弃。
由谷歌 DevOps Research and Assessment(DORA)部门发现,在最高效的组技术组织中,发布分支实际上是不存在的。
已经实现了持续部署(CD)的组织(拥有每天从主干发布多次的能力),可能会跳过发布分支:只要在主干上添加修复,并重新部署即可,这比使用发布分支要容易得多。
这样的话,cherry-pick 和分支似乎是不必要的开销。
同样的 DORA 研究也表明,“基于主干的开发” “没有长周期的开发分支” 和良好的技术成果之间存在着很强的正相关关系。
这两种观点的基本思想似乎都很清楚:分支是生产力的累赘。
谷歌的版本控制依赖于一个内部开发的集中式 VCS,称为 Piper,在生产环境中作为分布式微服务运行。
在主干上创建一个新客户端、添加一个文件,并将一个(未做评审的)变更提交给 Piper 总共可能需要 15 秒。这种低延迟的交互和易于理解/设计良好的可扩展性大大简化了开发人员的体验。
由于 Piper 是一个内部产品,我们有能力定制它并执行我们选择的任何源代码控制策略。
例如,我们在单一代码仓中有一个所有权的概念,在文件层次结构的每个级别,我们都可以找到 OWNERS 文件,其中列出了允许在存储库的子树中,批准提交的工程师的名字。
在多仓环境中,这可能是通过文件系统权限,强制控制每个存储库的提交访问权限,或通过 git commit hook 执行单独的权限检查来实现的。
通过控制 VCS,我们可以使所有权(ownership)和批准(approval)的概念更加明确,并在尝试提交操作时,由 VCS 强制执行。
除了我们的 VCS,谷歌版本控制政策的一个关键特性就是我们所说的 “单一版本”(One version)——对于存储库中的每个依赖项,必须只能选择该依赖项的唯一版本。
对于第三方包,这意味着在稳定状态下,只能有一个版本的包签入到我们的存储库中。
对于内部包,这意味着,如果不重新打包或重新命名就不能分叉(fork)。
对于我们的生态系统来说,这是一个很强大的特性:很少有包具有这样的限制 “如果包含这个包 A,就不能包含其他包 B”。
任何允许在同一个代码库中有多个版本的政策体系,可能都会存在这种代价高昂的不兼容性。
有可能你会逃脱一段时间,但总的来说,任何多版本的情况,都有可能导致大问题。
在实践中,“单一版本” 并不是硬性规定的,但是在添加新的依赖项时,要限制可以选择的版本,这种措辞表达了一种非常强烈的共识。
开发分支应该是最小化的,或者最好是非常短暂的。这源于过去 20 年里发表的大量成果,从敏捷过程到基于主干开发的 DORA 研究结果,甚至凤凰项目关于 “减少在制品” 的课程。
当我们把开发分支等同于未完成的工作时,这进一步证实了应该基于主干进行小的增量开发,并频繁的提交。
请记住,在添加依赖项时,新的开发必须没有选择。
重要的并不是单一代码仓,而是尽可能坚持 “单一版本” 的原则:当开发人员向一些已经在组织中使用的库添加依赖项时,不能有选项。
违反 “单一版本” 规则的选项会导致合并策略讨论、菱形依赖、损耗生产力和浪费精力。
如果可以使用独立的多个存储库,进行管理并坚持使用一个版本,或者工作可以完全解耦足以允许真正的独立存储库,那很好。
“单一版本” 规则:组织中的开发人员不能自行选择在哪里提交,或者依赖于现有组件的哪个版本。
17. 代码搜索
18. 构建工具与构建哲学
如果你问谷歌的工程师,在谷歌工作他们最喜欢什么,你可能会听到一些令人惊讶的事情:工程师喜欢构建系统。
谷歌在其生命周期中投入了大量的工程工作来从头创建自己的构建系统。
其目标是确保我们的工程师能够快速、可靠的构建代码。
从根本上说,所有构建系统都有一个直接的目的,它们将工程师编写的源代码转换成可由机器读取的可执行二进制文件。
构建系统不仅仅是为人类设计的,它们还允许机器自动执行构建,无论是用于测试还是用于发布到生产环境。
实际上,谷歌的大部分构建都是自动触发的,而不是由工程师直接触发的。
几乎我们所有的开发工具,都以某种方式与构建系统绑定,为每个在我们的代码库上工作的人提供了巨大的价值。
大型系统通常包括用各种编程语言编写的不同部分,这些部分之间存在依赖关系,这意味着没有任何一种语言的编译器可以构建整个系统。
一旦我们不得不处理来自多种语言或多个编译单元的代码,构建代码就不再是一个单步过程。
现在我们需要考虑我们的代码依赖于什么,并以适当的顺序构建这些片段,可能为每个片段使用不同的工具集。
如果我们更改任何依赖项,我们需要重复这个过程,以避免依赖陈旧的二进制文件。
对于一个中等规模的代码库,这个过程很快就会变得烦琐,并且容易出错。
管理自己的代码相当简单,但是管理它的依赖项就困难的多。
有各种各样的依赖项:
- 有时依赖于一个任务(例如,在我标记一个发布完成之前先推送文档)
- 有时依赖一个制品(例如,我需要计算机视觉库的最新版本来构建我的代码)
- 有时是对代码库的另一个部分有内部依赖
- 有时是对其他团队(组织或第三方)拥有的代码或数据的外部依赖
但在任何情况下,“在我得到这个之前我先需要那个” 的想法,在构建系统的设计中反复出现。
而管理依赖项可能是构建系统最基本的工作。
基于任务的构建系统:
- 难以并行构建
- 难以执行增量构建
- 难以维护和调试脚本
基于制品的构建系统:构建系统的主要任务应该是构建代码。工程师仍然需要告诉系统要构建什么,但是如何构建将有系统决定。
Blaze 中的构建文件不是图灵完备性脚本语言中描述如何生成输出的命令集,而是一个声明性的清单文件,它描述了要构建的一组制品,它们的依赖项以及影响它们构建方式的有限选项集。
当工程师在命令行上运行 blaze 时,他们制定一组要构建的目标(what),blaze 负责配置、运行和调度编译步骤(how)。
因为构建系统现在可以完全控制什么时候运行什么工具,所以它可以做出更有力的保证,在保证正确性的同时,让它更高效。
用制品而不是任务来重新定义构建过程,很巧妙,也很强大。
通过减少对程序员的灵活性,构建系统可以知道在构建的每一步都在做什么。
它可以利用这些知识,通过并行化构建过程并重用它们的输出,使构建更加高效。
但实际上这只是第一步,这些并行化和重用的构建块将构成一个分布式的、高度可伸缩的构建系统的基础。
19. Critique:谷歌的代码评审工具
20. 静态分析
21. 依赖管理
为什么依赖管理这么难,在这个领域中,许多半生不熟的解决方案都关注于狭隘的问题描述:如何导入本地开发代码可以依赖的包。
这是一个必要但不充分的描述。
诀窍不仅仅是找到一种方法来管理一个依赖项,而是如何管理一个依赖网络及其随时间的变化。
这个网络的某些子集对于第一方代码是直接必需的,而某些子集只是有传递依赖引入的。
一个稳定的依赖管理方案,必须在时间和规模上保持灵活:我们不能假设依赖图中任何特定节点具有无限的稳定性,也不能假设没有添加新的依赖(无论是我们所控制的代码中,还是我们所依赖的代码中)。
依赖管理本身就是一项挑战,来管理这些复杂 API 界面和依赖网络,而这些依赖的维护者之间通常很少或根本没有协调机制。
与管理依赖问题相比,更倾向于使用源代码控制方式:如果你可以从组织中获得更多代码,以获得更好的透明度和协调性,那么问题都可以很好的简化。
对于一个软件工程项目,添加依赖并非是免费的,而且建立 “持续的” 信任关系的复杂性也具有挑战性。
在组织中导入依赖需要小心行事,并了解持续提供支持的成本。
22. 大规模变更
23. 持续集成
24. 持续交付
任何软件工作的最大风险是最终构建的东西是无用的,越早、越频繁的将可工作的软件呈现在实际用户面前,就会越快的得到反馈,从而发现它的真正价值。
在交付用户价值之前,长时间处于进行中的工作是高风险和高成本的,甚至会消耗士气。
在谷歌,我们努力尽早且频繁发布,或者 “产品上市并迭代”,以使团队能够快速看到他们工作的影响,并更快的适应不断变化的市场。
代码的价值不是在提交时实现的,而是在特性呈现在用户面前时实现的。
缩短 “代码完成” 和用户反馈之间的时间,可以最大限度的降低正在进行的工作的成本。
多年来,在我们所有的软件产品中,我们发现,更快就是更安全,这可能与直觉相反。
你的产品的健康和开发速度实际上并不是对立的,更频繁且小批量发布的产品有更好的质量结果。
它们能更快的应对发现的缺陷,和意想不到的市场变化。
不仅如此,更快的话,成本也会更低,因为一个可预测且频繁发布的火车会迫使我们努力降低每次发布的成本,并让放弃发布的成本非常低。
25. 计算即服务
后记
谷歌的软件工程师在如何开发和维护一个大型的不断发展的代码库方面进行了非凡的试验。
在技术不断变化的环境中,软件工程在一个既定的组织中扮演着特别重要的角色。
今天,软件工程原则不仅仅是关于如何有效的运行一个组织,而是关于如何成为一个对用户和整个世界更负责任的公司。
可持续性的思想也是软件工程的核心。
在代码库的预期寿命中,我们必须能够对变化做出反应和适应,无论是产品方向、技术平台、底层库、操作系统等。
Google 软件工程
1. 什么是软件工程
软件工程就是随着时间而不断集成的编程。
海勒姆定律(Hyrum’s Law):当一个 API 有足够多的用户时,在约定中你所承诺的已不重要。所有在你系统里面被观察到的行为,都会被一些用户所依赖。
当我们想一想以 “现在可工作” 和 “一直可工作” 这两种不同的思想编写代码的区别,我们就会理出一些清晰的关系。
如果将代码视为具有高度可变生存期需求的工件,我们可以对编程风格进行分类:
两者都有其目的,但你选择哪一个关键取决于所讨论代码的预期寿命。
我们习惯于说,如果 “巧妙” 是一种赞扬,那就是编程,如果 “巧妙” 是一种指责,那就是软件工程。
对于大多数项目,在足够长的时间内,项目的所有内容都有可能会变化。
大多数项目都有更多的潜在技术转移风险。
当你能够安全的做出任何变更,并且在代码库的生命周期内都可以这样做,那你组织的代码库是具有可持续性的。
如果变更的成本过高,它将可能被推迟了。
如果成本随时间呈超线性增长,那么运营显然是不可扩展的。
最终时间会掌控一切,并且会出现一些意想不到的事情,使你必须改变。
只要通过一点实践,可以很容易的发现那些扩展属性不佳的政策。
最常见的情况是,我们可以想一下,如果组织中的工程师人数扩大 10 倍,或是 100 倍,工程师的工作状态会是怎样的。
当我们的组织规模是现在的 10 倍时,我们的工程师的产出也会增加 10 倍?我们的工程师必须完成的工作量随着组织规模的增长而增长吗?
这项工作是否随着代码库的增长而扩大?如果这两个都是真的,我们是否有机制来自动化或优化一些工作?如果没有,我们就有规模化方面的问题。
2012 年,我们尝试使用 “减少搅动” 的规则来阻止这种情况,即基础设施团队必须自己完成将内部用户迁移到新版本的工作,或者以向后兼容的方式进行适当的更新。
我们称之为 “搅动规则”(churn rule),这一政策的可扩展性很好。
那些使用了依赖的项目,不再仅仅为了跟上最新版本而投入过多的维护精力。
我们还学习到,有一个特定的专家组来执行大规模变更,要比要求每个团队自己来做这种变更会更具有可扩展性。
因为专家们花一些时间,并深入的了解整个问题,然后将该专业知识应用到每个子问题上。
如果我们强迫所有使用该依赖的团队响应这种版本升级,就意味着每个受影响的团队在紧急解决完这个升级问题之后,立刻又会丢掉那些对于他们来说没有什么用的特定知识。
所以,专家组政策更具可扩展性。
我们最喜欢的内部政策之一是,让负责基础设置的团队能够安全的进行基础设施的变更。
如果一个产品由于基础设施的变更而出现中断或其他问题,但是在我们的持续集成(CI)系统中的自动化测试用例并没有发现这个问题,那么就不应该由负责基础设施的团队承担责任。
更通俗的说,那就是,如果你喜欢它,你应该对它进行 CI 测试,我们称之为 “碧昂丝规则”。
如果没有这一点,基础设施团队的工程师可能需要跟踪每个受影响的团队,并询问他们如何才能进行他们的测试。
当有 100 个工程师的时候,我们可以做到,如果超过这个数,我们肯定就负担不起了。
我们发现,专业知识和共享的交流论坛在组织规模化上提供了巨大的价值。
当工程师在共享论坛上讨论和回答问题时,知识往往会传播开来,新专家不断增加。
如果有 100 个工程师在写 Java,并且一个友好且乐于助人的 Java 专家愿意回答问题,那么很快就会使这 100 个工程师写出更好的 Java 代码。
知识是具有病毒性的,而且专家是载体,对于扫除工程师常见的绊脚石的价值有很多值得说的。
基础设施的变更频率越高,变更就越容易做。
我们发现的一个广泛的事实是,在开发人员工作流的早期发现问题,通常会降低成本。
在谷歌内部,人们强烈反感 “我说了算”。重要的是,任何话题都要有一个决策者,当决策错误时需要有明确的升级路径,但目标是要 “达成共识”,而不是 “一致同意”。
我们希望能看到一些这样的例子 “我不同意你的标准/评估,但我了解你是如何得出这个结论的”。
所有这一切的内在含义是,每件事都需要有一个理由,“就是这样,没有理由”、“我说了算”、“因为其他人都是这样做的” 都是错误决定潜伏之处。
只要这样做事有效的,当我们面对两个工程选项的总成本而需要做出决策时,就应该给出相应的解释。
在像软件工程这样的高创意、高收益的领域,财务成本通常不是限制因素 —— “人” 才是。
让工程师保持快了、专注和投入所带来的效率很容易影响其他因素,原因很简单,因为专注和生产力是如此多变,想象一下 10%~20% 的差异。
谷歌是一种数据驱动的文化,事实上,这是一种简化。
即使没有数据,也可能有证据、先例 和 论证。
做一个好的工程决策就是衡量所有可用的输入,并就权衡做出明智的决定。
Jevons 悖论:随着资源使用效率的提高,对资源的消耗可能会增加。
分布式构建系统所节省的成本,远远超过了系统本身开发和维护的成本。
为狭窄的问题空间定制的解决方案,可能会胜过需要处理所有可能性的通用解决方案。
无论我们讨论的是 微服务、内存缓存、压缩程序 还是软件生态系统中的任何其他内容,通常来说,通过 分叉/重新实现 公共代码,并为你的窄域定制它,可以更轻松的添加新功能,或更确定的进行优化。
也许更重要的是,分叉会使得你与底层依赖的变更隔离开来,由另一个团队或第三方提供商所带来的这些变更将不会对你有任何限制。
你可以自己控制如何以及何时对变更做出反应。
另一方面,如果每个开发人员都讲软件项目中使用的所有东西分叉,而不是重用现有的东西,那么除了可持续性之外,可扩展性也受到影响。
如果你的项目寿命短,分叉的风险就小。
我们坚信数据可以支撑决策,但我们也要认识到数据会随着时间的推移而变化,新的数据可能会出现。
这意味着,从本质上讲,在所讨论的系统的生命周期内,需要不时的重新审视决策。
对于长期的项目,在做出初步决策后,能够改变方向通常是至关重要的。
而且重要的是,这意味着决策者需要有承认错误的权利,与通常的本能相反,承认错误的领导人得到更多的尊重,而不是更少。
以证据为导向,但也要意识到,无法度量的东西可能仍然有价值。
如果你是一个领导者,那是你应该做的:运用判断力做决断很重要。
我们的观点并不是说 软件工程 更优越,只是它们(跟编程相比)代表了两个不同的问题领域,具有不同的约束、价值和最佳实践。
相反,指出这种差异的价值在于认识到有些工具在一个领域很好,但在另一个领域却不然。
我们认为区分相关但不同的术语 “编程” 和 “软件工程” 是很重要的,这种区别很大程度上源于时间的推移对代码的管理、时间对规模的影响以及面对这些想法的决策。
编程是产生代码的直接行为,软件工程是一组必要的政策、实践和工具,使得代码在跨团队协作和使用时更有效。
谷歌一直在努力建立一个可持续的代码库和文化。
2. 如何更好的参与团队合作
我认为一个高效、成功的软件工程师需要把主要时间和精力花在编写好代码上,而不是整天都在与人沟通问题。
软件开发是一个需要协同作战的工作,团队是软件开发工作的基本组织。
要想在工程团队或任何其他创造性的合作中取得成功,团队中的每个人都需基于谦逊、尊重和信任的核心原则重新调整自己的行为。
人们害怕别人看到并评判他们正在进行的工作。
从某种意义上说,不安全感是人性的一部分,没有人喜欢被批评,尤其是害怕自己正在做的事情被别人批评。
天才神话是一种趋势,人类喜欢把团队的成功归于一个人或某位领导者。
许多工程师的内心深处都希望自己被看做是天才。
天才仍然会犯错误,拥有杰出的创意和精湛的编程技巧并不能保证你的软件会大受欢迎。
天才绝不是混蛋的借口,不论天才与否,社交能力差的人往往都是糟糕的队友。
特别是在谷歌这样的公司,决定你职业生涯的其实是与人协同的工作能力。
当我知道别人正在审视我之前所做的工作时,会感到强烈的不安全感,就好像他们马上就会来批判我是个白痴似的。
这是大部分程序员都会有的感觉,自然的反应是隐藏起来,工作,工作,工作,然后完善,完善,完善,确信没人会看到他们的错误为止,等到工作完成时,才有机会让自己的杰作公之于众。
躲起来,直到代码变得完美为止。
另一个隐藏工作的动机是担心别的程序员会窃取自己的想法并执行,保守秘密,就能把控这个想法在自己手中。
软件开发工作是需要高度专注和独处时间的智力工作,但不代表把所有时间都花在独自工作上。
软件开发工作的协同和评审非常重要,可以避免个人独自工作所带来的不必要的麻烦,也可避免个人独自工作时自以为是的潜力增长假象。
如果你把自己所谓的伟大想法隐藏在内心深处,并在实施之前拒绝与任何人沟通或展示,那就是在冒险。
很可能你在早期就犯了一些基本的设计错误。
这就是为什么人们在跳入深水之前先把脚浸在水中的原因:你需要确定你正在做正确的事情,你正在以正确的方式做以前从未做过的事情。
早期失误的概率很高,越早征求反馈,就越能降低这种风险。
记住这句经过无数人验证的至理名言:早失败,快失败,常失败。
早期的分享不仅仅是防止个人失误,而且还可以让你的想法得到验证,更重要的是还可以提升项目的巴士系数。
巴士系数,指多少关键开发者被巴士撞了会让项目停摆。
如果你是唯一一个了解原型代码工作原理的人,你可能会享受到很好的工作保障。
但是如果你被公交车撞了,这个项目就完蛋了。
如果有同事和你一起工作,你的巴士系数就翻了一番。
如果你有一个小团队来共同设计和开发原型,巴士系数会变得更高,当有团队成员离开时,项目也不会停摆。
在软件开发的过程中,我们必须确保每个责任领域除了有一个主要和次要的责任人以外,至少有良好的文档,这有助于项目成功和提升巴士系数。
希望大多数工程师都能认识到,与其成为一个失败项目的关键部分,不如成为一个成功项目的一部分。
除了巴士系数,还有总的工作进展问题。
工程师有时候总喜欢单干,但忽略了一个问题,单干不仅会增加工作强度,其实比想象中的速度慢很多。
与他人一起工作可以更快的获取集体智慧。
一般来说,程序员在紧凑的反馈中效率是最高的:写一个新函数,编译;添加一个测试用例,编译;重构一些代码,编译。
这样,在代码生成后,我们就能够尽快发现并修复错误。
这可以帮助我们时刻保证编写的代码是高质量的,而且确保软件正在向正确的方向一点一滴的有所进展。
目前 DevOps 理念明确指出了这些帮助提升技术生产力的目标,尽早获得反馈 ,尽早进行测试,尽早考虑安全和生产环境。
我们发现问题的时间越早,修复问题的成本就越低。
足够多的眼睛就可以让所有问题浮现,足够多的眼睛可以确保项目一直在正确的轨道上开展。
一直隐藏在洞穴默默工作的人们,当他们清醒过来时才发现,尽管他们最初的设想已经完成了,但是世界已经改变,他们正在做的事情已经变得不再重要了。
你可以利用大量的不受干扰的时间聚焦做某一件事情,但如果你用这些时间来做错误的事情,那就是在浪费时间。
我们还是认为工程师需要拿出持续不间断的时间专注于编写代码,但是同时也需要他们与团队进行连接。
如何找到正确的平衡点是一门艺术。
独自工作比他人一起工作本身更具有危险性。
即使你害怕别人偷走你的想法或担心自己不够聪明,你更应该担心的是可能会浪费大量的时间在错误的事情上。
在编程领域,孤独的工匠是极其稀有的,即使他们确实存在,他们也不会在真空环境中实现超人的成就,他们之所以可以改变世界,一定是灵感火花加上英雄团队努力的结果。
任何可以改变世界或让用户为之兴奋的软件都不是通过个人偷偷躲起来工作而完成的。
你需要跟其他人一起工作,分享愿景,安排工作分工,向别人学习,组建一支优秀的团队。
高效运作的团队是成功的关键。你应该为此而努力追求。
3. 知识共享
一个组织往往比某个网上的人更了解组织自身的问题,而且,它也应该能够回答它自己的大部分问题。
为了达到这一点,你既需要知道哪些专家能够解决这些问题,也需要知道如何将他们的知识分发出去的机制。
在组织内共享专业知识并非易事,没有强大的学习文化,就会遇到各种各样的挑战。
如果专家总是自己做每件事,不花时间通过指导或知识文档来培养新的专家,问题会变得很严重。
这种情况下,知识和责任将持续积累在那些已经拥有专业技能的人身上,而新的团队成员或新手只能靠自己一点一滴的积累来提升。
软件工程的核心是人,代码是人的重要产出,但只是构建产品过程中的一小部分。
代码不会从无到有,人的专业技能也不能凭空就拥有。
每个人在成为专家之前都是新手,一个组织的成功取决于人员的成长和投资。
文档化的知识可以更好的在团队甚至整个组织内部继承和流动。
但是,尽管书面文档比面对面交流的知识传播规模更大,但也要考虑一些权衡利弊。
比如书面文档可能更通用,但不适合个别学习者的场景,而且还有维护成本,需随时保持信息的相关性和动态更新。
部落知识就是存在于单个团队成员脑袋中但未文档化的知识。
如果我们可以面对面访谈这些专家,记录并维护这些知识,就可以让任何查阅到这些文档的人获取这些知识。
书面知识有规模化优势,可以让更多人获得这些知识,但通过访谈目标人群获取他们脑袋中的知识也非常有必要。
因为专家有能力综合归纳他们脑中广袤的知识体系,他们可以评估哪些信息适用于哪种使用场景,确定文档之间的关联性,并知道在哪里可以找到这些知识。
如果他们不知道在哪里找到答案,但至少知道谁曾经做过类似的事情。
部落知识与文档知识相辅相成,即使是拥有完美文档的完美专家团队,也需要与其他团队进行沟通、协调,并随时调整学习策略。
组织内部的培训需聚焦员工的学习和成长,以建立稳定的专家队伍。
要学习,首先你必须承认有些事情不明白。我们应该欢迎这样的诚实,而不是惩罚。
谷歌做的相当好,但有时也会有一些工程师,不愿意承认他们不理解某些事情。
群体交互模式:
作为领导者,要树立这种风范:不要错误的把 “年长” 和 “无所不知” 等同起来。
事实上,你知道的越多,就越是知道更多自己不知道的事情。
公开提问或承认自己在哪方面的知识是空白的,是非常好的行为,这样做对团队中的其他成员也会起到示范作用,让他们觉得这样做没有问题。
回答问题时,耐心和善意会培养出一种让人们寻求帮助时感觉安全的环境。
学习不仅仅是理解新事物,它还包括理解现有设计和实现背后的决策缘由。
切斯特森围栏(Chesterson's Fence)原则:在去除或改变某事物之前,首先了解它为什么会在那里。
如果你不知道它的用途,我肯定不会让你把它拆了。你去查查它的用途,之后我可能会允许你拆掉它。
谷歌历来有举办内外部技术讲座与课程的文化。
确保文档具有反馈机制,如果读者没有一个简单直接的方式来指出文档已经过时,或不准确,他们很可能会因为麻烦而不愿意告诉任何人。
如果这样的话,下一个新手还会遇到同样的问题。
如果大家觉得有人会注意到并考虑他们的建议,他们就会更愿意做更新。
在谷歌,工程师们可以直接从文档本身提交一个文档缺陷。
鼓励工程师记录他们的工作可能是困难的,编写文档需要花费时间和精力,而这些工作所带来的好处不是立竿见影的,只是为其他人提供了方便。
但对于这个组织来说是件好事,因为多数人可以从少数人的时间投资中获益。
但如果没有好的激励,这种鼓励行为很难继续。
规范的信息源是集中的,全公司范围的信息库,它提供了一种标准化的知识传播方法。
谷歌会在全公司范围内给所有工程师发送时事资讯,包括工程新闻(EngNews)、隐私/安全新闻(Ownd)和谷歌热点。
4. 平等工程
将软件工程组织本身打造成符合我们产品目标人群的结构。
仅有计算机科学学位的程序员团队,不足以实现包容且平等的工程。
一个杰出的工程师的标志之一,就是能够理解产品如何使不同群体的人获利或者受损。工程师应该有技术才能,但他们也应该有敏锐的洞察力,知道何时该做什么,何时不该做什么。这种洞察力是一种能够识别和拒绝可能导致不良结果的特性或产品的能力。这是一个崇高而又困难的目标,因为在成为高绩效工程师的道路上充斥着大量的利己主义思想。
在成为一名杰出的工程师的过程中,必须了解在不造成伤害的情况下行使权利所需的内在责任,这一点至关重要。
无论是开发软件,还是软件组织的发展,都需要团队的共同努力。随着软件组织的扩大,它必须对其用户群做出响应,并进行充分的设计。在当今互联的计算世界中,用户群涉及本地和世界各地的每个人。我们必须付出更多的努力,使设计软件的开发团队和他们所生产的产品,都能反映出一种价值观,即很多个多样且包容的用户群。而且如果一个工程组织想要扩大规模,它不能忽视弱势群体;这些代表弱势群体的工程师不仅增强了组织本身,他们还为软件的设计和实现提供了独特且必要的视角,这对整个世界都是真正有用的。
5. 团队领导的艺术
经理(Manager)是管人的,而技术主管(Tech Lead)则是管技术的。
没有船长的船只不过是一个漂浮的候船室,除非有人抓住方向舵并启动引擎,否则它只会随波逐流。一个软件就像那艘船,如果没有人驾驶它,你会被一群工程师浪费宝贵的时间,只能坐等奇迹发生(或者更糟的是,仍然在编写你不需要的代码)。
工程经理负责团队中每个人的绩效,生产力和幸福感,包括他们的技术主管,同时确保他们负责的产品满足业务需求。
一个团队的技术主管(TL)经常向该团队的经理汇报,负责产品的技术方面,包括技术决策和选择,架构,优先级,速度和项目管理。
在小型和新生的团队中,工程经理需要强大的技术技能集,默认情况下通常有一个 TLM:一个能够同时兼顾团队人员和技术需求的人。
在谷歌,大型,成熟的团队通常会有一堆搭档领导者,一个 TL 和一个工程经理作为合作伙伴一起工作。理论上说,要同时做好这两项工作而不完全筋疲力尽是非常困难的,所以最好有两位专家专注于各自的角色。
人们普遍认为,你可以给那些向你汇报工作的人安排工作,但当你需要让组织之外的人去做你认为需要做的事情时,情况就不同了。这种 “非职权影响力” 是你可以培养的最强大的领导特质之一。
如果你的产品想要有所进展(无论向哪个方向发展),那么,无论是否被正式任命,总要有个人站出来把握它的方向。假如你恰好是那种有动力且没有耐心的类型,很可能这个人就是是你。你可能会发现,自己已经卷入了帮助团队解决冲突,做出决策和协调人员的工作中。这种情况经常发生,而且经常是在无意中发生的。也许你从未想过成为一个 “领导者”,但不管怎样,它还是发生了。有些人将这种情况称为 “经理人炎症”。
彼得原理:在一个等级制度中,每个员工趋向于上升到他所不能胜任的地位。
大多数人都可能遇到过一个不能胜任自己工作的经理,或者是非常不善于管理人的经理。我们知道甚至有些人只在差劲的经理手下工作过。
管理者们似乎染上了一种疾病,那就是,他会忘记他原来的管理者对他做过的所有糟糕的事情,而突然开始用同样的方法来 “管理” 自己的下属。如果不及时治疗,这种疾病会杀死整个团队。
当我第一次称为谷歌的经理时,我从当时的工程总监 Steve Vinter 那里得到了最好的建议。他说,最重要的是,要克制住管理的冲动。
新上任的管理者最强烈的愿望之一就是积极的 “管理” 他们的员工,因为这是管理者应该做的,对吧?但这通常会产生严重的后果。
治疗 “管理” 疾病的方法是,充分运用 “仆人式领导”,这是一种很好的说法,作为一个领导者,你能做到最重要的事情就是为你的团队服务,就像一个管家照顾一个家庭的健康和幸福一样。作为一个仆人式的领导者,你应该努力营造一种 谦虚 和 信任 的氛围。这可能意味着消除团队成员自己无法消除的官僚障碍,帮助团队达成共识,甚至在团队工作到很晚时为他们买晚餐。仆人式领导者为他们的团队清理障碍,铺平道路,并在必要时提出建议,除此之外还不怕做一些脏活,累活。仆人式领导所做的唯一管理就是 “管理团队的技术和社交健康”,尽管纯粹关注团队的技术健康可能是诱人的,但团队的社交健康同样重要(但往往你难以管理)。
传统的经理担心如何完成任务,而伟大的经理关心的是要完成什么任务(并相信他们的团队能想出如何完成任务)。
我把 Jerry 当成人一样对待,他总是把工作做完,我从来不必担心他是否在办公桌旁,因为他不需要保姆来监督他完成工作。如果你的员工对他们的工作如此不感兴趣,以至于他们实际上需要传统的经理保姆来说服他们工作,那才是你真正的问题。
扬善于公庭,规过于私室。
你应该努力雇佣比你更聪明,可以取代你的人。这可能很困难,因为这些人经常会挑战你(另外,当你犯错时,他们可能还会怼你)。当然,这些人也会让你惊讶,干出很多伟大的事情。他们将能更好的驱动自己,有些人也会渴望领导团队。你不应该把这看作是企图篡夺你的权力。相反,你应该把它看成是一个机会,让你有精力去领导一个更大的团队,寻找新的机会,甚至可以去度个假,而不用担心每天都要检查团队是否完成了工作。这也是一个学习和成长的好机会,当周围都是比你更聪明的人的时候,你更容易扩展自己的专业知识。
人的方面是编写软件最具挑战性的部分,但与人打交道最困难的部分是处理不符合期望的人。有时,人们会因为时间不够长或不够努力而不符合期望,但最困难的情况是,无论它们工作多久或多么努力,都没有能力完成工作。
事实上,团队非常清楚谁是低绩效的员工,因为团队不得不拖着他们前行。忽视低绩效员工,不仅阻碍新的高绩效员工加入团队,还会导致现有高绩效员工的离职。你最终会得到一支表现欠佳的团队,因为这些人根本不会自愿离开团队。最后,你把低绩效员工留在团队里,也不会给他们带来任何益处;在你的团队中表现不好的人,实际上在其他团队很可能会表现不错。
尽快与低绩效员工沟通的好处是,你可以很好的帮助他们。如果你立即与一个低绩效员工沟通,你经常会发现他们只需要一些鼓励或指导就可以进入更高的生产力状态。如果你等太久才去与一个低绩效员工沟通,他们与团队的关系就会变得很糟糕,你也会非常沮丧。到这时候,你可能已经无法帮助他们了。
如何有效的指导低绩效员工呢?最好的类比是,想象你正在帮助一个一瘸一拐的人再次学会走路,然后慢跑,然后和其他人一起跑步。它几乎总是需要一些临时的微观管理,当然仍然需要谦虚,尊重和信任的态度,尤其是尊重。设定一个具体的时间框架(比如说两个月),以及你希望他们在这段时间内实现的某些具体的目标。把这些目标定得小,渐进且可衡量,这样就有机会取得很多小的成功。每周与团队成员会面,检查进展情况,并确保围绕每个即将到来的里程碑,设定明确的期望,以便很容易衡量成功与否。
一个经理对他们的团队有两个主要的关注领域:社交和技术。在谷歌,经理们在技术方面更强是很常见的,因为大多数经理都是从技术岗位晋升的(他们的主要工作目标是解决技术问题),他们往往会忽视人的问题。
如果你因为不信任而对团队进行微观管理,那其实是你招聘的失败,招聘一些连你自己都不信任的人。
在任何团队中,自尊心太强的人都是很难处理的,尤其是团队的领导者。相反,你应该努力培养一种强大的团队自我意识和认同感。
如果你能够很肯定那些在战壕里工作的人比你更了解他们工作的细节,那么这意味着,你会认同,你可能是推动团队达成共识并帮助确定方向的人,但如何实现目标的具体细节,最好由生产产品的人来决定。这不仅使他们有更强的主人翁意识,而且使他们对产品的成功(或失败)有更强的责任感。
大多数刚开始担任领导职务的人都觉得自己肩负着巨大的责任,要做好每一件事,了解每一件事,掌握所有的答案。我们可以向你保证,你不可能把每件事都做好,也不可能知道所有的答案,如果你表现得像这样,你很快就会失去团队的尊重。
人们对那些在犯错时道歉的领导人有着极大的尊重,而且与普遍的看法相反,道歉并不会让你变得脆弱。事实上,当你道歉时,人们通常会尊重你,因为道歉告诉人们你头脑冷静,善于评估情况,并且谦虚。
当你领导的团队更大时,调节你的反应和保持冷静,就变得更加重要,因为你的团队会(有意或无意的)从你身上寻找行动和应对周围发生的任何事情的线索。
形象一点来说,将公司的组织结构图视为一个齿轮链,每个贡献者都是一个一端只有几颗齿的小齿轮,而他们上面的每个经理都是另一个齿轮,最后 CEO 是一个有几百颗齿的最大齿轮。这意味着,每当一个 “经理齿轮”(可能有几十个齿)转一圈,“个人齿轮” 就转两三圈。**而 CEO 的一个小动作就能让倒霉的员工,在六七个齿轮的链条末端,疯狂的旋转!**你越是处在链条的前端,就可以越快的让你下面的齿轮旋转,无论你是否有意为之。
领导者总是在聚光灯下。这意味着如果你处于一个公开的领导地位,你总是被监视着,不仅是在主持会议或演讲时,甚至只是坐在办公桌前回复电子邮件时。你的同僚在观察你的肢体语言,你对闲聊的反应以及你吃午饭时的举动。他们读到的是自信还是恐惧?作为一个领导者,你的工作是激励,但激励是一项 7✖️24 小时全天候的工作。你对任何事情的明显态度,无论多么琐碎,都会在不知不觉中被注意到,并传染给你的团队。
另外一个禅宗管理技巧:提问。当一个团队成员向你征求意见时,这通常是相当令人兴奋的,因为你终于有机会解决一些问题了。这正是你在担任领导职位前多年所做的,所以你通常会跳进解决方案模式,但这是你最不该去的地方。寻求建议的人通常不想让你解决他们的问题,而是想让你帮助他们解决问题,最简单方法就是问这个人问题。你可以表现出一些谦虚,尊重和信任,试着通过提炼和探索问题来帮助人们自己解决问题。这通常会引导员工找到答案,它也会成为员工自己的答案。不管你是否有答案,使用这种技巧几乎总是会给员工留下你有答案的印象。
在许多情况下,认识正确的人比知道正确的答案更有价值。你并不需要知道所有的答案,但你通常可以帮助找到消除障碍的人。
如果你想让团队朝一个方向快速前进,你需要确保每个团队成员都理解并同意这个方向是什么。如果你有明确的目标,你需要设定明确的优先级,并在时机到来时帮助团队,令其知道应该如何做出权衡。
设定一个明确的目标,并让你的团队朝着同一个方向推动产品,最简单的方法是为团队创建一个简洁的使命声明。在你帮助团队确定了方向和目标之后,你可以退一步,给团队更多的自主权,定期检查以确保每个人都在正确的轨道上。这不仅可以使你腾出时间来处理其他领导任务,还可以极大的提高团队的效率。
我不会对你撒谎,但是当我不能告诉你一些事情,或者我真的不知道的时候,我会告诉你。如果一个团队成员找你谈一些你不能分享的事情,你可以告诉他们你知道答案,但是不能随便说。更常见的是当一个团队成员问你一些你不知道答案的问题时,你可以告诉那个人你不知道。
当你提供直接的反馈或批评时,你的表达方式是确保信息被听到而不被曲解的关键所在。如果你让接受者处于守势,他们不会考虑如何改变,而是考虑如何与你争辩,向你表明你错了。
作为一个领导者,有一种方法可以让你的团队长期保持高效(以及团队稳定),那就是花些时间衡量他们的幸福感。追踪团队幸福感的一个简单方法是,在每次一对一沟通结束时问你的团队成员 “你需要什么吗”,这个简单的问题是一个很好的总结方式,可以确保每个团队成员都拥有他们所需的东西。如果你每次一对一的时候都问这个问题,你会发现最终团队会记住这一点,有时甚至会给你一份清单,上面列出了让每个人的工作都更好的事情。
追踪团队成员幸福感的一个重要部分是追踪他们的职业发展。
在未来五年里,通常每个人都想做几件事:升职,学习新东西,从事一些重要的工作,以及与聪明人一起共事。不管他们是否用语言表达,大多数人都在思考这个问题。如果你想成为一个有效的领导者,就应该考虑如何帮助实现所有这些事情,并让团队知道你正在思考这个问题。
作为领导者,你的工作是确定谁需要什么,然后给他们,你的团队需要不同程度的激励和方向。为了让所有团队成员都得到他们需要的东西,你需要激励那些墨守成规的人,并为那些分心或不确定该做什么的人提供更强有力的方向。当然,也有些人 “飘忽不定”,既需要激励,也需要方向。因此,有了这种激励和方向的结合,就能让团队快乐并富有成效。
Dan Pink 在《驱动力》一书中解释说,让人们成为最快乐,最有成效的人的方法不是外在的激励他们(例如,向他们仍一堆现金),相反,你需要努力增加他们的内在动机。
你可以通过给人们三样东西来增加内在动机:自主,专精 和 目的。
当一个人有能力独立行动,而不需要别人对他进行微观管理时,他就拥有自主权。有了自主权的员工,你可能会给他们提供需要开发产品的自主方向,但让他们自己决定如何实现。这不仅有助于激励他们,因为他们与产品的关系更密切,还因为这让他们对产品有更强的主人翁精神。他们对产品成功越有自主权,他们就越希望看到产品成功。
专精意味着需要给别人机会来提升现有技能并学习新技能。提供足够的机会让员工专精,不仅有助于激励他们,还能让他们随着时间推移变得更好,从而形成更强的团队。
6. 大规模团队领导力
你会为失去这些细节而悲伤,你开始意识到你以前的工程专业知识与你的工作变得越来越不相关。相反,这时你的效率比以往任何时候都更依赖于你的技术直觉和激励工程师的能力。
三个 “总是” 的领导力:
管理一个团队的团队,意味着在更高层次做出更多的决策。你的工作更多的是关于高层战略,而不是任何解决任何具体的工程任务。在这个层次上,你所做的大多数决定都是关于找到正确的折中方案。
作为一个领导者,你需要决定你的团队每周应该做什么。在最更层面上,不管是一个团队还是更大的组织的领导者,你的工作是引导人们去解决那些困难,模糊不清的问题。所谓模糊不清,是指这个问题没有明显的解决方案,甚至可能无法解决。无论哪种方式,都需要对问题进行探索,引导,并努力使之处于可控状态(希望如此)。如果写代码类似于砍树,那么作为领导者,你的工作就是 “透过树木见森林”,找到一条穿过森林的可行路径,引导工程师们前往那些重要的树木。这一过程有三个主要步骤:首先,你需要识别盲点;其次,你需要识别权衡;然后,你需要决策并迭代解决方案。
你可以用这些信息为当前这个月作出最佳决策。下个月,你可能需要再次重新评估和重新平衡这些利弊,这是一个迭代过程。这就是我们所说的 “总是在决策” 的意思。
这里有一个风险。如果你没有将巩固你做流程设计成适合迭代进行的,那么,你的团队可能会陷入寻找完美解决方案的陷阱,这可能导致 “分析瘫痪”。你需要让你的团队适应迭代。
你的工作不仅仅是解决一个模糊不清的问题,同时更是让你的组织在没有你在场的情况下自行去解决它。如果你能做到这点,它将使你有更多的精力去面对一个新的问题(或新的组织),从而留下了一条让团队自组织自管理的成功之路。当然,这里的反模式是你将自己设置为单点故障的一种情况。
作为试金石,你团队正在处理一个难题,目前进展良好。现在想象一下,你,领导者,突然消失了。你的团队会继续前进吗?它会继续取得成功吗?
想想你上次休假,至少花了一个星期时间。你是否一直在检查你的工作邮件?问问你自己为什么。如果你安心休假,天会塌下来吗?如果是这样,你很有可能让自己成为了一个单点故障。你需要解决这个问题。
你的使命是,建立一个 “自驱” 的团队。成为一个成功的领导者意味着要去建立一个能够自己解决困难问题的组织。这个组织需要一组强大的领导者,健康的工程流程,以及一种积极的,自我延伸的文化。
构建这种自信的群体有三个主要部分:划分问题空间,授权子问题,根据需要进行迭代。
具有挑战性的问题通常由困难的子问题组成。如果你领导一个团队的团队,一个明显的选择就是让每一个团队负责其中的一个子问题。然而,风险在于子问题会随着时间而改变,而僵化的团队边界将无法注意到货适应这一事实。如果你有能力,请考虑一个更松散的组织结构,其中子团队可以改变规模,个人可以在子团队之间迁移,分配给子团队的问题可以随着时间的推移而变化。这需要在 “过于僵化” 和 “过于模糊” 之间把握好分寸。一方面,你希望你的团队有一个清晰的问题感,目标感和稳定的成就感。另一方面,人们也需要自由度去调整方向,并通过尝试新事物来应对变化的环境。
作为一个领导者,你的盘子里总是摆满了需要完成的重要任务。这些任务中的大多数对你来说都相当容易。假设你正在辛勤的处理你的收件箱,回复问题,然后决定留出 20 分钟的时间来解决一个长期困扰你的问题。但在你执行任务之前,请稍微停下来思考一下。问自己这样一个问题:我真的是唯一可以完成这项工作的人吗?
当然,你亲自动手可能是最有效率的,但是那样你就无法培养你的领导者,你不是在建立一个自立的组织。除非任务确实是时间紧迫火烧眉毛,否则就忍耐一下,把工作交给别人去做吧,大概你认识的某个人可以完成任务,单可能需要花更长的时间才能完成。如果需要的话,请指导他们的工作。你需要为你手下的领导者创造成长的机会;他们需要学会 “突破自我”,并自己完成这项工作,这样你就不会一直处在关键路径上了。
作为领导者的领导者,你需要牢记自己的目标。如果你发现自己陷入各种琐事之中,那你就是在给组织帮倒忙。当你每天开始工作时,都问自己一个关键的问题:我能做什么事我团队中其他人做不了的事?
有很多好的答案。例如,你可以保护你的团队免受组织政策的影响;你可以给予他们鼓励;你可以确保每个人都能和睦相处,从而营造一种谦虚,信任和尊重的文化。“向上管理” 也很重要,要确保你的管理链了解你的团队正在做什么,并和整个公司保持连接。
你正在构建一个蓝图,说明如何解决模糊不清的问题,以及你的组织如何随着时间推移管理这个问题。你不断的绘制森林的地图,然后把砍树的工作分配给其他人。
让我们假设你现在已经达成了目标,你已经建立了一个自运转的机器。你不再是个单点故障了。恭喜你!那么现在你该做什么了?
“现在做什么呢?”,简单的答案是指挥这台机器,并保持它的健康。但除非出现危机,否则你应该只是点到为止。
一个深思熟虑的调整可以产生巨大的影响。我们在管理人员的时候可以使用这种方法。请想象我们的团队像一个大飞艇一样飞行,缓慢而坚定的朝某个方向前进。我们花一周的时间仔细观察和倾听,而不是微观管理和尝试不断修正航向。周末的时候,我们在飞艇的某个精确的位置上做了一个小的粉笔记号,然后用了很小的力,在这个关键位置上 “轻轻一拍”,便调整了航向。
好的管理就是这样:95% 的观察和倾听,5% 在正确的地方进行关键的调整。
一个常见的错误是让一个团队负责特定的产品,而不是一个一般性的问题。产品是问题的解决方案。解决方案的寿命可能很短,并且产品可以被更好的解决方案替代。不过,一个问题如果选的好的话能够经久不衰。将团队身份固定到特定的解决方案,会随着时间的推移导致各种焦虑。团队会盲目的固执己见,因为解决方案已经成为了团队身份和自我价值的一部分。如果团队变成了负责一个问题,那么随着时间的推移,团队就可以腾出手去尝试不同的解决方案。
作为领导者,你最宝贵的资源是有限的时间,注意力和精力。如果你在积极构建团队职责和权力的过程中,没有学会保持头脑清醒,那么扩大规模注定要失败。
请记住,作为领导者,你的工作就是做那些只有你能做的事情。
周末不是假期,至少花三天时间 “忘记” 你的工作,至少花一周时间来彻底恢复精力。只有当你真正懂得如何切断与他人的联系时,你的假期才会充满乐趣。
有时候,无缘无故,你就过了糟糕的一天。你可能睡的很好,吃的很好,做了运动,但你仍然处于糟糕的情绪中。如果你是一位领导者,这是一件可怕的事情。你的坏情绪会给周围的人定下基调,并导致糟糕的决定。如果你发现自己正处于这种情况,就转身回家,宣布请一天病假。当天什么也别做,总比造成主动伤害好。
7. 度量工程生产力
在软件工程领域,谷歌发现当公司规模扩张时,拥有一支专注于工程生产力的专家团队本身就是非常有价值和重要的,并且能够利用来自这样一个团队的深刻见解,从而使公司收益。
随着组织规模的线性增长,沟通成本呈几何级数上升。为了扩大业务范围添加更多的人是很有必要的,但是,当增加额外的人员时,沟通成本并不是线性增长的。
因此,将无法根据工程组织的规模按比例线性的扩展业务范围。
不过,还有另一种方法可以解决我们的业务扩展问题:我们可以让每个人更有生产力。如果我们能够提高组织中每个工程师的个人工作效率,我们就能扩大业务范围,而不必相应的增加沟通开销。
谷歌的新业务迅速成长,这意味着要学会如何提高我们工程师的工作效率。
要做到这一点,我们需要了解是什么让他们高效,识别在我们的工程过程中低效的地方,并修复识别出的问题。
然后,我们将根据需要重复这个循环,进行持续改进。
通过这样做,我们将能够随着对业务需求的增加而扩展我们的工作组织。
然而,这种改进周期也需要人力资源。如果你的工程组织每年要花费 50 个工程师来理解和修复生产力存在的问题,那么每年提高相当于 10 个工程师的生产力,这种改进就是不值得的。因此,我们的目标不仅仅是提高软件工程的生产力,而且是高效的做到这一点。
在我们决定如何度量工程师的生产力之前,我们需要知道什么时候一个指标是值得度量的。
度量本身很昂贵:它需要人们度量工作流程,分析结果,并将结果发布给公司的其他部门。
此外,度量过程本身可能很繁重,并减缓了工程组织的其他工作。
在谷歌,我们提出了一系列问题来帮助团队确定是否值得一开始就度量生产力。
我们首先要求人们以具体的形式来描述他们想度量什么。
我们发现,人们越能具体的描述这个问题,他们就越有可能从这个过程中受益。
然后我们请他们考虑以下各方面的问题:
通过询问这些问题,我们发现,在很多情况下,做度量时不值得的,这很好!
有很好的理由不去度量工具或流程对生产力的影响。
当成功度量软件过程时,并不是着手证明一个假设正确或不正确;成功意味着,给利益相关人决策所需的数据。
如果该利益相关人不使用数据,那么项目常常是失败的。只有当基于度量结果能够做出具体的决策时,我们才应该去度量软件的过程。
在我们决定度量一个软件过程之后,我们需要确定使用什么度量指标。显然代码行数不合适。
在谷歌,我们使用 **目标/信号/指标 框架(GSM Goals/Signals/Metrics)**来指导指标创建。
路灯效应:如果你只去有路灯照亮的地方找你想找的东西,那你可能找错了地方。
如果只使用那些我们易于理解且易于度量的指标,而不管这些指标是否适合我们的需求时,就会出现这种情况。
GSM 迫使我们思考哪些指标真正能帮助我们实现目标,而不是简单的考虑我们有什么现成的指标。
对于每个度量指标,我们应该能够追溯到它要充当代理的信号,以及它试图度量的目标。
这样可以确保我们知道我们要度量哪些指标以及为什么要度量它们。
8. 风格指南与规则
制定规则的目的是鼓励好的行为和劝阻坏的行为。不同组织对 “好” 和 “坏” 的理解不同,这取决于组织关心什么。
好和坏并没有一个普世的标准;好与坏是主观的,是根据需要而量身定制的。
随着一个组织的发展,既定规则和指导方针形成了编码的通用词汇表。
共同的词汇表使工程师能够集中精力于他们的代码需要表达什么,而不是如何表达。
通过塑造这些词汇表,工程师们往往会自觉地,甚至是下意识的去做 “好的事情”。
因此,规则给了我们广泛的杠杆作用,将共同的发展模式推向期望的方向。
在定义一套规则时,关键问题不是 “我们应该拥有哪些规则?”,我们要问的问题是,“我们要达成什么目标?”。
当我们关注规则所服务的目标时,识别哪些规则支持这个目标,可以更容易的提取出一组有用的规则。
我们规则的目标是管理好开发环境的复杂性,保持代码库的可管理性,同时保证工程师高效的工作。
我们的另一个原则是为代码的读者优化,而不是为作者优化。
考虑到时间的推移,我们的代码被阅读的频率远远高于它被修改的频率。
我们宁愿编写代码时令人厌烦,也不愿它难以阅读。
我们看重 “简单读” 而不是 “简单写”。
如果工程师必须反复的为变量和类型输入可能更长但更具有描述性的名称时,虽然前期成本会更高,但是我们选择为所有未来的读者提供的可读性而付出这样的成本。
作为重要的一部分,我们还要求工程师在代码中留下明确的预期行为证据。
我们希望读者在阅读代码时,能够清楚的了解代码在做什么。
大多数风格指南包含关于注释的要求,注释也是为了支持读者快速理解代码的目标而设计的。
文档注释(加在给定文件、类或函数前面的块注释)描述了随后代码的设计或意图。
实现注释(在代码本身中散布的注释)证明或强调不明显的选择,解释一些棘手的问题,并强调代码的重要部分。
我们有涵盖这两种注释的风格指南规则,要求工程师必须提供其他工程师在阅读代码时可能需要的解释。
一致性使任何工程师即使进入代码库中不熟悉的部分,也能相当迅速的开始工作。
一个本地项目可以有其独特的个性,但其工具是相同的,技术是相同的,库也是相同的,而且都可以很好的工作。
一致性带来的好处远远大于我们失去的创作自由。
代码也是如此,保持一致性有时可能会感到受到限制,但这意味着更多的工程师用更少的精力完成更多的工作。
当代码库的风格和规范在内部保持一致时,编写代码工程师和其他阅读代码的人,可以更关注代码要完成什么,而不是代码的呈现方式。
在很大程度上,这种一致性可以让专家通过代码组合来降低认知成本。
当我们用相同的接口解决问题,并以一致的方式呈现代码时,专家们更容易浏览这些代码,专注于重要的内容,并理解它在做什么。
一致性还使得模块化代码和发现重复变得更容易。
一致性是对规模化的有力支持,工具是组织扩展的关键,代码的一致性使得构建那些能够理解、编辑和生成代码的工具变得更加容易。
如果每个人都有少量不同的代码,那么依赖于一致性的工具的全部好处就无法应用。
当每个人都使用相同的组件,每个人的代码都遵循相同更多结构和组织规则时,我们就可以投资到统一的工具建设上,这些工具为我们的很多维护任务建立起自动化的能力。如果每个团队需要分别投资同一工具的定制版本,根据他们独特的环境进行定制,我们将失去这种优势。
一致性也有助于扩展组织的人力,随着一个组织的增长,在代码库上工作的工程师数量也在增加。
尽可能的保持每个人正在编写的代码的一致性,可以更好的现在项目之间流动,最大限度的减少工程师切换团队的学习成本,增强组织在人员需求波动时,灵活调整和适应的能力。
我们主张一致性的层次结构:“保持一致性” 从局部开始,即给定文件内的规范先于给定团队的规范,然后是大型项目的规范,最后是整个代码库的规范。
鉴于我们追求的软件长生命周期和规模化的能力,我们认识到事情需要改变,因此我们创建了一个更新规则的流程。
9. 代码评审
代码评审是一个由作者以外的人评审代码的流程,通常在将代码引入代码库之前进行。
在谷歌,基本上每个变更在提交之前都要经过评审,每个工程师都要负责发起评审和评审变更。
代码评审通常需要一个流程,以及支持该流程的工具。
重要的是要记住(并接受)代码本身是一种负担。这可能是一个必要的负担,但就其本身而言,代码对于某个人来说知识一个维护任务。
很像飞机携带的燃料,它有重量,当然,它是飞机飞行所必需的。
当然,新特性通常是必需的,但是在开发代码之前,应该首先注意确保任何新特性都是必要的。
重复的代码不仅浪费了精力,实际上它比没有代码花费更多的时间。当代码库中有重复代码时,变更的执行通常需要更多的工作量。
10. 文档
由于文档的受益者是下游用户,所以通常不会给文档作者带来直接的好处。
文档需要更多的前期投入,而且需要很久以后才能为作者带来明显的收益。
但是,就像在测试上的投资一样,在文档上的投资会随着时间的推移而产生回报。
毕竟,你仅仅写一篇文档,但它会被阅读成百上千次,它的最初成本被摊销给所有未来的读者。
文档会随着时间推移而扩展,而且对于组织中的其他人员来说,扩展文档也是至关重要的。
它可以帮助回答以下问题:
有些工程师觉得自己不是个能干的作家。但是,你并不需要精通英语,就能写出可操作的文档。
你只需要走出一点自我,试着从读者角度来看事情。
文档被看做是额外的负担(需要另外去维护的别的事情),而不是使现有代码的维护变得更加容易的东西。
文档会减少其他用户提的问题,这对于编写文档的人来说,节约了不少时间。
如果你必须多次解释某件事,那么写文档这个过程是有意义的。
与测试非常相似,你编写一篇好的文档所付出的努力会让你在一生中获得数倍的收益。
文档会随着时间的推移变得至关重要,并且会随着组织规模的扩大,对特别关键的代码会带来巨大的好处。
工程师越是把文档作为软件开发的必要任务之一,他们就越是不会埋怨写作带来的前期成本和投入,因为好的文档可以让他们获得更多的长期利益。
工程师们在编写文档时所犯的最重要的错误之一,就是只为自己写文档。
这样做是很自然的,而为自己写文档也并非没有价值。
相反,在开始写作之前,你应该确定文档的目标读者。可以尝试先确定一个初级读者,并为这个读者写作。
只需要用读者期待的表达方式和风格写就可以了,只要你能读,你就能写。
记住,读者站在你曾经站过的地方,但不具备你的新领域知识。
所以,你不需要成为一个伟大的作家,只需要让想你一样的人和你现在一样熟悉这个领域。
在某些情况下,不同的读者需要不同的写作风格,但在大多数情况下,诀窍是以一种尽可能广泛的适用于不同受众群体的方式来写作。
通常情况下,当你需要向专家和新手解释一个复杂的主题时,为那些有领域知识的专家们写作,也许会让你走捷径,但是你会把新手搞糊涂,而向新手详细解释每件事,无疑会惹恼专家。
写这样的文档需要平衡,也没有什么灵丹妙药,然而,我们发现,做适当的平衡有助于你保持文档的简短。
保持足够的描述信息,向不熟悉这个主题的人解释复杂的主题,但不要忽视或惹恼专家类读者。
大多数技术文档都回答了一个 “How” 的问题。这个怎么用?我如何编写这个 API?如何设置这个服务器?
因此,软件工程师倾向于直接跳到文档的 “How” 部分,而忽略与之相关的其他问题:Who,What,When,Where,Why。
11. 测试概述
很长一段时间依赖,软件测试都处在一种状态,需要大量的手动测试,并且容易出错。
然而,自本世纪初依赖,软件行业的测试方法已经发生了巨大的变化,以应对现代软件系统的规模和复杂性。
发展的核心是开发人员驱动的自动化测试实践。
自动化测试可以防止缺陷逃逸,这些缺陷可能会影响到用户。
在开发周期中,一个缺陷被捕获的时间越晚,它的成本就越高,在许多情况下,这个成本呈指数增长。
然而,“捕获缺陷” 只是做自动化测试的一部分动机。另一个同样重要的原因是支持变更的能力。
无论你是添加新功能、进行保持代码健康的重构,还是进行更大的重新设计,自动化测试都可以快速发现错误,这使得有信心的更改软件成为可能。
迭代速度更快的公司,可以更快的适应不断变化的技术、市场条件和客户口味。
如果有一个健壮的测试实践,那就不必害怕变化,可以更好的拥抱变化,因为有基础的质量保障。
越想快的改变系统,就越需要一种快速的方法来测试它们。
编写测试的行为也改进了系统的设计,作为代码的第一个客户,测试可以告诉你关于设计选择的很多信息。
系统与数据库的耦合是否过紧?API 是否支持所需的用例?系统能处理所有的边界用例吗?
编写自动化测试会迫使你在开发周期的早期面对这些问题。
这样做通常会使得软件更加模块化,且有更大的灵活性。
测试的价值来自工程师对它们的新人,如果测试成为生产力的障碍,不断的带来琐事和不确定性,工程师们就会失去信任并开始寻找解决办法。
一个糟糕的测试套件可能比没有测试套件更麻烦。
除了使公司能够快速开发出优秀的产品外,测试对于确保我们生活中重要产品和服务的安全也变得至关重要。
在谷歌,我们已经确定测试并不是事后才考虑的事情,专注于质量和测试是我们工作的一部分。
我们已经认识到,未能将质量融入我们的产品和服务,将不可避免的导致糟糕的结果,有时这是很痛苦的。
因此,我们将测试作为我们工程文化的核心。
不能仅仅依靠程序员的能力来避免产品缺陷,即使每个工程师只是偶尔编写出缺陷,但是当有足够的人在同一个项目上工作之后,你也会被不断增长的缺陷列表所淹没。
假设有一个 100 人的团队,工程师们都非常优秀,每个月每人只写出一个缺陷。
总的来说,这群令人惊叹的工程师每个工作日仍然会产生 5 个新的缺陷。
更糟糕的是,在一个复杂的系统中,修复一个缺陷常常会导致另一个缺陷,因为工程师慢慢的习惯已知的缺陷,并围绕它们写代码。
最好的团队会想方设法将其成员的集体智慧转化成对整个团队的利益,这正是自动化测试的作用。
团队中的工程师编写测试后,它将被添加到其他人可用的公共资源池中。
团队中的其他每个人都可以运行测试,并在检测到问题时受益。
在指导新员工时,我们经常被问到,哪些行为或属性实际上需要测试?直截了当的答案是:测试所有不想被破坏的东西。
换言之,如果想确信某个系统展现出某个特定的行为,那么唯一能使用的方法就是未这个特定行为编写一个自动化的测试。
这包括所有常见的容易出问题的地方,如性能、行为正确性、可访问性和安全性。
碧昂丝规则:如果你喜欢它,你就应该测试它。
碧昂丝规则经常被负责在整个代码库中进行变更的基础设施团队所引用。
如果不相关的基础设施变更通过了所有测试,则产品团队需要修复它,并添加额外的测试。
代码覆盖率是衡量功能代码行被测试的比例。
代码覆盖率通常被视为了解测试质量的黄金标准指标,这有点不幸。
有可能出现这样的情况,那就是用一些测试来运行很多行代码,而从不检查每一行是否做了任何有用的事情。
这是因为代码覆盖率只衡量被调用的代码行,并不是调用的结果。
度量测试套件质量的一个更好的方法是考虑被测试的行为。
请尝试回答 “我们有足够的测试吗?”
代码测试覆盖率可以提供对未测试代码的一些洞察,但它不能代替对系统的测试情况的批判性思考。
我们代码库的开放性鼓励集体所有权,让每个人都对代码库负责。
这种开放性的一个好处是,你能够直接修复你所用到的产品或服务中的缺陷(当然,要经过批准),而不是抱怨它。
这也意味着许多人会对别人拥有的代码的一部分进行变更。
随着代码库的增长,将不可避免的需要对现有代码进行变更。
如果现有代码写的不好,自动化测试会使这些变更变得更困难。
脆弱测试(那些过分指定预期结果或依赖于那些复杂却被广泛使用的样板文件的测试),实际上会阻碍变更。
因为,即使做了不相关的变更,这些写得不好的测试也有可能会失败。
如果你曾经对一个特性做过 5 行修改,却发现了几十个无关的、被破坏的测试,那么你已经感受到了脆弱测试的冲突。
随着时间的推移,这种冲突会使团队不再为保持代码库健康而进行重构。
脆弱测试中一些最糟糕的情况来自对模拟(Mock)对象的误用。
谷歌的代码库遭受过严重的模拟(Mock)框架的滥用,它导致一些工程师宣布 “不再使用模拟(Mock)”。
虽然这是一个强有力的声明,但是理解模拟(Mock)对象的局限性,可以帮助你避免误用它们。
测试套件运行的速度越慢,被执行的频率就会越低,而且它提供的好处也就越少。
如果不能保持测试套件的确定性和运行快速性,那么它将成为提高生产力的障碍。
马桶测试:有人开玩笑的提议在厕所张贴传单,厕所是每个人每天都必须至少去一次的地方,不管是不是开玩笑,这个想法很容易做到。
自动化测试并不适合所有的测试任务。例如,测试搜索结果的质量通常需要人工判断。
通过使用自动化测试来涵盖人们已经充分理解的行为,使人工测试人员能够花费大量的精力和定性的工作,来专注于产品中他们能够为之提供最大价值的部分。
采用开发人员驱动的自动化测试是谷歌最具变更性的软件工程实践之一。
它使我们能够用更大的团队构建更大的系统,比我们想象的要快。
它帮助我们跟上技术变革的步伐。
12. 单元测试
粒度:测试所消耗的资源和允许它做的事情。
范围:测试打算验证多少代码。
我们使用 “单元测试” 一词来指范围相对狭窄的测试,例如,单个类或方法的测试。
除了防止缺陷,测试最重要的目的是提高工程师的工作效率。
与范围更广的测试相比,单元测试具有许多特性,这些特性使它们成为优化生产力的一种极好的方法。
谷歌编写的大多数测试都是单元测试,根据经验,我们鼓励工程师按照 80% 的单元测试和 20% 的范围更广的测试的比例来写测试。
因为单元测试在工程师的生活中,占据了如此重要的部分,所以谷歌非常注重测试的可维护性。
可维护的测试是指 “可工作” 的测试,在编写它们之后,工程师不需要再考虑它们,直至它们运行失败。
而这些失败表明出现了真正的有明确原因的缺陷。
糟糕的测试必须在签入之前修复,以免给未来的工程师带来拖累。
脆弱的测试是指在对生产代码进行不相关的变更,而又不会引入任何真正的缺陷时,但却令其失败的那些测试。
工程师必须诊断和修复此类测试,作为其工作的一部分。
最理想的测试是不变的,在编写之后,除非被测系统的需求发生变化,否则它永远不需要改变。
有两种方法可以验证被测系统是否按预期运行。
通过状态测试,可以观察系统本身,以查看调用后的系统表现。
通过交互测试,可以检查系统是否对其协作者执行了预期的操作序列,协作者又是如何响应调用该系统的。
许多测试是状态验证和交互验证的组合。
交互测试往往比状态测试更脆弱,同样的原因是测试私有方法,比测试公共方法更脆弱。
交互测试检查系统是通过什么样的调用过程得到结果的,而通常你应该只关心结果是什么。
有问题的交互测试最常见的原因是过度依赖模拟(Mocking)框架。
这些框架让测试替身的创建变得很容易,而这些测试替身用来记录和验证针对它们的每个调用,并在测试中,这些测试替身代替了实际对象。
这种策略直接导致脆弱的交互测试,因此,只要真实对象是快速和确定的,我们更倾向于使用真实对象而不是模拟对象。
随着时间的推移,测试清晰度变得更重要。测试通常会比编写测试的工程师存在更长久,并且随着系统的演进,对系统的需求和理解也会发生微妙的变化。
一个失败的测试,完全有可能是由一个已经不在团队中的工程师在多年前编写的,无法确定其目的或如何修复它。
这与不清晰的生产代码形成了鲜明的对比,对于不清晰的生产代码,可以花足够的时间通过查看调用它的代码和删除它时哪里被破坏了,来确定其用途。
但是对于一个不清楚的测试,你可能永远不会理解它的目的,因为删除测试除了(可能)在测试覆盖率中产生一块空白之外,不会产生任何影响。
在最坏的情况下,当工程师们不知道如何修复这些模糊的测试时,这些测试就会被删除。
移除这样的测试不仅会在测试覆盖率中引入一块空白,而且还表明测试在它存在的整个时期(可能是几年)一直没有提供任何价值。
为了使测试套件随着时间的推移能扩展并且有用,该套件中的每个单独的测试都尽可能清晰是很重要的。
帮助测试实现清晰性的两个高层属性是完整性和简洁性。
当一个测试的主体包含了读者为了理解它是如何得到结果而需要的所有信息时,这个测试是完整的。
当测试不包含其他干扰或不相关的信息时,它是简洁的。
一个测试的主体应该包含理解它所需的所有信息,而不包含任何无关的或分散注意力的信息。
与其为每个方法编写测试,不如为每个行为编写测试。
行为是系统在特定状态下如何响应一系列输入的保障。
行为通常可以用 given/when/then 来表示,given 银行账户为空,when 试图从中取款,then 交易被拒绝。
方法和行为之间的映射是多对多的,大多数复杂的方法实现多个行为,有些行为依赖于多个方法的交互。
将测试视为行为而不是方法相耦合会显著影响它们的结构。记住,每个行为都有三个部分:given 定义系统的预设条件,when 定义要在系统上执行的操作,then 验证结果。
当结构明确的时候,测试是最清晰的。一些框架,比如 cucumber 和 spock 直接固定了 given/when/then 的结构。
其他语言可以使用空格和可选注释使结构突出。
测试命名应该概括它正在测试的行为,一个好名字描述了系统正在进行的操作和预期的结果。
如果你陷入困境的话,一个很好的技巧就是尝试用单词 should 来开始测试命名。
如果需要再测试名称中使用 and 一词,那么很有可能你实际上在测试多个行为,并且应该编写多个测试。
不要把逻辑放进测试,测试代码并不需要再被测试,如果你觉得需要再编写一个测试,用来验证你写好的测试的话,那么一定是出了问题。
编写清晰的失败信息,清晰性的最后一个方面与编写测试无关,而是与工程师在测试失败时看到的东西有关。
一个好的失败消息包含与测试名称几乎相同的信息,它应该清楚的表示期望的结果、实际的结果和任何相关的参数。
DRY(不要重复自己),这种方法使得代码变更更容易,因为工程师只需要更新一段代码,而不需要跟踪多个引用。
这种方式的缺点是,它会使代码变得不清晰,要求读者追踪引用链来理解代码的作用。
在正常的生产代码中,这一缺点通常是为了使代码更易于变更和使用,而付出一个小代价。
但是这种 成本/收益 分析在测试代码的上下文有点不同,好的测试是为了稳定而设计的,事实上,当被测试的系统发生变化时,你通常希望一些测试会失败。
所以 DRY 在测试代码方面没有那么多好处,同时,对于测试来说,复杂性的代价更大,生产代码有测试套件的好处是,可以确保它在变得复杂时继续工作,而测试必须独立进行,如果它们不是自明正确的,就有可能出现错误。
如果测试变得太复杂,以至于我们感觉自己需要对测试进行测试,以确保它们正常工作,那么就会出现问题。
测试代码不应该完全是 DRY,而应该努力保持 DAMP,也就是说 “描述性和有意义的短语”(Descriptive and Meaningful Phrases)。
单元测试是最强大的工具之一,作为软件工程师,我们必须确保我们的系统在面对未预料到的变化时,能够长期工作。
但是,作用越大,责任也就越大,不细心的使用单元测试,可能会导致需要更多投入来维护系统,并且在对系统进行变更时需要更大的投入,但实际上却并没有提高我们对该系统的信心。
13. 测试替身
单元测试是保持开发人员工作效率,和减少代码缺陷的关键工具。
尽管对于简单的代码来说,它们很容易编写,但是随着代码变得越来越复杂,编写它们也变得困难。
测试替身是一个对象或函数,它可以在测试中代表那个真实的实现,类似于特技替身演员替代电影演员的情况。
过度使用模拟框架(mocking framework)会带来危险,许多工程师避免使用模拟框架,转而编写更真实的测试。
如果可以为代码编写单元测试,那么我们称代码是可测试的。
缝(seam)是一种通过允许使用测试替身来实现代码可测试性的方法,它可以为被测系统使用不同的依赖项,来替代在生产环境中所使用的依赖项。
依赖注入是引入缝的常用技术。简言之,当类使用依赖注入时,它所用的任何类(即类的依赖项)都讲传递给它,而不是直接实例化,从而使这些依赖项能够在测试中被替换。
如果是动态语言,那么可以动态替换单个函数或对象方法。
依赖注入在这些语言中不那么重要。因为动态语言的这种能力让在测试中使用依赖项的实际实现成为可能,同时只要重写不适合测试的依赖函数或方法即可。
编写可测试代码需要预先投资,它在代码库生命周期的早期尤为重要,因为越晚考虑可测试性,可测试性应用于代码库就越困难。
不考虑测试的代码通常需要重构或重写,然后才能添加适当的测试。
模拟(mocking)框架是一个软件库,它使在测试中创建测试替身更加容易;
它允许使用一个模拟对象,来替换真实对象,模拟对象是一个在测试中内联指定其行为的测试替身。
模拟框架的使用减少了样板代码,因为不必每次需要测试替身时,都定义一个新类。
尽管模拟框架有助于更容易的使用测试替身,但是在使用时也需要特别注意,因为他们的过度使用通常会使代码库更难维护。
使用测试替身有三种主要技术:
过度使用模拟框架有一种倾向,即用与真实实现不同步的重复代码污染测试,从而使重构变得困难。
倾向于实际实现的测试被称为 经典测试,还有一种称为 模拟测试 的测试风格,在这种风格中,首选的是使用模拟框架,而不是实际实现。
如果单元测试过多的依赖于测试替身,工程师可能需要运行集成测试或手动验证其功能是否按预期工作,以便获得相同的可信度。
14. 较大型的测试
15. 弃用
所有系统都会老化。旧系统需要持续的维护、深奥的专业知识,并且由于旧系统与周围生态系统逐渐分化,它会需要更多的工作量来维护。
因此,对于过时的系统,通常最好是将精力放在关闭它,而不是无限期的维护,让它们与替代它们的新系统并存。
我们将有序迁移旧系统,并最终将其移除的过程称之为弃用。
相比编程来说,弃用与软件工程学科契合更紧密,因为它需要考虑如何随着时间的推移来管理系统。
对于长期运行的软件生态系统,计划和执行正确的弃用计划,并通过消除随时间推移而积累在系统中的冗余和复杂性,从而降低资源成本并提高速度。
相反,错误的弃用系统可能造成的成本比置之不理更高。
虽然弃用系统需要付出额外的工作,但可以在软件系统设计时,就计划好未来的弃用,以便等到真正需要弃用时跟狗容易的进行移除。
我们对弃用的讨论从以下基本前提开始,代码是负债,而不是资产。
代码是要消耗成本的,其中创建系统所产生的的成本只是一部分,更多的成本消耗是随着系统在其整个生命周期的发展中,所产生的维护成本。
这些持续的维护成本,例如保持系统运行所需的运维资源,或随着周围生态系统的发展而需要不断更新其代码,这就意味着有必要对是否应该维持原有的系统还是弃用之间进行权衡。
弃用最适用于一个明显过时的系统,该系统有一个成型的替代产品已经存在,并且可以提供相同的功能。
从长远来看,我们发现拥有多个执行相同功能的系统会阻碍新系统的开发,因为它一直有可能被期望与旧系统有兼容性。
移除旧系统需要花费精力,但是这些换来的回报是更换的新系统可以更快的发展。
要谨慎的选择弃用项目,然后坚持完成它们也是很重要的。
在谷歌内部,我们发现迁移到全新系统的成本非常高,而且成本经常被低估。
通过就地重构的方式来逐步完成弃用工作,可以使现有系统保持运行,同时更容易为用户提供价值。
将弃用的概念纳入系统设计的阶段,在软件工程中可能是比较超前的,但是,在其他工程学科中却很常见。
以核电厂的设计为例,它是一个极其复杂的工程,在核电站的设计中就必须考虑使用寿命结束后的退役,甚至要为此预留经费。
可是,软件系统对这方面的考量少之又少,许多软件工程师对构建和启动新系统更感兴趣,而不是维护现有系统的任务。
我们鼓励谷歌的工程团队提出以下几个问题:
我们需要指出的是,是否长期支持某个项目,是公司在构建该项目的初期就要做的决定。
当一个软件系统存在以后,能做的就是维护该软件,谨慎的选择弃用该软件,或者在某些外部事件导致它崩溃时让它停止运行。
简而言之,你的公司如果没有做好准备支持该项目的整个生命周期,那就不要开始这个项目。
16. 版本控制与分支管理
由于 DevOps 而流行起来的 “基于主干的开发”(一个存储库,没有开发分支),其实是一种有利于规模化的策略方法。
与任何流程一样,版本控制也会带来一些开销,必须有人配置和管理你的版本控制系统,每个开发人员都必须使用它。
但别搞错了,这些事情的成本几乎总是很低。
任何事情都可以考虑不同的方案,甚至是一些基本的东西,如版本控制。
分支等同于在制品
我们认为,将开发分支作为实现产品质量稳定的手段在本质上是错误的。
同一个变更集最终会被合并到主干上,而小的合并要比大的合并更容易。与批量处理不相关的变更,然后再一起合并的方式相比,由编写这些变更的工程师自己进行合并会更容易。
如果只要一个工程师参与,那么更容易确定是谁的变更导致了回归问题。
合并一个大型的开发分支意味着测试的一次执行将同时检查多个代码变更,当失败时,更难以区隔开。
对问题进行分类和根本原因分析是困难的,修复它更困难。
基于主干的开发,严重依赖测试和 CI,保持构建为绿色(指成功),并在运行时禁用未完成/未测试的特性。
每个人都自动负责提交到主干,并与主干同步;不需要 “合并策略” 会议,也没有大型/昂贵的合并。
而且,不会有关于应该使用哪个版本的库的激烈讨论——因为只有一个版本。
如果产品发布之间的时间间隔,超过几个小时,可以创建一个发布分支,让这个分支只包含为了产品发布而构建的代码,这么做可能是个明智的做法。
如果从产品实际发布出去之时,到下一个发布周期之前,这段时间发现了任何关键缺陷,那么可以从主干挑选(cherry-pick)出用于修复这个缺陷的代码,合并到发布分支上。
与开发分支相比,发布分支通常是无害的,问题不在于 “拉分支” 这项技术成本,而是分支的使用方法。
开发分支和发布分支之间的主要区别在于预期的最终状态:开发分支预期合并会主干,甚至有可能被另一个团队进一步分支;而一个发布分支最终会被抛弃。
由谷歌 DevOps Research and Assessment(DORA)部门发现,在最高效的组技术组织中,发布分支实际上是不存在的。
已经实现了持续部署(CD)的组织(拥有每天从主干发布多次的能力),可能会跳过发布分支:只要在主干上添加修复,并重新部署即可,这比使用发布分支要容易得多。
这样的话,cherry-pick 和分支似乎是不必要的开销。
同样的 DORA 研究也表明,“基于主干的开发” “没有长周期的开发分支” 和良好的技术成果之间存在着很强的正相关关系。
这两种观点的基本思想似乎都很清楚:分支是生产力的累赘。
谷歌的版本控制依赖于一个内部开发的集中式 VCS,称为 Piper,在生产环境中作为分布式微服务运行。
在主干上创建一个新客户端、添加一个文件,并将一个(未做评审的)变更提交给 Piper 总共可能需要 15 秒。这种低延迟的交互和易于理解/设计良好的可扩展性大大简化了开发人员的体验。
由于 Piper 是一个内部产品,我们有能力定制它并执行我们选择的任何源代码控制策略。
例如,我们在单一代码仓中有一个所有权的概念,在文件层次结构的每个级别,我们都可以找到 OWNERS 文件,其中列出了允许在存储库的子树中,批准提交的工程师的名字。
在多仓环境中,这可能是通过文件系统权限,强制控制每个存储库的提交访问权限,或通过 git commit hook 执行单独的权限检查来实现的。
通过控制 VCS,我们可以使所有权(ownership)和批准(approval)的概念更加明确,并在尝试提交操作时,由 VCS 强制执行。
除了我们的 VCS,谷歌版本控制政策的一个关键特性就是我们所说的 “单一版本”(One version)——对于存储库中的每个依赖项,必须只能选择该依赖项的唯一版本。
对于第三方包,这意味着在稳定状态下,只能有一个版本的包签入到我们的存储库中。
对于内部包,这意味着,如果不重新打包或重新命名就不能分叉(fork)。
对于我们的生态系统来说,这是一个很强大的特性:很少有包具有这样的限制 “如果包含这个包 A,就不能包含其他包 B”。
任何允许在同一个代码库中有多个版本的政策体系,可能都会存在这种代价高昂的不兼容性。
有可能你会逃脱一段时间,但总的来说,任何多版本的情况,都有可能导致大问题。
在实践中,“单一版本” 并不是硬性规定的,但是在添加新的依赖项时,要限制可以选择的版本,这种措辞表达了一种非常强烈的共识。
开发分支应该是最小化的,或者最好是非常短暂的。这源于过去 20 年里发表的大量成果,从敏捷过程到基于主干开发的 DORA 研究结果,甚至凤凰项目关于 “减少在制品” 的课程。
当我们把开发分支等同于未完成的工作时,这进一步证实了应该基于主干进行小的增量开发,并频繁的提交。
请记住,在添加依赖项时,新的开发必须没有选择。
重要的并不是单一代码仓,而是尽可能坚持 “单一版本” 的原则:当开发人员向一些已经在组织中使用的库添加依赖项时,不能有选项。
违反 “单一版本” 规则的选项会导致合并策略讨论、菱形依赖、损耗生产力和浪费精力。
如果可以使用独立的多个存储库,进行管理并坚持使用一个版本,或者工作可以完全解耦足以允许真正的独立存储库,那很好。
“单一版本” 规则:组织中的开发人员不能自行选择在哪里提交,或者依赖于现有组件的哪个版本。
17. 代码搜索
18. 构建工具与构建哲学
如果你问谷歌的工程师,在谷歌工作他们最喜欢什么,你可能会听到一些令人惊讶的事情:工程师喜欢构建系统。
谷歌在其生命周期中投入了大量的工程工作来从头创建自己的构建系统。
其目标是确保我们的工程师能够快速、可靠的构建代码。
从根本上说,所有构建系统都有一个直接的目的,它们将工程师编写的源代码转换成可由机器读取的可执行二进制文件。
构建系统不仅仅是为人类设计的,它们还允许机器自动执行构建,无论是用于测试还是用于发布到生产环境。
实际上,谷歌的大部分构建都是自动触发的,而不是由工程师直接触发的。
几乎我们所有的开发工具,都以某种方式与构建系统绑定,为每个在我们的代码库上工作的人提供了巨大的价值。
大型系统通常包括用各种编程语言编写的不同部分,这些部分之间存在依赖关系,这意味着没有任何一种语言的编译器可以构建整个系统。
一旦我们不得不处理来自多种语言或多个编译单元的代码,构建代码就不再是一个单步过程。
现在我们需要考虑我们的代码依赖于什么,并以适当的顺序构建这些片段,可能为每个片段使用不同的工具集。
如果我们更改任何依赖项,我们需要重复这个过程,以避免依赖陈旧的二进制文件。
对于一个中等规模的代码库,这个过程很快就会变得烦琐,并且容易出错。
管理自己的代码相当简单,但是管理它的依赖项就困难的多。
有各种各样的依赖项:
但在任何情况下,“在我得到这个之前我先需要那个” 的想法,在构建系统的设计中反复出现。
而管理依赖项可能是构建系统最基本的工作。
基于任务的构建系统:
基于制品的构建系统:构建系统的主要任务应该是构建代码。工程师仍然需要告诉系统要构建什么,但是如何构建将有系统决定。
Blaze 中的构建文件不是图灵完备性脚本语言中描述如何生成输出的命令集,而是一个声明性的清单文件,它描述了要构建的一组制品,它们的依赖项以及影响它们构建方式的有限选项集。
当工程师在命令行上运行 blaze 时,他们制定一组要构建的目标(what),blaze 负责配置、运行和调度编译步骤(how)。
因为构建系统现在可以完全控制什么时候运行什么工具,所以它可以做出更有力的保证,在保证正确性的同时,让它更高效。
用制品而不是任务来重新定义构建过程,很巧妙,也很强大。
通过减少对程序员的灵活性,构建系统可以知道在构建的每一步都在做什么。
它可以利用这些知识,通过并行化构建过程并重用它们的输出,使构建更加高效。
但实际上这只是第一步,这些并行化和重用的构建块将构成一个分布式的、高度可伸缩的构建系统的基础。
19. Critique:谷歌的代码评审工具
20. 静态分析
21. 依赖管理
为什么依赖管理这么难,在这个领域中,许多半生不熟的解决方案都关注于狭隘的问题描述:如何导入本地开发代码可以依赖的包。
这是一个必要但不充分的描述。
诀窍不仅仅是找到一种方法来管理一个依赖项,而是如何管理一个依赖网络及其随时间的变化。
这个网络的某些子集对于第一方代码是直接必需的,而某些子集只是有传递依赖引入的。
一个稳定的依赖管理方案,必须在时间和规模上保持灵活:我们不能假设依赖图中任何特定节点具有无限的稳定性,也不能假设没有添加新的依赖(无论是我们所控制的代码中,还是我们所依赖的代码中)。
依赖管理本身就是一项挑战,来管理这些复杂 API 界面和依赖网络,而这些依赖的维护者之间通常很少或根本没有协调机制。
与管理依赖问题相比,更倾向于使用源代码控制方式:如果你可以从组织中获得更多代码,以获得更好的透明度和协调性,那么问题都可以很好的简化。
对于一个软件工程项目,添加依赖并非是免费的,而且建立 “持续的” 信任关系的复杂性也具有挑战性。
在组织中导入依赖需要小心行事,并了解持续提供支持的成本。
22. 大规模变更
23. 持续集成
24. 持续交付
任何软件工作的最大风险是最终构建的东西是无用的,越早、越频繁的将可工作的软件呈现在实际用户面前,就会越快的得到反馈,从而发现它的真正价值。
在交付用户价值之前,长时间处于进行中的工作是高风险和高成本的,甚至会消耗士气。
在谷歌,我们努力尽早且频繁发布,或者 “产品上市并迭代”,以使团队能够快速看到他们工作的影响,并更快的适应不断变化的市场。
代码的价值不是在提交时实现的,而是在特性呈现在用户面前时实现的。
缩短 “代码完成” 和用户反馈之间的时间,可以最大限度的降低正在进行的工作的成本。
多年来,在我们所有的软件产品中,我们发现,更快就是更安全,这可能与直觉相反。
你的产品的健康和开发速度实际上并不是对立的,更频繁且小批量发布的产品有更好的质量结果。
它们能更快的应对发现的缺陷,和意想不到的市场变化。
不仅如此,更快的话,成本也会更低,因为一个可预测且频繁发布的火车会迫使我们努力降低每次发布的成本,并让放弃发布的成本非常低。
25. 计算即服务
后记
谷歌的软件工程师在如何开发和维护一个大型的不断发展的代码库方面进行了非凡的试验。
在技术不断变化的环境中,软件工程在一个既定的组织中扮演着特别重要的角色。
今天,软件工程原则不仅仅是关于如何有效的运行一个组织,而是关于如何成为一个对用户和整个世界更负责任的公司。
可持续性的思想也是软件工程的核心。
在代码库的预期寿命中,我们必须能够对变化做出反应和适应,无论是产品方向、技术平台、底层库、操作系统等。