為複雜專案做出貢獻
原文由 Mitchell Hashimoto 于 發布,訂閱此部落格
身為一位經常維護與貢獻開源專案的人,我常被問到:你到底從哪裡開始?要如何著手一個新專案,才能做出有意義的改動?你又怎麼可能搞懂一個複雜專案的內部運作?
這些問題適用於任何軟體專案,無論是開源還是私有、業餘還是專業,我採取的方法在任何情況下都一樣。不過,專業工作有一個關鍵差異:你可以直接接觸到其他工程師,他們願意——甚至有義務——幫助你,而在開源專案中,你大多只能靠自己。
我已經建立了一套反覆用來處理複雜專案的模式,並在這篇文章中記錄下來。我不期待這個模式對每個人都有效,但希望它能幫助其他人更有信心去嘗試學習並參與複雜專案。
我用複雜專案這個詞來指稱任何實作無法被輕易理解的軟體專案。這個定義是主觀的;有些人認為是複雜的專案,另一些人可能不這麼認為,反之亦然。
步驟一:先成為使用者
要理解任何專案的內部運作,第一步就是先成為這個專案的使用者。你不需要成為專家級的使用者,但我個人認定完成這個階段的標準是,試著用這個專案做出一個真實的東西,即使它很小或很簡單。舉例來說,在參與Zig 程式語言之前,我就用它建立了好幾個真實的函式庫。
身為使用者,你會對專案的功能有廣泛的理解。閱讀參考文件和實際使用專案之間有著天壤之別,而建立一些小型的練習專案,正是彌合從理論到實務理解差距的重要一步。
此外,你會開始學習專案的慣用風格與寫法,這些構成了專案的文化基調,也有助於理解專案為何會以這種方式運作、為何會具備這些功能等等。這很重要,因為它能幫助你對其他參與專案的人產生同理心,也能作為判斷哪些改動對專案來說合理、哪些不合理的指引。
我也非常建議在這個階段加入社群。加入 IRC 或 Discord、參加在地聚會、觀看演講等等。花一段時間多聽少說。這裡的目標是培養同理心,並了解專案是如何運作的。我總是對自己光是看著別人學習就能學到這麼多感到驚訝。
步驟二:建置專案
學會如何建置專案,並取得可執行的二進位檔(或同等產物)。不用費心去理解建置系統、相依套件等等。只要依樣畫葫蘆地跟著指南、網站上的步驟,用任何你需要的方法,能夠在你的系統上穩定、反覆地從原始碼產生出可執行的二進位檔就好。
在學會建置專案之前,不要先去讀程式碼。我太常看到人們在還沒學會如何建置之前,就陷在試圖理解專案原始碼的泥沼中。對我來說,學習過程的一部分就是去實驗、去弄壞東西,而如果無法建置軟體專案,就很難去實驗和搞破壞。
不用擔心要建置出功能完整的版本。複雜專案常常有些功能只有在具備正確的相依套件、正確的系統、正確的設定等條件下才會啟用。如果是這種情況,完全不用在意這些。目標是先取得一個在你的系統上堪用的二進位檔。隨著你逐步完成後面的步驟,你會累積經驗與信心,再去追求功能更完整的建置。
在這個階段,我也建議學會如何執行測試套件並讓它通過。這會讓你在後續步驟中更容易去實驗和弄壞東西。複雜專案的測試套件往往也很複雜,所以有時這意味著只要讓其中一部分測試能跑起來就好——只要足夠讓你做實驗就行。
步驟三:學習熱路徑的內部運作
要學習內部運作,我喜歡用一種我稱為「向下追蹤,向上學習」的方法。
向下追蹤
我從某個功能或使用情境開始,由外而內去追蹤該功能所經過的程式路徑。在這個過程中,我會記下所經過的檔案、行號和函式,但我還不會嘗試去理解任何東西是如何運作的。這就是「向下追蹤」階段。
舉例來說,在研究 Zig 編譯器時,我從追蹤 zig build-exe 這個用來從 Zig 原始碼建置執行檔的指令開始。這個追蹤過程讓我找到了 zig CLI 的原始碼、build-exe 子指令,進而進入「Compilation」子系統,它接著會呼叫 lexer、parser 等等。除了追蹤路徑所需的部分之外,我並沒有去細讀實作細節。
透過這些追蹤筆記,你通常就能對某個功能的運作方式有個「全貌」的理解。根據檔名、函式名稱等等,你通常就能開始辨識出專案中的主要子系統。這有助於之後將學習過程切分成大小更合理的區塊。
不要試圖學會所有東西。我常見的錯誤是,人們試圖逐行讀完整個專案,結果迷失了數週甚至數月,最終感到氣餒。要保持專注,一次只學一個功能。
小技巧:在挑選功能時,選擇一個你身為使用者已經熟悉的功能。另外,如果可能的話,盡量挑一個表面上看起來很簡單的功能。例如,我為了學習編譯器而嘗試追蹤的第一個 Zig 程式,就是一個只做了兩數相加且完全沒有輸出的程式。
向上學習
追蹤完一個功能後,就該真正去學習各個已對應出的子系統是如何運作的。追蹤階段我是從最外層的進入點開始,例如 CLI 或 API 呼叫;而學習階段,我則傾向從最內層開始。
我選擇從最內層開始,是因為它通常是最基礎、抽象程度最低的部分。隨著你往上層層攀升,抽象程度也往往隨之增加,如果你不了解底層的組成部分,就會更難學習。
要開始學習某個特定的子系統,我會遞迴地「向下追蹤,向上學習」。我會先檢視公開、對外暴露的 API 介面,然後去了解每個 API 呼叫是如何運作的。這正是上層模組使用這個子系統的方式,所以這不僅能指引我該如何學習,也能在我往上探索時讓一切變得更清晰。
動手實驗、弄壞東西
在「向下追蹤,向上學習」的過程中,我發現透過實驗和弄壞東西來學習運作原理非常有幫助。這正是為何在嘗試閱讀內部程式碼之前,先學會如何建置專案是如此關鍵。
加入新的 log 敘述、實作一小段新功能、修改既有功能等等,然後重新建置專案,看看會發生什麼事。這也是真正檢驗你是否理解某個東西如何運作的好方法。
舉例來說,在學習 Zig 的 tokenizer 時,我加入了新的 token,看到它們確實被斷詞了,但接著發現 parser 失敗了。當我進展到下一個系統(parser)時,我就讓我的新 token 真正做點事。以此類推。
搭配其他媒體資源輔助
在這個階段,除了埋頭鑽研程式碼之外,也要用任何可取得的媒體資源來輔助:書籍、影片、部落格文章等等。如果有涵蓋內部運作的現成文獻,就去讀它!
然而,你不該期待光靠這些資料就能讓你成為專家。就像步驟一的「成為使用者」一樣,當你想「成為維護者」時,沒有任何東西可以取代親手弄髒手、實際去把玩原始碼。這又是理論與實務應用的另一個例子。
小技巧:如果沒有學習內部運作的資源,就試著自己寫一份吧!我當初就是這麼做,為 Zig 寫了關於Zig compiler internals的文章,因為我找不到類似且內容新穎的資源。書寫本身就是鞏固學習的好方法,也能幫助未來的貢獻者。
步驟四:閱讀並重新實作近期的 Commit
作為學習內部運作的最後一步,我會閱讀與我研究過的子系統相關的近期 commit,並檢驗自己是否完全理解為何要做出這些改動。這算是我學習過程中的「做課本後面的習題」階段。
我會查看整個專案的 commit 歷史,或是與我研究的子系統相關的特定檔案或資料夾的 commit 歷史。接著,我要嘛先研究解答(也就是 commit 中的變更),要嘛先看它修復了什麼 bug,然後嘗試自己去修,看看能否得出類似的解法。
為了「解題」,我會將儲存庫 checkout 到我要研究的 commit 的前一個 commit。我會重現它所修復的 bug(如果是 bug 修復),然後嘗試自己實作解法。最後,我會將自己的成果與維護者或貢獻者提交的 commit 做比較。
我給自己的唯一提示是所需的變更大小(VCS diff 上的 +/- 行數)。我建議一開始先避開需要超過 50 到 100 行變更的改動。
步驟五:做出一口大小的改動
我喜歡從小處著手,再逐步承接越來越大的任務。到了這個階段,你已經理解了專案中技術層面的部分,接下來該學習的是人的層面。這裡的目標是做出一個小改動,並了解貢獻與審核的流程。
最困難的部分通常是找到一個適合的小改動。這裡沒有特效藥。我會瀏覽 issues,尋找看起來對貢獻者友善的項目。我通常會經歷幾次錯誤的開始,或是完全放棄某個 issue 轉而嘗試其他項目。最終,我總會找到一個。尋找 issue、甚至修復 issue 所花的時間可能會長到令人沮喪,但這就是入場費。許多專案常會用「contributor friendly」這類標籤來引導新貢獻者。
如今大多數專案都會清楚地文件化其貢獻流程,所以一旦完成改動,就確實照著流程走。如果你已在先前的步驟中加入了社群,這正是個好機會去 ping 某個人、尋求幫助或請人幫你再次確認流程。
舉例來說,我對 Zig 的第一個貢獻是一個只有三行的改動,卻花了我兩個晚上、總共四、五個小時才完成(還不包括前面幾個步驟所花的時間)。如果這個 bug 今天再次出現,我幾分鐘內就能修好,但要達到那樣的熟練程度是需要時間的。
成功
到了這個階段,你已經學會了一個複雜專案,並成功做出了貢獻!
不要害怕複雜。我覺得太多工程師把那些典型被認為很複雜的專案——像是程式語言、瀏覽器、資料庫等等——看作是魔法,或是只有更高層次的人才能駕馭的東西。我喜歡提醒自己,所有的專案都是由其他人開始的。如果他們做得到,我也做得到。你也一樣可以。
希望透過分享我的方法,能讓其他人覺得複雜專案不再那麼遙不可及。
隨機一篇部落格
留言
登入後參與討論