引擎贡献者的最佳实践

前言

由于项目本身主要针对具有编程能力的用户, 因此Godot拥有大量具有贡献能力的用户. 尽管如此, 并非所有人都具有在大型项目或软件工程中工作的相同水平的经验, 这可能会在为项目贡献代码的过程中导致常见的误解和不良做法.

语言

本文档的范围是罗列让贡献者遵循的最佳实践,并且创建一种语言,供他们用来指代提交贡献过程中出现的常见情况。

尽管有些人可能认为将其扩展到通用软件开发很有用, 但我们的意图是仅将其限制在项目中最常见的情况下.

贡献大部分时间都归类为错误修复, 增强功能或新功能. 为了抽象化这个想法, 我们将它们称为 解决方案, 因为它们总是寻求解决可以描述为 问题 的问题.

最佳实践

#1: 这问题总是第一个

许多贡献者非常有创造力,他们只是享受设计抽象数据结构、创建漂亮的用户界面,或者纯粹喜欢编程。不管是什么情况,他们都会想出很酷的想法,但这些想法可能并不能真正解决任何实际问题。

../../_images/best_practices1.png

这些通常称为 寻找问题的解决方案. 在理想的情况下, 它们并不会有害, 但是实际上, 代码需要花费一些时间来编写, 占用空间作为源代码和二进制文件, 并且一旦存在就需要维护. 在软件开发中, 始终避免避免添加不必要的内容.

#2: 为了解决这个问题, 它必须放在开始位置

以前有过类似的尝试. 添加任何不必要的东西不是一个好主意, 但什么是必要的, 什么不是?

../../_images/best_practices2.png

这个问题的答案是, 问题需要确实存在, 才能真正解决. 不应该只是推测或认为问题存在. 用户使用软件的意图必须是创造一些他们 “需要” 的东西. 在这个过程中, 用户可能会遇到需要解决了才能继续进行的问题, 或者为了实现更高的生产率. 在这种情况下,*需要一个解决方案*.

坚信将来可能会出现问题,并且软件需要在问题出现时就已准备好解决它们,这被称为“保护未来”,其特点是思路如下:

  • 我想这对于用户来说是有用的….

  • 我认为用户最终需要…

这通常被认为是一个坏习惯, 因为试图解决当前不存在的问题往往会导致代码被编写但从未使用, 或者使用和维护起来比需要的复杂得多.

#3: 这问题有点复杂或频繁

软件是为解决问题而设计的, 但我们不能指望它能解决太阳底下存在的所有问题. 作为一个游戏引擎,Godot会为你解决问题, 所以它可以帮助你把游戏做得更好更快, 但它不会让整个游戏为你服务. 必须在某处划一条线.

../../_images/best_practices3.png

一个问题是否值得解决取决于用户解决问题的难度. 这种困难可以表示为:

  • 问题的复杂性

  • 问题发生的频率

如果问题对于大多数用户来说太复杂了,那么软件必须提供一个现成的解决方案。同样,如果问题很容易被用户变通,那么提供这样的解决方案是不必要的,应该让用户来做。

然而, 例外情况是, 当用户频繁地遇到这个问题时, 每次都要做简单的解决方案就成了一件麻烦事. 在这种情况下, 软件必须提供一个解决方案来简化这个用例.

根据我们的经验, 在大多数情况下, 当一个问题是复杂的或频繁的, 通常是显而易见的, 但可能出现的情况下, 绘制这条线是困难的. 这就是为什么总是建议与其他开发人员讨论(下一点).

#4: 这个解决方案必须与其他人讨论

经常发生的情况是,用户偶然发现问题时,他们只是沉浸在自己的项目中,所以他们自然会从自己的角度来解决问题,只考虑自己的用例。

正因为如此,用户提出的解决方案并不总是考虑开发人员经常意识到的其他用例,因此他们常常偏向于自己的需求。

../../_images/best_practices4.png

对于开发者来说, 视角是不同的. 他们可能会发现用户的问题过于独特, 无法证明解决方案的合理性(而不是用户的变通方法), 或者他们可能会建议一个适用于更广泛的已知问题的部分(通常更简单或更低级别)解决方案, 并将解决方案的其余部分留给用户.

无论如何, 在尝试贡献之前, 与其他开发人员或贡献者讨论实际问题是很重要的, 这样就可以在实现上达成更好的协议.

在这种情况下, 唯一的例外是当一个代码区域有一个明确的所有者(得到其他贡献者的同意)时, 该所有者直接与用户交谈, 并且最了解如何直接实现解决方案.

