Skip to content

CHAPTER 8 Style Guides and Rules

kimi edited this page Aug 15, 2021 · 8 revisions

代码风格指南和规则

大多数工程组织都有管理其代码库的规则--关于源文件存储位置的规则,关于代码格式的规则,关于命名和模式以及异常和线程的规则。大多数软件工程师都在一套控制他们如何操作的政策范围内工作。在谷歌,为了管理我们的代码库,我们维护了一套风格指南来定义我们的规则。

规则就是法律。它们不只是建议或推荐,而是严格的、强制性的法律。因此,它们是普遍可执行的--除非在需要使用的基础上被批准,否则规则不能被忽视。与规则相反,指南提供了建议和最佳实践。这些位是好的,甚至是非常值得遵循的,但与规则不同的是,它们通常有一些变化的空间。

我们把我们定义的规则,即写代码必须遵守的 "做"和 "不做",收集在我们的编程风格指南中,这些指南被视为典范。"风格"在这里可能有点名不副实,意味着一个仅限于格式化做法的集合。我们的风格指南不仅仅是这样;它们是管理我们代码的一整套惯例。这并不是说我们的风格指南是严格的规定性的;风格指南的规则可能需要判断,例如使用 "在合理范围内尽可能描述"的名称的规则。相反,我们的风格指南是对我们的工程师负责的规则的最终来源。

我们为谷歌使用的每一种编程语言制定了单独的风格指南。在高层次上,所有的指南都有类似的目标,旨在引导代码开发,并着眼于可持续性。同时,它们之间在范围、长度和内容上也有很大的差异。编程语言有不同的优势,不同的特点,不同的优先级,以及在谷歌不断发展的代码库中采用的不同历史路径。因此,独立定制每种语言的指南要实际得多。我们的一些风格指南是简洁的,专注于一些总体原则,如命名和格式化,正如我们的Dart、R和Shell指南所展示的。其他的风格指南包括更多的细节,深入研究特定的语言特征,并延伸到更长的文件中--特别是我们的C++、Python和Java指南。一些风格指南重视语言的典型非Google使用--我们的Go风格指南非常简短,只在一个摘要指令中增加了一些规则,以遵守外部认可的惯例。其他的规则包括从根本上不同于外部规范的规则;我们的C++规则不允许使用异常,这是一个在Google代码之外广泛使用的语言特性。

即使是我们自己的风格指南也有很大的差异,这使得我们很难准确地描述一个风格指南应该包括什么。指导谷歌风格指南发展的决定源于保持我们代码库可持续发展的需要。其他组织的代码库对可持续发展有不同的要求,因此需要一套不同的定制规则。本章主要从Google的C++、Python和Java风格指南中抽取例子,讨论指导我们制定规则和指南的原则和过程。

为什么有规则?

那么,我们为什么要制定规则?制定规则的目的是为了鼓励 "好"的行为,阻止 "坏"的行为。对 "好"和 "坏"的解释因组织而异,取决于该组织所关心的问题。这种指定不是普遍的偏好;好与坏是主观的,并根据需要而定。对于一些组织来说,"好"可能会促进支持小内存足迹的使用模式,或优先考虑潜在的运行时优化。在其他组织中,"好"可能会促进行使新语言功能的选择。有时,一个组织最关心的是一致性,所以任何与现有模式不一致的东西都是 "坏的"。我们必须首先认识到一个特定的组织所重视的东西;我们使用规则和指导来鼓励和阻止相应的行为。

随着一个组织的成长,既定的规则和指南塑造了编码的共同词汇。一个共同的词汇允许工程师专注于他们的代码需要说什么,而不是他们如何说。通过塑造这种词汇,工程师会倾向于做默认的 "好"事情,甚至是下意识的。因此,规则为我们提供了广泛的杠杆作用,使常见的开发模式朝着理想的方向发展。

创建规则

在定义一套规则时,关键问题不是 "我们应该有什么规则"?要问的问题是,"我们试图推进什么目标?" 当我们专注于规则所要服务的目标时,确定哪些规则支持这一目标,就会更容易提炼出一套有用的规则。在谷歌,风格指南作为编码实践的法律,我们不问:"什么东西进入风格指南?"而是问:"为什么一些东西进入风格指南?" 我们的组织通过拥有一套规范编写代码的规则获得了什么?

指导原则

让我们把事情放在背景中。谷歌的工程组织由超过3万名工程师组成。这些工程人员在技能和背景方面表现出极大的差异性。每天大约有60,000个项目提交到一个超过20亿行的代码库中,而这个代码库可能会存在几十年。我们正在优化一套不同于其他大多数组织需要的价值,但在某种程度上,这些问题是普遍存在的--我们需要维持一个对规模和时间都有弹性的工程环境。

在这种情况下,我们的规则的目标是管理我们的开发环境的复杂性,保持代码库的可管理性,同时仍然允许工程师有成效地工作。我们在这里做了一个权衡:帮助我们实现这一目标的大量规则意味着我们限制了选择。我们失去了一些灵活性,甚至可能会得罪一些人,但权威的标准所带来的一致性和减少冲突的好处是最重要的。

鉴于这种观点,我们认识到一些指导我们规则发展的总体原则,这些原则必须:

  • 承担起自己的责任
  • 对读者进行优化
  • 保持一致
  • 避免容易出错的和令人惊讶的结构
  • 必要时向实际情况让步

规则必须发挥其作用

不是所有的东西都应该被纳入风格指南。要求一个组织中的所有工程师学习和适应任何新的规则,都是一种非零的成本。如果规则太多,不仅工程师在写代码时更难记住所有相关的规则,而且新的工程师也更难学会他们的方法。更多的规则也会使维护规则集变得更具挑战性和更昂贵。

