Software Engineering at Google翻译-III-8-Style Guides and Rules(风格指南和规则 )

第8章


风格指南和规则

作者: Shaindel Schwartz


编辑: Tom Manshreck

参考:https://github.com/daizhenhong/swe-at-google/tree/main/Part_III_Processes/Style_Guides_and_Rules

欢迎大家来github上一起贡献!

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

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

我们收集我们定义的规则,编写代码时必须遵守的规则。接下来,在我们的编程风格指南中,它被视为经典。这里的“风格”可能有点用词不当,它暗示的是一个仅限于格式化实践的集合。我们的风格指南不止于此;它们是管理我们代码的一整套约定。这并不是说我们的风格指南是严格规定的;风格指导规则可能需要判断,比如使用“尽可能合理的描述性”名称的规则。相反,我们的风格指南是我们的工程师负责的规则的最终来源。

在我们的编程风格指南中,我们收集了我们定义的规则,编写代码时必须遵循的做和不做的规则,这些规则被视为标准。这里的“风格”可能有点用词不当,它暗示的是一个仅限于格式化实践的集合。我们的风格指南不止于此;它们是管理我们代码的一整套约定。这并不是说我们的风格指南是严格规定的;风格指导规则可能需要判断,比如使用“尽可能合理的描述性”名称的规则。相反,我们的风格指南是我们的工程师负责的规则的最终来源。

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

即使是我们自己的风格指南也存在很大的差异,这使得我们很难精确地描述一个风格指南应该涵盖什么内容。指导谷歌风格指南开发的决策源于保持代码库可持续性的需要。其他组织的代码库天生对可持续性有不同的要求,这就需要一套不同的定制规则。本章讨论了指导我们规则和指南开发的原则和过程,主要从谷歌的c++、Python和Java风格指南中抽取示例。

为什么需要规则

那么我们为什么要有规则呢?制定规则的目的是鼓励“好的”行为,阻止“坏的”行为。对“好”和“坏”的解释因组织而异,这取决于组织关心的是什么。这样的设计不是普遍的偏好;好与坏是主观的,是根据需要而定的。对于一些组织,“good”可能会促进支持小内存占用或优先考虑潜在运行时优化的使用模式。在其他组织中,“好”可能促进使用新语言特性的选择。有时,组织非常关心一致性,因此与现有模式不一致的任何东西都是“不好的”。我们必须首先认识到一个给定的组织的价值;我们使用规则和指导来鼓励和阻止相应的行为。

随着组织的发展,已建立的规则和指导方针形成了通用的编码词汇表。通用词汇表可以让工程师专注于他们的代码需要表达什么,而不是如何表达。通过塑造这种词汇,工程师会倾向于默认地、甚至是潜意识地去做“好的”事情。因此,规则为我们提供了广泛的杠杆作用,以便将共同的开发模式推向所需的方向。

定义规则

当定义一组规则时,关键问题不是“我们应该有什么规则?”我们要问的问题是:“我们想要实现的目标是什么?”当我们关注规则将服务的目标时,识别哪些规则支持这个目标,可以更容易地提取有用的规则集。在谷歌,风格指南作为编码实践的法律,我们不会问,“风格指南中包含什么?”而是“为什么要把一些东西放进风格指南?”我们的组织通过制定一套规范代码编写的规则获得了什么?

指导原则

让我们把事情放在背景中:谷歌的工程组织由3万多名工程师组成。工程师群体在技能和背景方面表现出巨大的差异。每天大约有6万份文件提交给超过20亿行代码的代码库,这些代码库可能会存在几十年。我们正在优化一套不同于大多数其他组织所需要的价值,但在某种程度上,这些关注是无处不在的——我们需要维持一个对规模和时间都有弹性的工程环境。

在这种情况下,我们规则的目标是管理开发环境的复杂性,保持代码库的可管理性,同时仍然允许工程师高效地工作。我们在这里做了取舍:帮助我们实现这一目标的大量规则确实意味着我们在限制选择。我们失去了一些灵活性,甚至可能会冒犯一些人,但权威标准带来的一致性和减少冲突的收益会胜出。

鉴于这一观点,我们认识到一些指导我们制定规则的首要原则,这些原则必须:

  • 尽职尽责
  • 为读者优化
  • 保持一致
  • 避免容易出错和令人惊讶的结构
  • 必要时对现实做出让步
规则必须发挥作用