另外,Godot的理念是支持易用性和维护性, 而不是绝对性能. 性能优化将被考虑, 但是如果它们使得某些东西太难使用或者给代码库增加了太多的复杂性, 那么它们可能不会被接受.

#5: 针对每个问题, 都有自己的解决方案

对于程序员来说, 找到问题的最佳解决方案总是一个最令人愉快的挑战. 然而, 有时事情可能会过火, 程序员会试图想出解决尽可能多问题的解决方案.

当为了使这个解决方案显得更加奇妙和灵活, 纯粹的基于推测的问题(如#2所述)也出现在舞台上时, 情况往往会变得更糟.

../../_images/best_practices5.png

主要问题是,在现实中,它很少以这种方式工作。大多数情况下,只要为每个问题编写一个单独的解决方案,代码就会变得更简单,更易于维护。

此外, 针对个别问题的解决方案对用户来说更好, 因为他们找到的东西正是他们所需要的, 而不必学习和记忆一个更复杂的系统, 他们只需要简单的任务.

大而灵活的解决方案还有一个额外的缺点,那就是,随着时间的推移,它们很少对所有用户都有足够的灵活性,他们就会不断请求添加更多的函数(并使 API 和代码库变得越来越复杂)。

#6: 迎合常见的用例, 为罕见的用例敞开大门

这是前一点的延续, 这进一步解释了为什么这种思维方式和设计软件是首选.

如前所述(第2点), 我们(作为设计软件的人)很难真正理解所有未来的用户需求. 试图同时编写满足许多用例的非常灵活的结构常常是一个错误.

我们可能会想出一些我们认为很出色的东西, 但当它被实际使用时, 我们会发现用户甚至永远不会使用它的一半, 或者他们会要求一些不太符合我们最初设计的功能, 迫使我们要么扔掉它, 要么让它变得更复杂.

现在的问题是, 如何设计软件, 让用户得到我们知道他们需要的东西, 但又足够灵活, 让他们在将来做我们不知道他们可能需要的事情?

../../_images/best_practices6.png

这个问题的答案是, 为了确保用户仍然可以做他们想做的事情, 我们需要让他们访问一个 “低级API”, 他们可以用它来实现他们想要的, 即使这对他们来说是更大的工作, 因为这意味着重新实现一些已经存在的逻辑.

在现实场景中, 这些用例无论如何都是非常罕见的, 所以需要编写一个定制的解决方案是有意义的. 这就是为什么仍然要为用户提供基本的构建块来实现这一点很重要.

#7:偏好局部解决方案

当寻找一个问题的解决方法时, 例如实现新特性或者修复一个BUG, 有时最简单的方式是在核心代码层中, 添加数据或是一个新的代码函数.

这里的主要问题是, 在核心层中添加一些只在很远的地方使用的东西, 不仅会使代码更难理解(一分为二), 而且还会使核心API更大, 更复杂, 更难理解.

这是不好的, 因为核心api的可读性和干净性总是非常重要的, 因为它是新贡献者学习代码库的一个起点.

../../_images/best_practices7.png

想要做到这一点的常见理由是, 只需在核心层中添加一个hack, 通常代码就更少了.

尽管如此, 不建议这种做法. 通常, 解决方案的代码应更接近问题的根源, 即使它涉及更多的代码, 重复的代码, 更复杂的代码或效率较低的代码也是如此. 可能需要更多的创造力, 但这始终是建议之路.

#8: 不要用复杂的解决方案来处理简单的问题

并非每个问题都有一个简单的解决方案, 很多时候, 正确的选择是使用第三方库来解决问题.

由于 Godot 需要在大量的平台上发布,我们无法对库进行动态链接。相反,我们将它们捆绑在我们的源代码树中。

../../_images/best_practices8.png

结果, 我们对所输入的内容非常挑剔, 并且倾向于使用较小的库(实际上, 单头库是我们的最爱). 只有在别无选择的情况下, 我们最终将更大的东西捆绑在一起.

此外, 库必须使用足够的许可证才能包含在Godot中. 可接受的许可证的一些例子有Apache2.0, BSD, MIT, ISC和MPL2.0. 特别是, 我们不能接受GPL或LGPL下许可的库, 因为这些许可有效地禁止专有软件中的静态链接(Godot在大多数导出项目中都是这样分发的). 这个要求也适用于编辑器, 因为我们可能希望长期在iOS上运行它. 由于iOS不支持动态链接, 因此静态链接是该平台上的唯一选项.