为此,我们特意选择不包括预计不言自明的规则。谷歌的风格指南并不打算用律师的方式来解释;没有明确规定的东西并不意味着它是合法的。例如,C++风格指南没有禁止使用goto的规则。C++程序员已经倾向于避免使用它,所以包括一个明确的规则来禁止它将带来不必要的开销。如果只有一两个工程师犯了错误,通过创建新的规则来增加每个人的心理负担是不合适的。

为读者优化

我们规则的另一个原则是为代码的读者而不是作者进行优化。考虑到时间的流逝,我们的代码被阅读的频率将远远超过它被编写的频率。我们宁可让代码打得繁琐,也不愿意让它难以阅读。在我们的 Python 风格指南中,在讨论条件表达式时,我们认识到它们比 if 语句更短,因此对代码作者来说更方便。然而,由于它们往往比更冗长的 if 语句更难让读者理解,我们限制了它们的使用。我们重视 "简单阅读"而不是 "简单编写"。我们在这里做了一个权衡:当工程师必须为变量和类型反复输入可能更长的描述性名称时,前期成本会更高。我们选择为所有未来的读者提供可读性而支付这一成本。

作为优先级的一部分,我们还要求工程师在他们的代码中留下预期行为的明确证据。我们希望读者在阅读时能够清楚地了解代码在做什么。例如,我们的Java、JavaScript和C++风格指南规定,当一个方法覆盖一个超类方法时,要使用override注解或关键字。如果没有明确的原地设计证据,读者很可能会弄清楚这个意图,尽管这需要每个读者在阅读代码时多花点心思。

当预期行为的证据可能令人惊讶时,它就变得更加重要了。在C++中,仅仅通过阅读一段代码,有时很难追踪指针的所有权。如果一个指针被传递给一个函数,在不熟悉该函数的行为的情况下,我们不能确定会有什么预期。调用者仍然拥有这个指针吗?函数是否拥有所有权?在函数返回后,我可以继续使用这个指针吗,或者它可能已经被删除了?为了避免这个问题,我们的C++风格指南倾向于在打算转移所有权时使用std::unique_ptr。unique_ptr是一个管理指针所有权的结构,确保指针只存在一个副本。当一个函数将unique_ptr作为参数并打算取得指针的所有权时,调用者必须明确地调用移动语义。

// Function that takes a Foo* and may or may not assume ownership of 
// the passed pointer.
void TakeFoo(Foo* arg);

// Calls to the function don’t tell the reader anything about what to 
// expect with regard to ownership after the function returns.
Foo* my_foo(NewFoo());
TakeFoo(my_foo);

将此与以下内容进行比较。

// Function that takes a std::unique_ptr<Foo>.
void TakeFoo(std::unique_ptr<Foo> arg);
// Any call to the function explicitly shows that ownership is 
// yielded and the unique_ptr cannot be used after the function 
// returns.
std::unique_ptr<Foo> my_foo(FooFactory()); TakeFoo(std::move(my_foo));

考虑到风格指南的规则,我们保证所有的调用站点只要适用就会包括所有权转移的明确证据。有了这个信号,代码的读者不需要了解每个函数调用的行为。我们在API中提供了足够的信息来推理它的交互关系。这种在调用地点的清晰的行为文档确保代码片段保持可读性和可理解性。我们的目标是局部推理,目标是清楚地了解在调用地点发生了什么,而不需要寻找和参考其他代码,包括函数的实现。

大多数涉及注释的风格指南规则也是为了支持这一目标,即为读者提供就地证据。文档注释(预先放在某个文件、类或函数上的块状注释)描述了后面代码的设计或意图。实现注释(穿插在代码本身中的注释)证明或强调不明显的选择,解释棘手的部分,并强调代码的重要部分。我们有涵盖这两类注释的风格指南规则,要求工程师提供另一个工程师在阅读代码时可能寻找的解释。

要有一致性

我们对代码库内的一致性的看法与我们对谷歌办公室的理念相似。由于工程人员众多,分布广泛,团队经常被分到不同的办公室,谷歌员工也经常发现自己在其他地方出差。尽管每个办公室都保持其独特的个性,接受当地的风味和风格,但对于完成工作所需的任何事情,都刻意保持一致。一个来访的Googler的徽章可以在当地所有的徽章阅读器上使用;任何谷歌设备都可以获得WiFi;任何会议室的视频会议设置都有相同的界面。Googler不需要花时间学习如何设置这一切;他们知道,无论他们在哪里都是一样的。在办公室之间移动,仍能轻松完成工作。

这就是我们对源代码的追求。一致性是使任何工程师能够跳到代码库中不熟悉的部分并相当迅速地开始工作的原因。一个本地项目可以有它独特的个性,但它的工具是一样的,它的技术是一样的,它的库是一样的,而且它都能工作。

一致性的优势