并不是所有的东西都应该放在风格指南中。要求组织中的所有工程师学习和适应任何新规则的成本是非零的。有了太多的规则,2不仅会让工程师在编写代码时更难记住所有相关的规则,而且也会让新工程师更难学会他们的方法。更多的规则也会使维护规则集更具挑战性和更昂贵。
由此,我们有意选择不包括预期是不言而喻的规则。谷歌的风格指南不打算以律师的方式解释;仅仅因为某件事没有被明确宣布为非法,并不意味着它就是合法的。例如,c++风格指南没有规定禁止使用goto。c++程序员已经倾向于避免使用它,所以包含禁止使用它的显式规则将引入不必要的开销。如果只有一两个工程师犯了错误,那么通过创建新规则来增加每个人的精神负担是无法扩展的。

为代码阅读者优化

我们规则的另一个原则是优化代码的读者而不是作者。随着时间的推移,我们的代码被阅读的频率将远远高于编写的频率。我们宁愿代码是乏味的键入,而不是难以阅读。在我们的Python风格指南中,当讨论条件表达式时,我们认识到它们比if语句短,因此对代码作者来说更方便。但是,由于它们往往比更冗长的if语句更难让读者理解,所以我们限制了它们的使用。我们认为“读起来简单”比“写起来简单”更重要。我们在这里做了一个权衡:当工程师必须重复地为变量和类型输入可能更长的描述性名称时,前期的成本会更高。我们选择支付这笔费用,是因为它为所有未来的读者提供了可读性。

作为优先级划分的一部分,我们还要求工程师在他们的代码中留下明确的行为证据。我们希望读者在阅读代码时能够清楚地理解代码在做什么。例如,我们的Java、JavaScript和c++风格指南要求在方法重写超类方法时使用override注释或关键字。就算没有明确的设计证据,读者页很可能会明白这一意图,尽管这需要对每个阅读代码的读者进行更多的挖掘。

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

// 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中提供了足够的信息来推断它的交互。这种清晰的调用站点行为文档确保了代码片段的可读性和可理解性。我们的目标是进行局部推理,目标是清楚地了解在调用点发生了什么,而不需要查找和引用其他代码,包括函数的实现。

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

保持一致

我们对代码库一致性的看法类似于我们应用于谷歌办公室的哲学。由于工程人员数量庞大、分布广泛,团队经常被分散到不同的办公室,谷歌员工经常会发现自己要到其他站点去工作。尽管每个办公室都保留了自己独特的个性,融入了当地的风味和风格,但为了完成工作,所有的东西都被刻意保持不变。访问谷歌员工的徽章将与所有当地的徽章阅读器合作;任何谷歌设备都可以使用WiFi;任何一间会议室的视频会议设置都将具有相同的界面。谷歌员工不需要花时间去学习如何设置这些;他们知道,无论他们在哪里,他们的梦想都是一样的。在不同的办公室之间转换工作很容易,而且还能完成工作。

这就是我们在源代码中所追求的。一致性使任何工程师都能快速地进入代码库中不熟悉的部分并开始工作。一个本地项目可以有它独特的个性,但是它的工具是一样的,它的技术是一样的,它的库是一样的,而且都是Just Works。

一致性的优点

尽管不允许办公室定制徽章阅读器或视频会议界面可能会让人觉得受到限制,但一致性带来的好处远远大于我们失去的创作自由。代码也是如此:一致性有时可能会让人觉得受到限制,但它意味着更多的工程师用更少的努力完成更多的工作3:

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

在规模上

几年前,我们的c++风格指南承诺,几乎不会改变会使旧代码不一致的风格指南规则:“在某些情况下,改变某些风格规则可能有很好的理由,但我们仍然保持事物的原样,以保持一致性。”
当代码量比较小、旧代码和历史逻辑相对少的时候,这是有意义的。

当代码库变得更大、更老时,这就不再是需要优先考虑的事情了。这是(至少对于我们c++风格指南背后的仲裁人来说)一个有意识的改变:当改变这一点时,我们明确地声明c++代码库将不再是完全一致的,我们甚至也不打算这样做。
不仅要将规则更新到当前的最佳实践,而且还要要求我们将这些规则应用到已经编写的所有内容,这样的负担太大了。我们的大规模变更工具和过程允许我们几乎所有的更新
我们的代码遵循几乎每一个新的模式或语法,所以大多数旧的代码显示最新的批准的风格(见第22章)。然而,这种机制并不完美;当代码库变得足够大时,我们不能确保每一个旧的代码都符合新的最佳实践。对完美一致性的要求已经达到了一个点,这样的代价太大了。

设置标准 当我们提倡一致性时,我们倾向于关注内部一致性。有时,当地的惯例在全球惯例被采用之前就已经出现了,因此调整一切以适应全球惯例是不合理的。在这种情况下,我们提倡一致性的层次结构:“保持一致”从本地开始,在这里,给定文件中的规范先于给定团队的规范,后者先于更大项目的规范,后者先于整个代码库的规范。事实上,风格指南包含了许多明确遵守当地惯例的规则6,重视当地的一致性而不是科学技术的选择。
然而,对于一个组织来说,仅仅创建并遵守一套内部惯例是不够的。有时,应考虑到外部社会通过的标准。

