引擎貢獻者最佳實踐
前言
Godot 擁有大量具備貢獻能力的使用者,因為這個專案本身主要面向會寫程式的人。但並不是每個人都具備大型專案或軟體工程的經驗,這在貢獻程式碼的過程中,容易導致常見的誤解與不良實踐。
用語
本文旨在列出貢獻者應遵循的最佳實踐,並建立一套可於貢獻過程中討論常見情境時使用的用語。
雖然一般化的軟體開發最佳實踐很有參考價值,但我們會聚焦於 Godot 專案中最常見的情境。
貢獻大致可分為修正錯誤、改善功能或新增功能。為了簡化說明,這些我們都稱為「解決方案」,因為它們都是針對某個可以被稱為「問題」的狀況來解決。
最佳實踐
#1:問題優先
許多貢獻者極具創意,享受設計抽象資料結構、打造良好使用者介面,或單純喜歡寫程式。不論動機為何,他們會想出許多點子,但這些點子未必能真正解決實際問題。
這類情況通常被稱為「為問題而尋找解決方案」。理論上這沒什麼壞處,但實際上,程式碼需要花時間撰寫、佔用空間,且一旦存在就需維護。避免加入不必要的內容,始終是軟體開發中的好習慣。
#2:解決問題前,必須先確認問題真的存在
這是前一個最佳實踐的變體。雖然說加入不必要的東西不好,但什麼叫做必要、什麼又是不必要?
這個問題的答案是,只有「確實存在」的問題才值得解決。問題不能只是臆測或想像。使用者必須以預期方式操作軟體、產生需求,在過程中遇到阻礙或為了提升效率產生問題,這時才真正需要解決方案。
認為未來可能出現的問題必須預先因應,這種想法稱為「未雨綢繆(Future proofing)」,常見表現如下:
我認為如果讓使用者可以…會很實用…
我覺得使用者以後一定會需要……
這通常被認為是壞習慣。試圖解決「目前根本不存在」的問題,常會導致寫出沒有人會用到的程式碼,或者讓系統比實際需求複雜、難以維護。
#3:問題必須夠複雜或夠常見
軟體的目的是解決問題,但我們不能期望它解決「所有」問題。Godot 作為一套遊戲引擎,可以幫助你更好、更快地開發遊戲,但它不會幫你做完一整個遊戲。我們必須劃出界線。
一個問題是否值得解決,可以從使用者是否容易繞過來判斷。判斷標準如下:
問題的複雜度
問題的發生頻率
如果問題對大多數使用者來說「太複雜」而難以自行解決,軟體就應該提供現成的解決方案。同理,若問題很容易繞過,那就沒有必要額外提供解法。
但有個例外:如果某問題「非常常見」,即使每次繞過都很簡單,久了也會造成困擾。這時候,軟體就應該提供簡化流程的解決方案。
通常我們很容易判斷問題是否複雜或常見,但有時也會遇到難分的情況。所以我們建議多和其他開發者討論(見下一點)。
#4:解決方案必須和他人討論
當使用者在自己的專案中遇到問題時,往往只會從自己的角度思考解決方法。因此提出的方案未必能兼顧所有使用情境,常常只符合個人需求。
開發者則會用不同角度來看待。他們可能覺得某個使用者的問題過於特殊而無需專門解法(而是建議繞過),或建議一個較簡單、較底層的通用方案,讓使用者自行處理剩下部分。
無論如何,在貢獻之前,務必和其他開發者或貢獻者充分討論實際遇到的問題,才能達成更好的實作共識。
唯一的例外是,若該區塊的程式碼有明確的負責人,且他直接與使用者溝通、最了解如何實作解決方法,則可直接由其決定。
此外,Godot 的理念是「易用性與可維護性」優先於絕對效能。我們會考慮效能優化,但如果因此讓東西變得難用或讓程式碼過於複雜,可能就不會採納。
#5:每個問題各有其最佳解法
對程式設計師來說,尋找最優解總是一大樂趣,但有時會做過頭,試圖提出一個能包山包海、解決無數問題的方案。
這種情況往往會更糟,因為為了讓解決方案看起來更厲害、更彈性,許多純粹臆測的問題(見#2)也會被硬塞進來。
其實現實中這種做法很少有效。大部分時候,針對各別問題寫獨立解決方案,反而讓程式碼更簡潔、更容易維護。
此外,針對特定問題設計的方案對使用者更友善。這讓使用者能直接找到剛好符合需求的功能,而不用為了單純任務去學更複雜的系統。
又大又彈性的解決方案還有一個缺點:長期來看,它們往往沒辦法滿足所有人的需求,導致使用者不斷要求加新功能,讓 API 與程式碼基礎越來越複雜。
#6:優先滿足常見需求,罕見需求保持彈性
這點是前述內容的延伸,進一步說明為何這種思維與設計方式比較好。
如前面(#2)提過,作為軟體設計者,我們很難預知所有未來的使用需求。嘗試一次設計能涵蓋所有情境的超彈性架構,通常會適得其反。
我們可能會自認設計得很棒,但事後卻發現使用者根本用不到一半功能,或他們需要的東西和原本設計沒法吻合,最後不是被迫放棄,就是被迫讓系統變更複雜。
那問題來了:我們要如何設計一套軟體,既能滿足「已知需求」,又能在未來適度開放給「目前未知的需求」?
答案是:為了讓使用者能做想做的事,我們必須提供「底層 API」讓他們自行實作,即使這樣會讓他們多花點功夫(因為可能得自行重寫部分邏輯)。
現實上,這類情境非常少見,所以讓使用者自行寫專屬解法是合理的。這就是為什麼要提供基礎組件,讓使用者有辦法客製化。
#7:優先採用局部解決方案
在尋找問題解法時,不論是新增功能還是修正錯誤,有時最簡單的做法就是直接在核心層增加資料或新函式。
這麼做的主要問題是,若加到核心層的東西只在遠端某一處才會被用到,不僅讓程式碼難以追蹤(被拆散),還會讓核心 API 變肥大、複雜、更難理解。
這樣不好,因為核心 API 需維持高度可讀性與清晰度,畢竟有大量程式碼依賴它。而且新進貢獻者學習程式碼時,通常也是從核心 API 開始。
會想直接加在核心層,通常只是因為這樣寫起來程式碼最少。
但這種做法並不建議。原則上,解決方案的程式碼應該盡量靠近問題發生處,即使這樣會讓程式碼變更多、重複、複雜或稍微沒那麼有效率。這也許需要更多創意,但這才是推薦的做法。
#8:不要用複雜解法處理簡單問題
不是每個問題都有簡單解法,很多時候,正確做法是引入第三方函式庫。
由於 Godot 需要支援多平台發行,我們無法動態載入函式庫,只能將其直接內嵌於原始碼樹中。
因此,我們對要加入的函式庫相當謹慎,傾向採用輕量級函式庫(最佳是單一標頭檔案)。只有在別無選擇時,才會考慮嵌入大型函式庫。
此外,函式庫的授權必須夠開放才能納入 Godot。可接受的授權條款如 Apache 2.0、BSD、MIT、ISC、MPL 2.0 等。我們無法接受 GPL 或 LGPL 授權的函式庫,因為它們不允許靜態連結到專有軟體(Godot 匯出專案大多如此發行)。這個要求也適用於編輯器,因為我們未來可能要讓編輯器在 iOS 上執行。由於 iOS 不支援動態連結,只能使用靜態連結。