尽管对于一个办公室来说,不允许定制徽章阅读器或视频会议界面可能会感觉到限制性,但一致性的好处远远超过了我们失去的创造性自由。代码也是如此:保持一致有时可能会感觉受到限制,但这意味着更多的工程师能以更少的努力完成更多的工作。

  • 当一个代码库的风格和规范内部一致时,编写代码的工程师和阅读代码的其他人就可以专注于正在完成的工作,而不是它的呈现方式。在很大程度上,这种一致性允许专家分块。当我们用相同的接口来解决我们的问题,并以一致的方式对代码进行格式化时,专家就更容易扫视一些代码,将注意力集中在重要的部分,并理解它在做什么。这也使得代码的模块化和发现重复的工作变得更加容易。由于这些原因,我们把大量的注意力集中在一致的命名规则上,一致地使用通用模式,以及一致的格式和结构。也有很多规则在一个看似很小的问题上提出了一个决定,仅仅是为了保证事情只用一种方式完成。例如,选择缩进所需的空格数,或对行长的限制。这里有价值的部分是有一个答案的一致性,而不是答案本身。
  • 一致性使扩展成为可能。工具是一个组织进行扩展的关键,一致的代码使得建立能够理解、编辑和生成代码的工具更加容易。如果每个人都有不同的小块代码,那么依赖于统一性的工具的全部好处就无法应用--如果一个工具可以通过添加缺失的导入或删除未使用的包含来保持源文件的更新,如果不同的项目为他们的导入列表选择不同的排序策略,那么这个工具可能无法在任何地方工作。当每个人都在使用相同的组件,当每个人的代码都遵循相同的结构和组织规则时,我们就可以投资于能在任何地方工作的工具,为我们的许多维护任务建立自动化。 如果每个团队都需要单独投资于同一工具的定制版本,为他们独特的环境量身定做,我们就会失去这种优势。
  • 一致性在扩展组织的人力部分时也有帮助。随着组织的发展,从事代码库工作的工程师数量也在增加。尽可能地保持每个人都在工作的代码的一致性,可以更好地跨项目流动,最大限度地减少工程师转换团队的时间,并建立组织的能力,以灵活和适应人数需求的波动。一个不断成长的组织也意味着其他角色的人与代码互动--例如,SRE、库工程师和代码管理员。在谷歌,这些角色往往跨越多个项目,这意味着不熟悉某个团队项目的工程师可能会跳到这个项目的代码上工作。在整个代码库中保持一致的经验使这种做法变得有效。
  • 一致性也确保了对时间的适应性。随着时间的推移,工程师离开项目,新人加入,所有权转移,项目合并或分裂。争取一个一致的代码库可以确保这些转变的成本很低,并允许我们对代码和工作在上面的工程师有几乎不受约束的流动性,简化了长期维护所需的过程。

在规模上
几年前,我们的C++风格指南承诺几乎不会改变会使旧代码不一致的风格指南规则。"在某些情况下,可能有很好的理由来改变某些风格规则,但我们还是要保持原样,以保持一致性。
当代码库比较小的时候,旧的、有灰尘的角落比较少,这是有道理的。 当代码库越来越大,越来越老的时候,这就不再是一个需要优先考虑的问题了。这(至少对我们的C++风格指南背后的仲裁者来说)是一个有意识的变化:当打击这一点时,我们明确指出,C++代码库将永远不会再完全一致,我们甚至也不希望如此。
不仅要根据当前的最佳实践来更新规则,而且还要要求我们将这些规则应用于所有曾经写过的东西,这简直是太大的负担了。我们的大规模变更工具和流程允许我们更新几乎所有的代码,以遵循几乎每一种新的模式或语法,从而使大多数旧的代码表现出最新的批准的风格(见第22章)。然而,这样的机制并不完美;当代码库变得如此之大时,我们不可能确保每一段旧代码都能符合新的最佳实践。要求完美的一致性已经达到了代价太大的地步。

设定标准。当我们提倡一致性时,我们倾向于关注内部一致性。有时,在全球惯例被采用之前,地方性的惯例就已经出现了,而调整所有的东西来适应这种情况是不合理的。在这种情况下,我们提倡一致性的等级制度。"保持一致"从本地开始,一个特定文件的规范先于一个特定的团队的规范,而团队的规范又先于大项目的规范,大项目的规范又先于整个代码库的规范。事实上,风格指南中包含了许多明确服从于本地惯例的规则,重视这种本地一致性而不是科学的技术选择。

然而,对于一个组织来说,仅仅创建并坚持一套内部惯例是不够的。有时,应考虑到外部社区采用的标准。

计算空格
Google 的 Python 风格指南最初规定我们所有的 Python 代码都要缩进两个空格。外部 Python 社区使用的标准 Python 风格指南使用四空格缩进。我们早期的Python开发大多是为了直接支持我们的C++项目,而不是为了实际的Python应用。因此,我们选择使用两空缩进,以便与我们的 C++ 代码保持一致,因为我们的代码已经以这种方式进行了格式化。随着时间的推移,我们发现这个理由其实并不成立。写Python代码的工程师读写其他Python代码的频率比读写C++代码的频率高得多。每次我们的工程师需要查找一些东西或参考外部代码时,我们都要付出额外的努力。每次我们试图将我们的代码输出到开放源码时,我们也会经历很多痛苦,花时间来协调我们内部代码和我们想要加入的外部世界之间的差异。
当Starlark(谷歌设计的一种基于Python的语言,作为构建描述语言)拥有自己的风格指南的时候,我们选择改变为使用四空格缩进,以与外部世界保持一致。

如果惯例已经存在,一个组织与外部世界保持一致通常是个好主意。对于小型的、独立的和短暂的工作来说,这可能不会有什么影响;内部的一致性比在项目的有限范围之外发生的事情更重要。一旦时间的流逝和潜在的扩展成为因素,你的代码与外部项目互动的可能性就会增加,甚至最终出现在外部世界。从长远来看,坚持广泛接受的标准可能会得到回报。

避免容易出错和令人惊讶的结构体

我们的风格指南限制使用我们所使用的语言中的一些更令人惊讶、不寻常或棘手的结构。复杂的特征往往有一些微妙的陷阱,乍一看并不明显。在没有彻底了解其复杂性的情况下使用这些特性,很容易造成误用和引入bug。即使一个结构被项目的工程师很好地理解,也不能保证未来的项目成员和维护者也有同样的理解。