间隔数量

谷歌的Python风格指南最初要求我们所有的Python代码都采用双空格缩进。外部Python社区使用的标准Python风格指南使用四空格缩进。我们早期的大部分Python开发都是直接支持我们的c++项目,而不是实际的Python应用程序。因此,我们选择使用双空格缩进,以与我们的c++代码保持一致,c++代码已经以这种方式格式化了。随着时间的推移,我们发现这种理论并不成立。编写Python代码的工程师读和写其他Python代码的频率要比读和写c++代码的频率高得多。每次我们的工程师需要查找或引用外部代码片段时,我们都要花费额外的精力。每次我们试图将代码片段输出到开源时,我们都经历了很多痛苦,花了很多时间来调和内部代码和我们想要加入的外部世界之间的差异。
当Starlark(一种基于python的语言,设计于谷歌,作为构建描述语言)有了自己的风格指南时,我们选择使用四间距缩进来与外界保持一致。7

如果惯例已经存在,那么组织与外部世界保持一致通常是一个好主意。对于小的,独立的,短命的努力,它可能不会有什么不同;内部一致性比发生在项目有限范围之外的任何事情都重要。一旦时间的推移和潜在的可伸缩性成为因素,代码与外部项目交互甚至最终与外部世界交互的可能性就会增加。从长远来看,坚持被广泛接受的标准可能会有回报。
避免容易出错和令人惊讶的结构(Avoid error-prone and surprising constructs)

我们的风格指南限制了我们使用的语言中一些更令人惊讶、不寻常或棘手的结构的使用。复杂的特征往往有一些不易察觉的缺陷,乍一看并不明显。在没有彻底了解其复杂性的情况下使用这些特性,很容易误用它们并引入错误。即使项目工程师很好地理解了一个结构,未来的项目成员和维护者也不能保证有相同的理解。
这就是我们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读取这些值,而是从远程过程调用(Remote Procedure Call, RPC)请求消息或数据存储读取这些值,那会怎么样呢?这种模糊化的代码可能会导致重大的安全缺陷,如果只是通过简单地验证返回值是否错误,这种缺陷很难被发现。测试和验证这样的代码也很困难。

Python的动态特性允许这样的行为,并且在非常有限的情况下,使用hasattr()getattr()是有效的。然而,在大多数情况下,它们只会造成混淆并引入错误。
尽管一些编程专家可能能通过这些高级语言特性完美地解决一些问题,但强大的特性通常更难理解,而且没有得到广泛的应用。我们需要所有能够操作代码库的工程师,而不仅仅是专家。它不仅支持新手软件工程师,而且对SRE来说也是一个更好的环境——如果SRE在调试生产中断,他们会跳入任何可疑的代码,甚至是用他们不熟练的语言编写的代码。我们更看重简化、简单、易于理解和维护的代码。

让步实用性 (Concede to practicalities)

用拉尔夫·沃尔多·爱默生的话说:“愚蠢的一致性是小心灵的妖怪。”在我们追求一致的、简化的代码库时,我们不能盲目地忽略其他所有东西。我们知道样式指南中的一些规则会遇到保证例外的情况,这是可以的。在必要的时候,我们允许对可能与我们的规则相冲突的优化和实际问题做出让步。
性能很重要。有时,即使这意味着牺牲一致性或可读性,它也只是为了适应性能优化。例如,虽然我们的c++风格指南禁止使用异常,但它包含了一条允许使用noexcept的规则,noexcept是一个与异常相关的语言说明符,可以触发编译器优化。互操作性也很重要。为特定的非google部分而设计的代码如果适合其目标,可能会做得更好。例如,我们的c++风格指南包含了一个通用CamelCase命名准则的例外,它允许对那些模仿标准库特性的实体使用标准库的snake_case风格8。c++风格指南还允许对Windows编程的豁免,在Windows编程中,与平台特性的兼容性需要多重继承,这对所有其他c++代码来说都是明确禁止的。我们的Java和JavaScript风格指南都明确指出,生成的代码(经常与或依赖于项目所有权之外的组件)超出了指南规则的范围。9一致性是至关重要的;适应是关键。

样式指南(The Style Guide)

那么,语言风格指南应该包含哪些内容呢?所有的风格指南规则大致分为三类:

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

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

执行最佳实践

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

