參與引擎貢獻的最佳實踐

簡介

由於 Godot 主要是給有能力編寫程式的人用的,因此有非常多 Godot 使用者都有能力參與貢獻。雖然如此,但這些人之中並非所有人對於大型專案或是軟體工程都有相同的處理經驗,也導致了許多在為專案貢獻程式碼時常見的誤解與不良的實踐。

語言

本篇文章討論的範圍是有關貢獻者應遵守的最佳實踐列表,以及建立一個在提供貢獻的過程時能用來引述常見情形的語言。

雖然有的人認為這些最佳實踐可以沿伸到一般軟體開發上,但我們主要只想限定在於 Godot 專案中最常見的情況。

參與貢獻大部分的時間都是在處理 Bug 修正、改進功能與新增功能。為了將這個概念抽象化,我們在此稱為 解決方案 ,因為這些貢獻通常都是找出能解決所謂 問題 的方法。

最佳實踐

#1: 問題優先

有許多貢獻者都非常有創意,且享受設計抽象資料結構與建立優秀的使用者介面過程,或是喜愛程式設計。不管是遇到什麼問題,這些貢獻者都能想出許多好點子,但這些好點子不一定能解決實際的問題。

../../_images/best_practices1.png

這種情況通常稱為 找出問題的解決方案 (Solutions in Search of a Problem)。在理想的世界裡,找出問題的解決方案並不是有害的,但,實際上,要寫程式碼就必須要花費時間,而且原始碼跟二進位檔都會佔用空間,再來,一旦有了新的程式也就需要維護。在軟體開發上,最好能避免新增任何不需要的東西。

#2: 若要解決問題,則必須要先有問題

這個最佳實踐算是前一個的變體。雖然最好不要新增任何不需要的東西,但什麼東西算是需要的,什麼又算是不需要的呢?

../../_images/best_practices2.png

這個問題的回答,就是問題必須要先 存在 才能被解決。問題決不能只是臆測或某種信仰。使用者必須要以軟體被預期的使用方法使用軟體來產生 需求 。在這個過程中,使用者可能會陷入需要一種解決方案才能解決的問題,或是需要一個解決方案才能提升效率的狀況。這時候,就 需要解決方案

深信未來會出現問題,且認為軟體必須要在問題出現的時候準備好隨時被解決就稱為 防範未來 (Future proofing),通常會以下列這幾種想法出現:

  • 如果使用者能 ... 的話會很實用

  • 使用者之後會需要能 ...

嘗試解決 實際上不存在 的問題通常可以看作是一種壞習慣,且會導致寫出不會被用到的程式碼,或是寫出用法與維護起來比起需求還要更複雜的程式碼。

#3: 問題必須要複雜或頻繁

軟體是被設計來解決問題的,但我們不能指望軟體能解決 世界上所有的問題 。作為一個遊戲引擎,Godot 會負責解決問題,這樣使用者就能更好更快地製作遊戲,但 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 的存取,這樣使用者就能達成他們的目標,甚至對使用者來說更好,因為這樣一來就能通過低階 API 來重新實作一些已經存在的邏輯。

但不管怎麼說,在現實場景中這些使用情況是很少見的,所以讓使用者只能自行實作解決方案是很合理的。這也是為什麼提供使用者實作解決方案的基本要素很重要的原因。

#7: 解決方案必須位於相同層級

在尋找問題的解決方案時,如實作新功能或修正 Bug,有時候最簡單的方法就是將資料或新函式加到核心層級的程式碼中。

這個做法的主要問題就是,當我們加到核心層級上的東西只有某個離核心很遠的一個東西會用到,不但會讓程式碼很難追蹤 (一分為二),也會讓核心 API 更肥大、更複雜、且更難以直接理解。

這樣不好,因為想想有多少程式碼都仰賴核心 API 來運作,核心 API 是否易讀與是否清楚非常重要。另一個原因是因為核心 API 是新貢獻者開始瞭解程式碼的起點。

../../_images/best_practices7.png

會想將程式碼加到核心層級的原因通常是因為這麼做只要寫更少的程式碼。

因此,這種做法並不推薦。一般來說,解決方案的程式碼應該儘量靠近原本發生問題的地方,就算這樣會讓程式碼變得更多、出現重複、或是更複雜與更沒效率。可能需要發揮更多創意,但我們非常建議這麼做。

#8: 不要大炮打小鳥

並非所有問題都有簡單的解決方案,而且,很多時候,正確的選擇是使用第三方函式庫來解決問題。

由於 Godot 必須要發佈到大量不同的平台,所以我們不能直接動態地連結到函式庫上,而必須要將這些程式碼捆綁到我們的原始碼裡。

../../_images/best_practices8.png

因此,我們非常挑剔什麼東西能放進原始碼庫中,而且我們傾向使用小型的函式庫 (事實上,最好只有單一標頭檔)。除非是沒有其他選擇的情況,我們才會將大型的函式庫捆綁進來。

另外,函式庫也必須要使用足夠鬆散的授權條款才能夠被包含進 Godot。舉例來說,我們可以使用 Apache 2.0, BSD, MIT, ISC 與 MPL 2.0。更具體來說,我們不能使用 GPL 或 LGPL 授權的函式庫,因為這些授權條款特別禁止靜態連結至專屬軟體內 (也就是 Godot 大多數匯出專案發佈的方法)。這項要求也適用於編輯器,因為我們長期來看還想在 iOS 上執行。由於 iOS 不支援動態連結,因此靜態連結是 iOS 上唯一的選項。