这个道理在我们的 Python 风格指南中得到了体现,即避免使用诸如反射之类的强大功能。反射的 Python 函数 hasattr() 和 getattr() 允许用户使用字符串访问对象的属性。

if hasattr(my_object, 'foo'): 
some_var = getattr(my_object, 'foo')

现在,通过这个例子,一切可能看起来都很好。但考虑到这一点。

some_file.py:
    A_CONSTANT = [
        'foo',
        'bar',
        'baz',
    ]
other_file.py:
    values = []
    for field in some_file.A_CONSTANT: 
    values.append(getattr(my_object, field))

在搜索代码时,你怎么知道这里访问的是字段foo、bar和baz?没有给读者留下明确的证据。你不容易看到,因此也不容易验证哪些字符串被用来访问你的对象的属性。如果我们不是从A_CONSTANT中读取这些值,而是从远程过程调用(RPC)请求消息或从数据存储中读取这些值呢?这种混淆的代码可能会导致一个重大的安全漏洞,这个漏洞很难被注意到,只要错误地验证消息就可以了。测试和验证这样的代码也很困难。

Python 的动态特性允许这种行为,在非常有限的情况下,使用 hasattr() 和 getattr() 是有效的。然而在大多数情况下,它们只是造成了混淆和引入了错误。

尽管这些高级的语言特性对于知道如何利用它们的专家来说,可能会完美地解决一个问题,但强大的特性往往更难理解,而且使用也不是很广泛。我们需要我们所有的工程师都能在代码库中操作,而不仅仅是专家。这不仅仅是对新手软件工程师的支持,也是为SRE提供更好的环境--如果SRE正在调试生产中断,他们会跳到任何一点可疑的代码,甚至是用他们不流利的语言编写的代码。我们更看重简化的、直接的代码,它更容易理解和维护。

向实际情况让步

用拉尔夫-瓦尔多-爱默生的话来说,就是 "愚蠢的一致性是小脑袋的妖精"。在我们寻求一个一致的、简化的代码库的过程中,我们不想盲目地忽略其他的东西。我们知道,我们的风格指南中的一些规则会遇到需要例外的情况,这没有问题。必要时,我们允许对可能与我们的规则相冲突的优化和实用性做出让步。

性能很重要。有时,即使这意味着牺牲一致性或可读性,适应性能优化也是合理的。例如,尽管我们的C++风格指南禁止使用异常,但它包括一条允许使用noexcept的规则,这是一个与异常有关的语言指定符,可以触发编译器优化。

互操作性也很重要。旨在与特定的非谷歌作品一起工作的代码,如果为其目标量身定做,可能会做得更好。例如,我们的C++风格指南包括一个对一般CamelCase命名准则的例外,允许对模仿标准库特征的实体使用标准库的snake_case风格。C++风格指南还允许对Windows编程的豁免,在这种情况下,与平台特性的兼容需要多重继承,这是所有其他C++代码明确禁止的。我们的Java和JavaScript风格指南都明确指出,生成的代码经常与项目所有权之外的组件对接或依赖,不在指南的规则范围内。一致性是至关重要的;适应性是关键。

风格指南

那么,语言风格指南有哪些内容呢?大致有三类,所有风格指南的规则都属于这三类。

  • 避免危险的规则
  • 执行最佳做法的规则
  • 确保一致性的规则

避免危险

首先,我们的风格指南包括关于语言特性的规则,这些特性由于技术原因必须或不必须做。我们有关于如何使用静态成员和变量的规则;关于使用lambda表达式的规则;关于处理异常的规则;关于构建线程、访问控制和类继承的规则。我们涵盖了哪些语言特性需要使用,哪些结构需要避免。我们指出了可以使用的标准词汇类型和用途。我们特别包括关于难以使用和难以正确使用的裁决--一些语言特性有细微的使用模式,可能不直观或不容易正确应用,导致微妙的错误悄然出现。对于指南中的每一项裁决,我们的目标是包括权衡的利弊,以及对所做决定的解释。这些决定大多是基于对时间的适应性的需要,支持和鼓励可维护的语言使用。

强制执行最佳实践

我们的风格指南还包括执行一些编写源代码的最佳做法的规则。这些规则有助于保持代码库的健康和可维护性。例如,我们规定了代码作者必须在哪里以及如何加入注释。我们的注释规则涵盖了注释的一般惯例,并扩展到包括必须包括代码内文档的特定情况--在这些情况下,意图并不总是很明显,例如开关语句中的落差,空的异常捕获块,以及模板元编程。我们也有详细说明源文件结构的规则,概述了预期内容的组织。我们有关于命名的规则:包的命名、类的命名、函数的命名、变量的命名。所有这些规则都是为了指导工程师的实践,以支持更健康、更可持续的代码。

我们的风格指南所执行的一些最佳实践是为了使源代码更易读。许多格式化规则都属于这个类别。我们的风格指南规定了何时以及如何使用垂直和水平的空白,以提高可读性。它们还包括行长限制和括号对齐。对于某些语言,我们通过使用自动格式化工具来满足格式化要求--Go的gofmt,Dart的dartfmt。逐项列出格式化要求的详细清单或命名必须应用的工具,其目的都是一样的:我们有一套一致的格式化规则,旨在提高可读性,并应用于我们所有的代码。

我们的风格指南还包括对新的和尚未被充分理解的语言特征的限制。我们的目标是在大家学习的过程中,先期为一个功能的潜在隐患设置安全栅栏。同时,在每个人都跑起来之前,限制使用给了我们一个机会来观察使用模式的发展,并从我们观察到的例子中提取最佳实践。对于这些新功能,在一开始,我们有时并不确定该如何给予适当的指导。随着采用的普及,希望以不同方式使用新功能的工程师们与风格指南的所有者讨论他们的例子,要求允许超出最初限制所涵盖的额外使用情况。观察收到的豁免请求,我们可以了解到该功能是如何被使用的,并最终收集到足够的例子来归纳出好的做法。在我们有了这些信息之后,我们可以回到限制性的裁决,并修改它以允许更广泛的使用。