我们的风格指南中实施的一些最佳实践旨在使源代码更具可读性。许多格式规则都属于这一类。我们的样式指南指定了何时以及如何使用垂直和水平空格,以提高可读性。它们还包括行长度限制和支撑对齐。对于某些语言,我们通过使用自动格式化工具来满足格式化需求——gofmt用于Go, dartfmt用于Dart。逐项列出格式化需求的详细列表,或者为必须应用的工具命名,目标是相同的:我们有一组一致的格式化规则,旨在提高应用于所有代码的可读性。

我们的风格指南还包括对新的和尚未被很好理解的语言特性的限制。我们的目标是在学习过程中,在一个功能的潜在缺陷周围预先安装安全围栏。与此同时,在每个人开始跑步之前,限制使用让我们有机会观察从我们观察的例子中开发和提取最佳实践的使用模式。对于这些新特性,在开始的时候,我们有时不确定应该给予适当的指导。随着选项的传播,希望以不同方式使用新特性的工程师与风格指南的所有者讨论他们的例子,要求允许在最初限制范围之外的其他用例。通过观察出现的放弃请求,我们了解了该特性是如何被使用的,并最终收集了足够多的例子来总结好的实践。在我们得到这些信息之后,我们可以回到限制性裁决,并修改它以允许更广泛的使用。

案例分析: 介绍 std::unique_ptr

当c++ 11引入std::unique_ptr(一种智能指针类型,表示动态分配对象的独占所有权,并在unique_ptr超出作用域时删除对象)时,我们的样式指南最初不允许使用该指针。对于大多数工程师来说,unique_ptr的行为是不熟悉的,而该语言引入的相关的move语义是非常新的,对大多数工程师来说,非常令人困惑。防止在代码库中引入std::unique_ptr似乎是更安全的选择。我们更新了工具来捕获对不允许类型的引用,并保留了现有的指导意见,建议使用其他类型的现有智能指针。 随着时间流逝,工程师有机会调整move语义的含义,我们也越来越相信使用std::unique_ptr直接符合我们的风格指南的目标。在函数调用站点上,std::unique_ptr所提供的关于对象所有权的信息使读者更容易理解该代码。引入这种新类型所增加的复杂性,以及随之而来的新的move语义,仍然是一个值得关注的问题,但是代码库长期整体状态的显著改进使得采用std::unique_ptr是一个值得的权衡。
确保一致性

我们的风格指南还包含了一些规则,涵盖了许多较小的内容。对于这些规则,我们做出并记录一个决定主要是为了做出并记录一个决定。这类规则中的许多规则都没有重大的技术影响。像命名约定、缩进间隔、导入顺序这样的东西:通常一种形式比另一种形式没有明确的、可衡量的技术优势,这可能是技术社区一直在争论它们的原因11。选择一个,我们就退出了无休止的辩论循环,可以继续前进了。我们的工程师不再花时间讨论两个空间和四个空间的对比。这类规则的重要部分不是我们为给定的规则选择了什么,而是我们选择的事实。

其他

除此之外,还有很多内容不在我们的风格指南中。我们试着专注于那些对我们代码库的健康状况有最大影响的事情。这些文档中绝对有一些没有详细说明的最佳实践,包括许多很好的工程建议:不要太灵活、不要fork代码库、不要重复造轮子等等。像我们的风格指南这样的文档并不能让一个完全的新手对软件工程有全面的理解——我们有一些假设,这是有意的。

变更Rules(Changing the Rules)

我们的风格指南不是静态的。与大多数事情一样,随着时间的推移,风格指导决策的环境和指导给定规则的因素很可能会改变。有时,情况的变化足以使人们需要重新评估。如果发布了新的语言版本,我们可能需要更新规则以允许或排除新的特性和习惯用法。如果一条规则导致工程师努力绕过它,我们可能需要重新检查该规则应该提供的好处。如果我们用来执行规则的工具变得过于复杂和难于维护,那么规则本身可能已经衰败,需要重新审视。注意规则何时准备好进行另一个检查是保持规则集相关和最新的过程的重要部分。

在我们的风格指南中,规则背后的决定是有证据支持的。在添加规则时,我们将花时间讨论和分析相关的利弊以及潜在的后果,并试图验证给定的更改是否适合谷歌运行的规模。谷歌风格指南中的大多数条目都包含了这些考虑,列出了在过程中权衡的利弊,并给出了最终裁决的理由。理想情况下,我们优先考虑这种详细的推理,并将其包含在每条规则中。

记录给定决策背后的原因,使我们能够在需要改变的时候识别出事情。随着时间的推移和环境的变化,以前做出的一个好的决定可能不是现在最好的决定。清楚地指出影响因素后,我们就能够确定何时与一个或多个因素相关的更改需要重新评估规则。

案例分析: CamelCase命名

