Godot引擎贡献最佳实践
翻译自Godot引擎贡献指南,原文:best_practices_for_engine_contributors
#1 问题总要放在第一位
许多代码贡献者非常的有创造力,他们享受设计抽象数据结构,搭建优秀的交互界面的过程,又或许他们只是单纯地热爱编程。代码贡献者们总是有一些很酷的想法,但这些想法或许能、或许不能解决真实的问题。
这就被称为寻找问题的解决方案。在理想的世界,它们也许并不有害,但在现实世界中,代码是需要时间去完成的,它们占用空间,而且一经存在就需要时间去维护。避免一切不必要的事物,在软件开发中一直被视作一种好的实践方式。
#2 问题要先存在,才能被解决
关联文章:Imaginary Problems Are the Root of Bad Software
这是前一种实践方式的变体。添加一切不必要的事物不是一个好主意,但怎么决定什么是“必要的”,什么又不是呢?
答案是:问题要先存在,才能被解决。它不能是一种假想,或是一种认定。用户一定是在使用这个软件,来创造一些他们需要的东西。在此过程中,用户可能需要解决一个问题才能继续推进工作,或达到更高的产出。在这个情境下,一个解决方案就是必须的了。
“一些问题可能在未来发生,软件必须要在问题发生前做好准备”,这种想法被称为“面向未来”,以下就是一些很有象征性的想法:
- 我认为这个方案会对用户很有帮助...
- 我认为用户最终会需要一个...
这被普遍地认为是一个不好的习惯,因为试图解决一个当下不存在的问题,往往会产出一些从未被使用的代码,或者是一些比实际需要更复杂、更难维护的代码。
#3 问题需要是复杂或频繁的
软件的存在是为了解决问题,但是我们不能指望解决所有问题。作为一个游戏引擎,Godot会帮助你让游戏更好更快,但是它不能帮你把整个游戏做完。在这里,必须要划清界限。
问题是否值得解决取决于解决问题所需要的付出。需要的付出取决于:
- 问题的复杂度
- 问题的发生频率
如果解决问题对用户来说过于复杂,软件应该为这个问题提供一个完善的解决方案。同样的,如果一个问题对用户来说很容易解决,提供一个解决方案就显得没有必要了。
一个例外是,如果用户频繁地遇到相同的问题,每次都需要自己来解决就会形成困扰。这种情况下,软件应该提供一种解决方案来简化用例。
一般来说判断一个问题是否复杂或频繁是很容易的,但有时也可能很困难。这就是为什么(在下一点)我们会推荐与其他开发者进行讨论。
#4 问题需要与他人一起讨论
通常,当用户遇到问题时,他们往往沉浸在自己的项目中。这些用户会很自然地从他们的角度来解决问题,只考虑他们自己的用例。因此,用户提出的解决方案并不总是考虑所有的用例,往往偏向于用户自身的需求。
开发者的角度又不一样了。他们可能会用户的问题过于独特,以至于无法给出一个合适的解决方案,或者他们可能会推荐一个能解决广泛问题的解决方案(更简单的,或者更低层次的API)来解决部分问题,而将剩下的部分留给用户。
无论如何,在尝试做出贡献之前,与其他开发者或者贡献者讨论实际问题是非常重要的,这样可以在实现上达成更好的一致。
唯一的例外当一块代码有一位明确商定的负责人,他直接与用户对话,并且有着能够直接实现解决方案的丰富知识。
值得一提的是,Godot的设计理念是偏好易用与可维护性,而不是性能。性能优化也会被纳入考量,但若它们让事情变得过于复杂,或者给代码库添加了太多复杂度,它们是不会被允许的。
#5 给每个问题属于它自己的解决方案
对程序员来说,寻找解决问题的最优解决方案一直一个享受的挑战。但是也很容易做过头。有些时候,贡献者会试图提出能解决尽可能多问题的解决方案。
当程序员试图让解决方案看起来更巧妙灵活,纯粹基于假定推测的问题也粉墨登场(如#2中描述的),情况往往急转直下。
最主要的问题在于,现实生活中这样一般是行不通的。有时候,为每个问题提供它自己的独立的解决方案,往往能达到更精简,更可维护的代码。
更进一步地说,目标于解决独立问题的解决方案对用户来说是更好的。有确定目标的解决方案能够让用户找到他们确切需要的东西,而不需要为了简单的任务去学习一系列复杂的系统。
大而复杂的解决方案还有一个缺点在于,随着时间推移,它们仍然无法覆盖所有用户的需求。用户会提出更多需求,最终大大增加了API和代码库的复杂度。
#6 迎合常见用例,也为罕见特例留下可能
这是前一部分的延续,进一步展开说明了这种为什么倾向于这样思考和设计软件的形式。
在先前也提到过了(#2),对我们来说(作为设计软件者)准确地理解所有未来用户的需求是非常困难的。试图一次性地完成一个能够迎合众多用例的结构灵活解决方案,往往是一个错误。
我们可能会提出一些我们认为很优秀的解决方案,但随后就发现用户甚至连其中一般的功能都不会用到,或者他们需求的功能并不能很好地融入到我们的初始设计中。这就导致我们必须扔掉整套解决方案,或者让它变得更加复杂。
问题在于,我们要如何去设计软件,才能同时满足当下已知的用户需求,以及未来未知的用户需求?
答案即为,为了保证能够满足用户的需求,我们需要为他们提供可用的底层API,尽管对于他们来说可能需要重新实现一些已经存在的逻辑。
在现实生活中,这些用例无论如何都是非常罕见的,所以让用户去实现一个客制化的解决方案是有道理的。所以仍然将基础的构建块提供给用户,这一点是很重要的。
#7 倾向于本地解决方案
在我么们为问题寻求一个解决方案时,可能是实现新功能或者修复一个bug,很多时候最简单的方法就是往核心层代码中添加数据或者一个新的函数。
这里最主要的问题在于,往核心层中添加一些只会在很远地方(很上层)使用的代码,不止会让代码更难读懂,还会让核心API更大更复杂,同样更难理解。
这是很糟糕的,因为核心API的可读性和清晰度一直是至关重要的,有大量的代码都建立在它之上,同时它也是新进代码贡献者学习代码库的起点。
想要这样做的一个常见理由是,通常简单地往核心层加一个hack,代码改动量会比较小。
这样做是不被推荐的。一般来说,解决方案的的代码应该离发生问题的代码尽可能的近,尽管可能需要额外的,重复的,更加复杂或者更低效的代码。可能需要一些额外的创造性,但这一直是我们推崇的做法。
#8 不要为简单的问题使用复杂的罐装方案(canned solution)
不是每个问题都有简单的解决方案,大多数情况下,正确的选择是使用一个第三方库来解决问题。
因为Godot需要移植到很多硬件平台上,我们不能动态地链接库。我们必须把这些库静态链接,打包进源码中。
所以,我们对于接入地第三方库非常挑剔,我们倾向于更小的库(仅头文件的库是我们的最爱)。只有当别无选择的时候,我们才会将大的库打包进来。
库必须使用足够宽松的许可证才能被包含到 Godot 中。 可接受的许可证包括 Apache 2.0、BSD、MIT、ISC 和 MPL 2.0。 特别的是,我们不能接受根据 GPL 或 LGPL 许可的库,因为这些许可证实际上禁止专有软件中的静态链接(Godot大多数导出项目中都需要是静态链接的)。 这个要求也适用于编辑器,因为我们可能希望长期在 iOS 上运行它。 由于 iOS 不支持动态链接,因此静态链接是该平台上的唯一选择。