案例研究:引入std::unique_ptr
当C++11引入std::unique_ptr这个智能指针类型时,它表达了对动态分配对象的独占所有权,并在unique_ptr超出范围时删除该对象,我们的风格指南最初不允许使用。对大多数工程师来说,unique_ptr的行为是不熟悉的,而语言引入的相关移动语义是非常新的,对大多数工程师来说,也是非常混乱的。防止在代码库中引入std::unique_ptr似乎是更安全的选择。我们更新了我们的工具,以捕捉对不允许的类型的引用,并保留了我们现有的推荐其他类型的现有智能指针的指导。
时间过去了。工程师们有机会适应移动语义的影响,我们越来越相信,使用std::unique_ptr直接符合我们风格指导的目标。std::unique_ptr在函数调用处提供的关于对象所有权的信息使读者更容易理解该代码。引入这种新类型所增加的复杂性,以及随之而来的新的移动语义,仍然是一个强烈的关注点,但是代码库的长期整体状态的显著改善使得采用std::unique_ptr是一个值得的折衷。

建立一致性

我们的风格指南也包含了涵盖很多小东西的规则。对于这些规则,我们主要是为了做出和记录一个决定。这个类别中的许多规则并没有重大的技术影响。像命名规则、缩进间距、导入顺序等:通常没有明确的、可衡量的、对一种形式的技术好处,这可能就是为什么技术社区倾向于继续争论它们。我们的工程师不再花时间去讨论两个空格和四个空格的问题了。对于这类规则来说,重要的不是我们为某个规则选择了什么,而是我们已经选择了这个事实。

而对于其他一切...

有了这些,有很多东西都不在我们的风格指南中。我们试图把重点放在对我们代码库的健康有最大影响的事情上。这些文件中绝对有一些最佳实践没有说明,包括许多基本的良好工程建议:不要自作聪明,不要分叉代码库,不要重新发明轮子,等等。像我们的风格指南这样的文件不可能把一个完全的新手带入对软件工程的大师级理解--有些事情我们是假设的,这是故意的。

改变规则

我们的风格指南并不是一成不变的。就像大多数事情一样,随着时间的推移,风格指南的决定所处的环境以及指导某项裁决的因素都有可能发生变化。有时,条件的变化足以证明需要重新评估。如果一个新的语言版本发布,我们可能要更新我们的规则,以允许或排除新的功能和习语。如果一个规则导致工程师们投入精力去规避它,我们可能需要重新审视这个规则所提供的好处。如果我们用来执行规则的工具变得过于复杂,维护起来很麻烦,那么规则本身可能已经腐烂,需要重新审视。注意到一条规则何时可以重新审视,是使我们的规则集保持相关和最新的过程的一个重要部分。

我们的风格指南中的规则背后的决定是有证据支持的。当添加一条规则时,我们会花时间讨论和分析相关的利弊以及潜在的后果,试图验证一个特定的变化对于谷歌的运营规模是合适的。谷歌风格指南中的大多数条目都包括这些考虑,列出了在这个过程中权衡的利弊,并给出了最终裁决的理由。理想情况下,我们会优先考虑这种详细的推理,并将其包含在每一条规则中。 记录一个给定的决定背后的理由,使我们有能力认识到什么时候需要改变。鉴于时间的流逝和条件的变化,以前做出的好决定可能不是现在的最佳决定。有了明确的影响因素,我们就能确定何时与一个或多个因素有关的变化需要重新评估该规则。

案例研究:CamelCase 命名
在Google,当我们为Python代码定义最初的风格指南时,我们选择使用CamelCase命名风格,而不是方法名称的snake_case命名风格。尽管公共的Python风格指南(PEP 8)和大多数Python社区都使用了snake_case命名方式,但当时Google的大部分Python应用是为C++开发人员提供的,他们将Python作为C++代码库的脚本层。许多定义的Python类型都是相应的C++类型的封装器,由于Google的C++命名规则遵循CamelCase风格,跨语言的一致性被视为关键。
后来,我们达到了建立和支持独立的Python应用程序的地步。最经常使用Python的工程师是开发Python项目的Python工程师,而不是拉拢一个快速脚本的C++工程师。我们给我们的Python工程师造成了一定程度的尴尬和可读性问题,要求他们为我们的内部代码保持一个标准,但每次引用外部代码时都要不断调整另一个标准。我们也让那些有Python经验的新员工更难适应我们的代码库规范。
随着我们的Python项目的增长,我们的代码更频繁地与外部Python项目进行交互。我们在一些项目中加入了第三方Python库,导致我们的代码库中混合了我们自己的CamelCase格式和外部偏好的snake_case风格。当我们开始开源我们的一些Python项目时,在一个外部世界中维护它们,而我们的惯例是不合规的,这既增加了我们的复杂性,也增加了社区的戒心,因为他们认为我们的风格令人惊讶,有点奇怪。
面对这些争论,在讨论了代价(失去与其他Google代码的一致性,对习惯了我们Python风格的Googlers进行再教育)和好处(获得与大多数其他Python代码的一致性,允许已经渗入第三方库的东西)之后,Python风格指南的风格仲裁者决定改变规则。在限制它作为一个文件范围内的选择,对现有代码的豁免,以及让项目决定什么是最适合他们的自由度的情况下,Google Python风格指南被更新为允许蛇形命名。