在谷歌,当我们为Python代码定义初始风格指导时,我们选择使用CamelCase命名风格,而不是使用snake_case命名风格来命名方法名。尽管公共Python风格指南(PEP 8)和大多数Python社区使用了snake_case命名,但当时谷歌的大多数Python用法是为c++开发人员使用Python作为c++代码库之上的脚本层。许多已定义的Python类型都是相应c++类型的包装器,由于谷歌的c++命名约定遵循CamelCase风格,因此跨语言一致性被视为关键。 后来,我们开始构建并支持独立的Python应用程序。最经常使用Python的工程师是开发Python项目的Python工程师,而不是编写快速脚本的c++工程师。我们给我们的Python工程师造成了一定程度的笨拙和可读性问题,要求他们为我们的内部代码维护一个标准,但在每次引用外部代码时不断调整另一个标准。我们还让有Python经验的新员工更难以适应我们的代码库规范。 随着Python项目的发展,我们的代码与外部Python项目的交互越来越频繁。我们在一些项目中合并了第三方Python库,导致我们的代码库中混合了我们自己的CamelCase格式和外部偏爱的snake_case样式。当我们开始开源我们的一些Python项目时,在一个我们的惯例不墨守常规的外部世界中维护它们,既增加了我们的复杂性,也增加了社区对我们风格的警惕,他们觉得我们的风格令人惊讶,有些怪异。 提出这些论点后,讨论了成本(与其他谷歌代码失去一致性,谷歌人习惯了我们的Python风格)和好处(获得与大多数其他Python代码的一致性,允许已经泄漏到第三方库),Python风格指南的风格仲裁者决定改变规则。有了它被应用为文件范围内的选择的限制,现有代码的豁免,以及项目决定什么是最适合他们的自由,谷歌Python风格指南被更新为允许snake_case命名。
过程(The Process)

考虑到我们所追求的长生命周期和扩展能力,我们认识到事情需要改变,因此我们创建了一个更新规则的过程。改变我们的风格指南的过程是基于解决方案的。风格指南更新的建议是用这个视图来框定的,识别现有的问题,并将建议的更改作为修复问题的一种方法。在这个过程中,“问题”并不是可能出错的假设例子;问题是通过在现有谷歌代码中发现的模式来证明的。给定一个被证明的问题,因为我们已经在现有的风格指南决策背后有了详细的理由,我们可以重新评估,检查一个不同的结论现在是否更有意义。

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

关于风格指南更改的建议可能是完整的,包括建议的具体的、更新的措辞,或者可能以关于给定规则的适用性的模糊问题开始。社区会讨论新想法,并从其他语言用户那里获得反馈。一些提案被社区一致否决,被认为是不必要的、过于模糊的或无益的。另一些则收到了积极的反馈,被认为是有价值的,或者有一些建议的改进。这些通过社区评审的提案将受到最终决策批准的制约。

Style决策者

在谷歌,对于每种语言的风格指南,最终的决定和批准都是由风格指南的所有者——我们的风格仲裁者——做出的。对于每一种编程语言,一组长期的语言专家是风格指南的所有者和指定的决策者。特定语言的风格仲裁人通常是该语言库团队的高级成员,以及其他具有相关语言经验的长期谷歌员工。
任何风格指南更改的实际决策都是对所提议的修改的工程权衡的讨论。仲裁者在风格指南优化的一致目标上下文中做出决定。更改并非根据个人喜好;他们权衡判断。事实上,c++风格仲裁组目前由四个成员组成。这可能看起来很奇怪:如果委员会成员人数为奇数,就可以防止出现出现意见分歧的情况。然而,由于决策制定方法的本质,没有什么是“因为我认为它应该是这样的”,一切都是一种权衡,决策是通过共识而不是投票做出的。这个由四名成员组成的小组就这样愉快地运作着。

例外 (Exceptions)

没错,我们的规则就是法律,但也有例外。我们的规则通常是为更大的一般情况而设计的。有时,特定的情况会受益于对特定规则的豁免。当出现这种情况时,会咨询风格仲裁者,以确定是否存在授予某个特定规则豁免的有效案例。

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

允许例外的情况是,允许打破规则比避免打破规则更有益。c++风格指南禁止隐式类型转换,包括单参数构造函数。但是,对于那些设计成透明包装其他类型的类型,其中底层数据仍然是精确和精确表示的,允许隐式转换是完全合理的。在这种情况下,授予对no-implicit-conversion规则的豁免。对有效的豁免有这样明确的理由可能表明有关规则需要加以澄清或修订。然而,对于这个特定的规则,足够的豁免请求,接收符合有效的豁免情况但实际上不是因为特定类型的问题实际上并不是一个透明的包装器类型或因为类型是一个包装器但实际上并不需要保持地方原有的规则仍然是值得的。

指南(Guidance)

