Skip to content

CHAPTER 8 Style Guides and Rules

kimi edited this page Aug 12, 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是一个值得的折衷。

建立一致性

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

而对于其他一切...

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

Clone this wiki locally