流程

认识到事情需要改变,考虑到我们所追求的长寿命和扩展能力,我们创建了一个更新我们规则的流程。改变我们的风格指南的过程是基于解决方案的。风格指南更新的建议是以这种观点为框架的,确定一个现有的问题,并将建议的改变作为解决这个问题的方法。在这个过程中,"问题"不是可能出错的假设性例子;问题是在现有的谷歌代码中发现的模式。考虑到一个被证明的问题,因为我们有现有的风格指南决定背后的详细理由,我们可以重新评估,检查不同的结论现在是否更有意义。

编写受风格指南约束的代码的工程师群体,往往最能注意到何时需要改变规则。事实上,在谷歌,我们的风格指南的大多数变化都是从社区讨论开始的。任何工程师都可以提出问题或建议修改,通常从专门讨论风格指南的特定语言 邮件列表开始。

风格指南的修改建议可能是完整的,包括具体的、更新的措辞建议,也可能开始时是关于某个规则的适用性的模糊问题。社区对收到的想法进行讨论,从其他语言使用者那里得到反馈。一些建议被社区一致否决,因为它们被认为是不必要的,太含糊的,或没有好处的。另一些则得到了积极的反馈,被认为是有价值的,要么是原封不动的,要么是经过了一些建议的改进。这些建议,也就是那些通过社区审查的建议,将受到最终决策的批准。

风格仲裁者

在Google,每一种语言的风格指南都由风格指南的拥有者--我们的风格仲裁者做出最终决定和批准。对于每一种编程语言,一组长期的语言专家是风格指南的所有者和指定的决策者。一个特定语言的风格仲裁者通常是该语言库团队的高级成员和其他具有相关语言经验的长期Googlers。

任何风格指南变化的实际决策是对拟议修改的工程权衡的讨论。仲裁人在商定的目标范围内做出决定,而风格指南就是为这些目标进行优化的。修改不是根据个人喜好进行的;它们是权衡的判断。事实上,C++风格仲裁员小组目前由四个成员组成。这可能看起来很奇怪:拥有奇数的委员会成员可以防止在出现分歧决定时出现票数相同的情况。然而,由于决策方式的性质,没有什么是 "因为我认为应该这样"的,所有的事情都是一种权衡的评估,决定是通过共识而不是通过投票作出的。这个由四名成员组成的小组很好地发挥了作用。

例外情况

是的,我们的规则是法律,但也有一些规则需要例外。我们的规则通常是为更大的、一般的情况设计的。有时,特定的情况会受益于对某一规则的豁免。当这种情况出现时,会咨询风格仲裁员,以确定是否有有效的理由批准对某一特定规则的豁免。

豁免不会被轻易授予。在C++代码中,如果引入了一个宏API,风格指南规定它必须使用项目特定的前缀来命名。由于C++处理宏的方式,将它们视为全局名称空间的成员,所有从头文件导出的宏必须有全局唯一的名称,以防止冲突。关于宏命名的风格指南规则确实允许仲裁者对一些真正的全局性的实用宏给予豁免。然而,当要求排除项目特定前缀的豁免请求背后的原因是由于宏名称长度或项目一致性而产生的偏好时,豁免将被拒绝。代码库的完整性比项目的一致性更重要。

允许例外的情况是,允许破坏规则比避免破坏规则更有利。C++风格指南不允许隐式类型转换,包括单参数构造函数。然而,对于那些被设计成透明地包裹其他类型的类型,在底层数据仍然被准确地表示出来的情况下,允许隐式转换是完全合理的。在这种情况下,允许对不隐式转换规则的豁免。有这样一个明确的有效豁免案例,可能表明有关规则需要澄清或修改。然而,对于这条特定的规则,收到的豁免请求中,有足够多的请求看起来符合有效的豁免理由,但实际上并不符合--要么是因为相关的特定类型实际上不是透明的封装类型,要么是因为该类型是封装类型,但实际上并不需要,因此保持规则的原状仍然是值得的。

指导意见

除了规则之外,我们还策划了各种形式的编程指导,从对复杂主题的深入讨论到对我们认可的最佳实践的简短而尖锐的建议。

指南代表了我们工程经验的智慧结晶,记录了我们从一路走来的经验教训中提取的最佳实践。指导意见倾向于关注我们观察到的人们经常出错的事情,或者是不熟悉的、因此容易混淆的新事物。如果说规则是 "必须的",我们的指导就是 "应该的"。

我们培养的指导原则的一个例子是我们使用的一些主要语言的入门手册。我们的风格指南是规范性的,规定了哪些语言特征是允许的,哪些是不允许的,而入门指南是描述性的,解释了指南所认可的特征。它们的覆盖面很广,几乎涉及到了一个在谷歌刚开始使用语言的工程师所需要参考的每一个主题。它们没有深入研究某一特定主题的每一个细节,但它们提供了解释和推荐使用。当工程师需要弄清楚如何应用他们想要使用的功能时,这些入门手册旨在作为首选的指导性参考。

几年前,我们开始发布一系列的C++提示,提供一般的语言建议和谷歌特有的提示。我们涵盖了硬的东西--对象寿命、复制和移动语义、依赖参数的查找;新的东西--C++ 11的功能,因为它们在代码库中被采用,预先采用的C++17类型,如string_view、optional和variant;以及需要温和的纠正的东西--提醒不要使用using指令,警告要记住注意隐式bool转换。这些提示来自于遇到的实际问题,解决了风格指南中没有涵盖的真正的编程问题。他们的建议,与风格指南中的规则不同,不是真正的法则;他们仍然属于建议而不是规则的范畴。然而,鉴于它们是从观察到的模式而不是抽象的理想中发展起来的,它们的广泛和直接的适用性使它们从大多数其他建议中脱颖而出,成为一种 "普通的典范"。小贴士重点突出,相对较短,每条都不超过几分钟的阅读时间。这个 "每周提示"系列在内部非常成功,在代码审查和技术讨论中经常被引用。