除了规则之外,我们还以各种形式提供编程指导,从对复杂主题的长而深入的讨论到对我们认可的最佳实践的短而有针对性的建议。

指导代表了我们收集的工程经验的智慧,记录了我们从一路上学到的教训中提取的最佳实践。指导倾向于关注我们观察到的人们经常出错的事情,或者不熟悉的新事物,从而导致困惑。如果规则是“必须”,那么我们的指导就是“应该”。

我们培养的一个指导库的例子是我们使用的一些主要语言的一套引物。虽然我们的风格指南是规定性的,规定了哪些语言特性是允许的,哪些是不允许的,但primer是描述性的,解释了指南认可的特性。他们的内容相当广泛,几乎涉及了谷歌中新接触语言使用的工程师需要引用的每一个主题。它们不会深入研究给定主题的每一个细节,但它们提供解释和推荐使用。当工程师需要弄清楚如何应用他们想要使用的功能时,primer的目的是作为指导参考。

几年前,我们开始发布一系列c++技巧,其中混合了通用语言建议和特定于google的技巧。我们将讨论硬的东西——对象生命期、复制和移动语义、依赖于参数的查找;新的东西——c++的11个特性在代码库中被采用,预先采用的c++的17种类型,如string_view、optional和variant;还有一些东西需要轻微的纠正——提醒不要使用using指令,提醒要记住寻找隐式bool转换。这些技巧来源于所遇到的实际问题,解决了样式指南中没有涉及的实际编程问题。与风格指南中的规则不同,他们的建议并不是真正的经典;他们仍然属于建议类,而不是统治类。然而,考虑到它们是从观察到的模式而不是抽象的理想中成长起来的方式,它们广泛而直接的适用性使它们有别于大多数其他建议,成为一种“共同准则”。这些小贴士的内容都比较狭隘,篇幅也相对较短,每一条都不超过几分钟的阅读时间。这个“每周技巧”系列在内部已经非常成功,在代码评审和技术讨论中经常被引用12

软件工程师进入一个新的项目或代码库时,已经掌握了他们将要使用的编程语言的知识,但却不知道如何在谷歌中使用编程语言。为了弥补这一差距,我们为使用中的每一种主要编程语言开设了一系列“@Google 101”课程。这些全天的课程关注的是,是什么使使用该语言开发的代码库与众不同。它们涵盖了最常用的库和习惯用法、内部首选项和自定义工具的使用。对于一个刚刚成为谷歌c++工程师的c++工程师,本课程填补了他们不仅是一个好的工程师,而且是一个好的谷歌代码库工程师的缺失。

除了教授旨在让完全不熟悉我们的设置和快速运行的课程外,我们还为深入代码库的工程师提供现成的参考资料,以找到可以帮助他们在工作中发挥作用的信息。这些引用的形式各不相同,并且跨越了我们使用的语言。我们内部维护的一些有用的参考资料包括:

  • 特定于语言的建议,通常很难获得正确的领域(如并发和哈希)。
  • 语言更新中引入的新特性的详细分解,以及如何在代码库中使用它们的建议。
  • 我们库提供的关键抽象和数据结构列表。这阻止了我们重新创建已经存在的结构,并提供了对“我需要一个东西,但我不知道它在我们的库中被称为什么”的响应。

应用Rules(Applying the Rules)

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

尽管一定程度的培训总是必要的——毕竟,工程师必须学习规则,这样他们才能编写遵循规则的代码——当涉及到检查符合性时,而不是完全依赖于基于工程师的验证,我们强烈倾向于使用工具自动化执行。

自动化规则实施确保规则不会随着时间的推移或组织的规模扩大而被删除或遗忘。新的人加入;他们可能还不知道所有的规则。规则会随着时间而改变;即使有良好的沟通,也不是每个人都能记住所有事情的当前状态。项目不断发展并添加新功能;以前不相关的规则突然适用了。工程师检查规则遵从性取决于内存或文档,这两者都可能失败。只要我们的工具保持最新,与我们的规则更改同步,我们就知道我们的规则被我们所有的工程师应用于我们所有的项目。自动化实施的另一个优点是将解释和应用规则的差异最小化。当我们编写脚本或使用工具检查符合性时,我们根据单个不变的规则定义验证所有输入。我们不会让每个工程师来解释。人类工程师用带有偏见的视角看待一切事物。无论是否是无意识的,潜在的微妙的,甚至可能是无害的,偏见仍然会改变人们看待事物的方式。让工程师来执行很可能导致对规则的解释和应用不一致,也可能导致对责任的期望不一致。我们授权给工具的越多,留给人类偏见进入的入口就越少。

工具还使执行具有可伸缩性。随着组织的成长,一个专家团队就可以编写公司其他部门都可以使用的工具。如果公司的规模扩大一倍,在整个组织内执行所有规则的努力不会增加一倍,它的成本与以前差不多。