软件工程师在进入一个新的项目或代码库时,对他们将要使用的编程语言有所了解,但对该编程语言在谷歌内部的使用情况缺乏了解。为了弥补这一差距,我们为每种主要的编程语言开设了一系列"@Google 101"课程。这些全天课程的重点是,在我们的代码库中,使用该语言的开发有何不同。它们涵盖了最常用的库和习语、内部偏好和自定义工具的使用。对于刚刚成为谷歌C++工程师的人来说,该课程填补了使他们不仅成为一名优秀工程师,而且成为一名优秀的谷歌代码库工程师的缺失。

除了教授旨在让完全不熟悉我们的设置的人快速运行的课程外,我们还为深入代码库的工程师培养了现成的参考资料,以便在旅途中找到可以帮助他们的信息。这些参考资料的形式各不相同,涵盖了我们所使用的各种语言。我们内部维护的一些有用的参考资料包括以下内容。

  • 针对通常较难正确的领域(如并发和散列)的特定语言建议。
  • 对语言更新所引入的新功能进行详细分解,并就如何在代码库中使用这些功能提出建议。
  • 列出我们的库所提供的关键抽象和数据结构。这使我们不至于重新发明已经存在的结构,并对 "我需要一个东西,但我不知道它在我们的库中叫什么"提供了一个回应。

应用规则

规则,就其性质而言,在可执行的情况下,具有更大的价值。规则可以通过教学和培训在社会上实施,也可以通过工具在技术上实施。我们在谷歌有各种正式的培训课程,涵盖了我们的规则所要求的许多最佳实践。我们还投入资源,保持我们的文件更新,以确保参考材料保持准确和最新。当涉及到对我们规则的认识和理解时,我们整体培训方法的一个关键部分是代码审查所发挥的作用。我们在Google实施的可读性流程--即通过代码审查指导新加入Google开发环境的特定语言的工程师--在很大程度上是为了培养我们的风格指南所要求的习惯和模式(见第3章中关于可读性流程的细节) 这个过程是我们如何确保这些实践被学习并跨越项目边界应用的重要一环。

尽管某种程度的培训总是必要的--工程师毕竟必须学习规则,这样他们才能写出遵守规则的代码--当涉及到检查合规性时,与其完全依赖基于工程师的验证,我们更愿意用工具来自动执行。

自动规则执行确保规则不会随着时间的推移或组织规模的扩大而被放弃或遗忘。新人加入;他们可能还不知道所有的规则。规则随着时间的推移而改变;即使有良好的沟通,也不是每个人都会记得所有东西的当前状态。项目成长并增加了新的功能;以前不相关的规则突然变得适用。工程师检查规则是否符合要求,要么依靠记忆,要么依靠文档,这两者都可能失败。只要我们的工具保持更新,与我们的规则变化同步,我们就知道我们的规则被我们所有的工程师应用于所有的项目。

自动执行的另一个优势是最大限度地减少了规则解释和应用的差异性。当我们写一个脚本或使用一个工具来检查合规性时,我们根据规则的单一、不变的定义来验证所有的输入。我们并没有把解释权留给每个工程师。人类工程师用他们的偏见来看待一切。无论是否有意识,潜在的、微妙的、甚至可能是无害的偏见,仍然会改变人们看待事物的方式。让工程师来执行,可能会看到对规则的不一致的解释和应用,可能会有不一致的责任期望。我们把更多的权力交给工具,留给人类偏见的切入点就越少。

工具化也使得执行工作可以扩展。随着一个组织的成长,一个专家团队可以编写公司其他部门可以使用的工具。如果公司规模扩大一倍,在整个组织中执行所有规则的努力不会增加一倍,它的成本和以前一样。

即使我们通过整合工具获得了优势,也不可能对所有的规则进行自动执行。一些技术规则明确地要求人类进行判断。例如,在C++风格指南中。"避免复杂的模板元编程"。"使用auto来避免嘈杂、明显或不重要的类型名称--在这种情况下,类型对读者的清晰度没有帮助。" "组合往往比继承更合适。" 在Java风格指南中。"对于如何[排列你的类的成员和初始化器],没有一个正确的配方;不同的类可能以不同的方式排列其内容。" "对捕获的异常不做任何反应是非常少见的。" "覆盖Object.finalize是极为罕见的。" 对于所有这些规则,都需要判断,而工具化不能(还不能!)取代这个位置。

其他的规则是社会性的,而不是技术性的,用技术性的解决方案来解决社会问题往往是不明智的。对于许多属于这一类的规则,细节往往不太明确,工具化会变得复杂而昂贵。把这些规则的执行留给人类往往更好。例如,当涉及到一个给定的代码更改的大小(即受影响的文件和被修改的行数)时,我们建议工程师倾向于小的更改。小的改动对工程师来说更容易审查,所以审查往往更快、更彻底。他们也不太可能引入错误,因为更容易推理出小改动的潜在影响和效果。然而,小的定义是有点模糊的。一个在数百个文件中传播相同的单行更新的变化实际上可能很容易审查。相比之下,一个较小的、20行的变化可能会引入复杂的逻辑,并产生难以评估的副作用。我们认识到有许多不同的尺寸测量方法,其中一些可能是主观的--特别是在考虑到一个变化的复杂性时。这就是为什么我们没有任何工具来自动拒绝一个超过任意行数限制的拟议修改。评审员可以(而且确实)在他们判断一个变化太大时进行反驳。对于这个和类似的规则,执行是由编写和审查代码的工程师来决定的。然而,当涉及到技术规则时,当它是可行的,我们赞成技术执行。

错误检查器

许多涵盖语言使用的规则可以通过静态分析工具来执行。事实上,我们的一些C++图书馆员在2018年年中对C++风格指南的非正式调查估计,大约90%的规则可以被自动验证。错误检查工具采取一套规则或模式,并验证一个给定的代码样本是否完全符合要求。自动验证消除了代码作者记忆所有适用规则的负担。如果工程师只需要寻找在代码审查过程中出现的违规警告--其中很多都有建议的修正--由一个已经紧密集成到开发工作流程中的分析器来完成,我们就可以把遵守规则的努力降到最低。当我们开始使用工具来标记基于源标签的废弃功能,并在原地出现警告和建议的修复,废弃API的新用途的问题几乎在一夜之间消失了。保持合规性的成本降低,使工程师更有可能愉快地贯彻执行。

我们使用clang-tidy(用于C++)和Error Prone(用于Java)等工具来自动执行规则的过程。关于我们的方法,见第20章的深入讨论。

我们使用的工具是为支持我们定义的规则而设计和定制的。大多数支持规则的工具是绝对的;每个人都必须遵守规则,所以每个人都使用检查规则的工具。有时,当工具支持最佳实践时,在符合惯例方面有更多的灵活性,有选择退出机制,允许项目根据他们的需要进行调整。

代码格式化器

在谷歌,我们通常使用自动样式检查器和格式化器来强制执行我们代码中的一致格式。行长的问题已经不再有趣了。工程师们只需运行样式检查器并继续前进。当格式化每次都以相同的方式完成时,它在代码审查中就成了一个无关紧要的问题,消除了审查周期,否则这些周期就会被用来发现、标记和修复小的风格缺陷。

在管理有史以来最大的代码库时,我们有机会观察由人类完成的格式化和由自动化工具完成的格式化的结果。平均而言,机器人比人类好很多。在一些地方,领域的专业知识很重要--例如,格式化一个矩阵,人类通常可以比通用格式化器做得更好。如果不是这样,用自动样式检查器来格式化代码也很少出错。

我们通过提交前的检查来强制使用这些格式化器:在代码可以提交之前,一个服务会检查在代码上运行格式化器是否产生任何差异。如果产生了,就会拒绝提交,并说明如何运行格式化器来修复代码。谷歌的大多数代码都要经过这样的预提交检查。对于我们的代码,我们对C++使用clang-format;对Python使用yapf的内部包装器;对Go使用gofmt;对Dart使用dartfmt;对BUILD文件使用buildifier。

案例研究:gofmt
谷歌于2009年11月10日将Go编程语言作为开放源代码发布。从那时起,Go已经成长为一种用于开发服务、工具、云基础设施和开源软件的语言。
我们知道,从第一天起我们就需要一个标准的Go代码格式。我们也知道,在开源发布之后,几乎不可能再加装一个标准格式。因此,最初的Go版本包括gofmt,这是Go的标准格式化工具。
动机
代码审查是软件工程的最佳实践,但在审查中却有太多的时间花在格式的争论上。尽管标准格式并不是每个人都喜欢的,但它足以消除这种时间上的浪费。 通过标准化格式,我们为能够自动更新Go代码而不产生虚假差异的工具奠定了基础:机器编辑的代码将与人类编辑的代码无法区分。
例如,在2012年Go 1.0之前的几个月里,Go团队使用了一个名为gofix的工具来自动更新1.0之前的Go代码,使其成为稳定版本的语言和库。由于gofmt的存在,gofix产生的差异只包括重要的部分:对语言和API的使用的改变。这使得程序员能够更容易地审查这些变化,并从工具的变化中学习。
影响
Go程序员希望所有的Go代码都是用gofmt格式化的。gofmt没有配置旋钮,其行为也很少改变。所有主要的编辑器和IDE都使用gofmt或模拟其行为,因此几乎所有的Go代码的格式都是一样的。起初,Go 的用户抱怨这个强制标准;现在,用户经常把 gofmt 作为他们喜欢 Go 的众多原因之一。即使在阅读不熟悉的Go代码时,其格式也是熟悉的。
数以千计的开放源码包都在阅读和编写Go代码。因为所有的编辑器和IDE都同意Go的格式,所以Go工具是可移植的,很容易通过命令行集成到新的开发者环境和工作流程中。
改造
2012年,我们决定在谷歌使用新的标准格式化器自动格式化所有BUILD文件:buildifier。BUILD文件包含用谷歌的构建系统Blaze构建谷歌软件的规则。一个标准的BUILD格式将使我们能够创建自动编辑BUILD文件的工具,而不破坏其格式,就像Go工具对Go文件所做的那样。
一位工程师花了六周时间才使谷歌20万个BUILD文件的重新格式化被不同的代码所有者所接受,在此期间,每周有一千多个新的BUILD文件加入。谷歌用于进行大规模修改的新生基础设施大大加速了这项工作。(见第22章)

结论

对于任何组织来说,尤其是对于像谷歌这样庞大的工程师队伍来说,规则可以帮助我们管理复杂性并建立一个可维护的代码库。一套共享的规则框住了工程流程,使其能够扩大规模并持续增长,从而保持代码库和组织的长期可持续性。

TL;DRs

  • 规则和指导应旨在支持对时间和扩展的适应性。
  • 了解数据,以便可以调整规则。
  • 不是所有的事情都应该成为规则。
  • 一致性是关键。
  • 在可能的情况下,自动执行。
Clone this wiki locally