即使具有我们通过合并工具获得的优势,也不可能对所有规则进行自动化执行。一些技术规则明确要求人的判断。例如,在c++风格指南中:“避免复杂的模板元编程。”“使用auto来避免类型名称过于嘈杂、明显或不重要——在这种情况下,类型不能帮助读者清晰地理解。“组合通常比继承更合适。”在Java风格指南中:“对于如何[排序你的类的成员和初始化式]并没有一个正确的方法;不同的类可能以不同的方式排列内容。”“对所捕获的异常不做任何响应很少是正确的。"极少覆盖Object.inalize。"对于所有这些规则,判断是必需的,工具还不能代替判断。

其他规则是社会性的而不是技术性的,用技术性的解决方案来解决社会性问题通常是不明智的。对于这个类别下的许多规则,细节往往定义得不太好,工具将变得复杂和昂贵。让人类来执行这些规则通常会更好。例如,当涉及到给定代码变更的大小时(即,受影响的文件数量和被修改的行数),我们建议工程师喜欢较小的变更。对于工程师来说,小的变更更容易进行审查,因此审查往往更快、更彻底。它们也不太可能引入bug,因为更容易推断出较小更改的潜在影响和效果。然而,“小”的定义有些模糊。将相同的单行更新传播到数百个文件的更改实际上可能很容易检查。相比之下,一个较小的20行修改可能会引入复杂的逻辑,并产生难以评估的副作用。我们认识到有许多不同的衡量尺度,其中一些可能是主观的——特别是当考虑到变化的复杂性时。这就是为什么我们没有任何工具来自动拒绝超过任意行限制的建议更改。如果评审者认为一个变化太大,他们可以(而且确实会)推迟。对于这种规则和类似的规则,执行取决于编写和审查代码的工程师。然而,当涉及到技术规则时,只要它是可行的,我们就支持技术强制执行。

错误检查(Error Checkers)

许多涉及语言使用的规则可以通过静态分析工具强制执行。事实上,我们的一些c++图书管理员在2018年年中对c++风格指南进行的一项非正式调查估计,其中大约90%的规则可以自动验证。错误检查工具采用一组规则或模式,并验证给定的代码示例是否完全符合。自动验证消除了代码作者记住所有适用规则的负担。如果工程师只需要查找违规警告(其中许多都带有建议的修复程序),那么我们就可以将遵守规则所需的努力最小化。当我们开始使用工具基于源代码标记来标记已弃用的函数时,警告和建议的就地修复都浮出水面,弃用api的新用法问题几乎一夜之间就消失了。降低合规成本使工程师更有可能愉快地贯彻执行。

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

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

代码格式器(Code Formatters)

在谷歌,我们通常使用自动样式检查器和格式化器来强制代码内的格式一致。行长度的问题已经不再有趣工程师只是运行风格检查程序13,并继续前进。如果每次都以相同的方式进行格式化,那么在代码审查期间就不会出现问题,从而消除了用来查找、标记和修复小样式错误的审查周期。

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

我们通过预提交检查来强制使用这些格式化器:在代码可以提交之前,服务会检查在代码上运行格式化器是否会产生任何差异。如果是,提交将被拒绝,并说明如何运行格式化程序来修复代码。谷歌上的大多数代码都要接受这种预提交检查。在我们的代码中,c++使用了clang-format;Python的yapf内部包装器;Go的gofmt;Dart的dartfmt;和BUILD的buildifier。

案例分析:gofmt

谷歌于2009年11月10日以开源方式发布了Go编程语言。从那时起,Go 已经发展成为一种开发服务、工具、云基础设施和开源软件的语言14

我们从一开始就知道我们需要一个Go代码的标准格式。我们也知道,在开源版本发布后,几乎不可能再改进标准格式。因此,最初的Go版本包括gofmt,这是Go的标准格式工具。
Motivations

代码评审是软件工程的最佳实践,但是太多的时间被花在评审中争论格式。虽然标准格式不是每个人都喜欢的,但它足以消除这些浪费的时间。15

通过标准化格式,我们为自动更新Go代码而不产生虚假差异的工具奠定了基础:机器编辑的代码将与人工编辑的代码区分开来。16

例如,在2012年Go 1.0发布之前的几个月里,Go团队使用了一个名为gofix的工具,自动将1.0之前的Go代码更新为Go语言和库的稳定版本。多亏了gofmt, diffs gofix只包含了重要的部分:语言和api使用的变化。这允许程序员更容易地检查更改并从工具所做的更改中学习。
Impact

Go程序员希望所有的Go代码都使用gofmt格式。Gofmt没有配置旋杆,它的行为很少改变。所有主要的编辑器和ide都使用gofmt或模仿它的行为,所以几乎所有的Go代码的格式都是相同的。一开始,Go用户抱怨强制性的标准;现在,用户经常把gofmt作为他们喜欢Go的众多原因之一。即使阅读不熟悉的Go代码,格式也很熟悉。

成千上万的开源包读写Go代码因为所有的编辑器和idea都支持Go格式17,所以Go工具是可移植的,并且很容易通过命令行集成到新的开发环境和工作流中。

Retrotting
在2012年,我们决定使用一个新的标准格式器来自动格式化谷歌中的所有BUILD文件:buildifier。BUILD文件包含了使用Blaze(谷歌的构建系统)构建谷歌软件的规则。标准的BUILD格式将使我们能够创建自动编辑BUILD文件而不破坏其格式的工具,就像Go工具对Go文件所做的那样。

一名工程师花了六周时间,才让谷歌的20万个BUILD文件被不同的代码所有者接受,在此期间,每周都要添加超过1000个新的BUILD文件。谷歌为进行大规模变革而建立的基础设施大大加快了这一努力。(第22章)。

总结(Conclusion)

对于任何组织,尤其是像谷歌的工程团队这么大的组织,规则帮助我们管理复杂性并构建一个可维护的代码库。一组共享的规则框定了工程过程,以便它们能够扩展和持续增长,保持代码库和组织的长期可持续性。

内容提要(TL;DRs)

  • 规则和指南的目标应该根据时间和规模做出调整
  • 了解具体情况以便对规则进行调整
  • 不是所有的事情都应该成为规则
  • 一致性是关键
  • 尽可能自动化执行规则校验

  1. 我们的许多风格指南都有外部版本,你可以在 https://google.github.io/styleguide上找到。我们在本章中引用了这些指南中的许多例子。 ↩︎

  2. 工具很重要。衡量“太多”的标准不是游戏规则的原始数量,而是工程师需要记住多少规则。例如,在clang格式之前的糟糕日子里,我们需要记住大量的格式规则。这些规则并没有消失,但使用我们目前的工具,遵守这些规则的成本已经大幅下降。我们已经达到了这样一个阶段:有人可以添加任意数量的格式规则,而没有人会在意,因为工具会为你做这些 ↩︎

  3. 这要归功于H.Wright在访问并对比了大约15个不同的谷歌办公室做出的基于真实性的比较 ↩︎

  4. Chunking是一种认知过程,它将信息碎片组合成有意义的“块”,而不是逐个记录。例如,国际象棋专家考虑的是棋子的配置,而不是单个棋子的位置。 ↩︎

  5. See 4.2 Block indentation: +2 spaces, Spaces vs. Tabs, 4.4 Column limit:100 and Line Length. ↩︎

  6. use the contest,示例 ↩︎

  7. 7 Style formatting for BUILD files implemented with Starlark is applied by buildifier. See https://github.com/bazelbuild/buildtools. ↩︎

  8. See Exceptions to Naming Rules. As an example, our open sourced Abseil libraries use snake_case naming for
    types intended to be replacements for standard types. See the types defined in https://github.com/abseil/abseilcpp/blob/master/absl/utility/utility.h. These are C++11 implementation of C++14 standard types and therefore
    use the standard’s favored snake_case style instead of Google’s preferred CamelCase form. ↩︎

  9. See Generated code: mostly exempt. ↩︎

  10. See https://google.github.io/styleguide/cppguide.html#Comments, http://google.github.io/styleguide/pyguide#38-
    comments-and-docstrings, and https://google.github.io/styleguide/javaguide.html#s7-javadoc, where multiple
    languages define general comment rules. ↩︎

  11. Such discussions are really just bikeshedding, an illustration of Parkinson’s law of triviality ↩︎

  12. https://abseil.io/tips has a selection of some of our most popular tips ↩︎

  13. When you consider that it takes at least two engineers to have the discussion and multiply that by the number
    of times this conversation is likely to happen within a collection of more than 30,000 engineers, it turns out
    that “how many characters” can become a very expensive question. ↩︎

  14. In December 2018, Go was the #4 language on GitHub as measured by pull requests. ↩︎

  15. Robert Griesemer’s 2015 talk, “The Cultural Evolution of gofmt,” provides details on the motivation, design,
    and impact of gofmt on Go and other languages. ↩︎

  16. Russ Cox explained in 2009 that gofmt was about automated changes: “So we have all the hard parts of a pro‐
    gram manipulation tool just sitting waiting to be used. Agreeing to accept ‘gofmt style’ is the piece that makes
    it doable in a finite amount of code.” ↩︎

  17. The Go AST and format packages each have thousands of importers. ↩︎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值