我打造大型技術專案的方法
無論是從零開始打造全新專案、實作大型功能,或是展開大規模重構,要保持動力並完成大型技術專案往往並不容易。對我而言,一個非常有效的方法,就是持續看到實質的成果,並以此為依據來安排工作順序。
我們都體驗過剛開始新專案時的那種興奮感。頭幾個星期迫不及待想坐在電腦前投入工作。然後隨著時間慢慢過去,你開始分心,或找藉口愈做愈少。如果這是正職工作,你會硬撐著痛苦地一步步熬到終點,每天都很煎熬。如果只是為了興趣,幾年後回頭看,只會想起當初本來可能達成的成果。
我發現,當我把大型任務拆解成一個個能帶來具體、可見進展的區塊時,往往更能完成工作,並在整個專案過程中保持熱情。每個人受激勵、被驅動的方式都不同,所以這個方法不一定適合你,但就普遍而言,我還沒遇過不會為一場精彩的展示(demo)感到興奮的工程師。而目標,就是要不斷給自己一場精彩的 demo。
我並不是說這篇文章提到的任何觀點有多新穎。它確實與許多常見的軟體工程或管理實務有共通之處。我只是想分享自己處理大型技術工作時所採取的方式,以及為何會這樣做。
在這篇文章中,我會以我的終端模擬器專案作為範例貫穿全文,這樣才能分享真實、具體的經驗。其實也有很多其他專案可以舉例,但我選擇這個,是因為它與我的本職工作無關,而且是近期完成的,記憶猶新。
我想非常明確地說明,我並不是要指責那些沒有完成專案的人。只要你樂在其中、感到有成就感(或者根本不在乎有沒有完成),那就很好,也祝福你。這篇文章是寫給那些更想把專案完成的人,或是單純想了解我是如何努力讓專案更能善終的人。
起跑點
一開始,你手上有個大型專案,必須想清楚該如何開始。對我來說,這是最困難的部分,我常常會花上好幾個小時——有時甚至好幾天——猶豫不決,不斷糾結於正確的起點。
以我的終端模擬器來說,有許多我知道若要完成專案就必須具備的大型元件:terminal parsing(終端解析)、執行與管理 shell 行程、字型渲染、格點渲染、輸入處理(鍵盤/滑鼠)等等。在通往「完成」的路上,有數百個相對龐大的子專案。
如果我一開始的目標就是做出一個可以執行 Neovim 的可啟動終端,那我肯定會陷入大麻煩。即使考量到未知的未知(unknown unknowns),這個目標聽起來就太過龐大。我可以直覺地意識到,在這條路上有太多元件:渲染 GUI、行程啟動、終端解析與狀態管理。這是個糟糕的目標,它太大了,我大概一、兩個月就會失去興趣。
相反地,我會去思考什麼樣的專案是務實可行的,並且能讓我盡快看到成果。一旦套用這個篩選標準,可行的子專案數量就會大幅縮小。以下是幾個例子:
- VT Parsing - 解析終端機的逸出序列
- 空白視窗渲染 - 開啟一個視窗並繪製空白畫布
- 子行程啟動 - 啟動子 shell,例如 bash、zsh、fish,設定 TTY 並能夠讀取其輸出(例如初始的 shell 提示字元)
在這個階段,我不會試圖列舉所有大型子專案。我只是大致掌握專案未來的輪廓,然後找出一個可以獨立建構、並且能實際看到某種具體成果的項目。
這是在經驗最能發揮作用的階段。經驗較豐富的工程師通常更能有效地描繪出專案未來的輪廓。他們能更準確地辨識出各種子元件,並看出這些部分如何組合在一起。在經驗較少、或在不熟悉的領域中,我就只能盡力猜測,並預期有更高的機率在某個時間點會把已完成的工作丟棄重來。
早期的成果
早期的工作往往不太直觀可見,這使得要看到具體成果似乎很困難。舉例來說,如果我選擇先處理終端機的 VT parsing,我就無法在沒有串接某種 UI 的情況下親眼看到它運作。或者,如果在其他專案中我選擇先處理資料庫結構描述與最精簡的 API,同樣也需要撰寫客戶端加上 CLI 或 GUI,才能看到成果。
如果一開始選擇的子專案是 UI,當然就能很快看到成果!基於各種原因,我很少從前端開始,通常會先從後端著手。而無論如何,你最終還是會做到後端,並遇到類似的挑戰。
要跨過這個階段,最好的工具就是自動化測試(在這個階段通常是單元測試)。自動化測試讓你能實際執行程式碼並看到它正常運作,同時也有助於養成良好的開發習慣。
這也為你挑選最初幾項任務提供了另一個指引:如果是非圖形化的工作,你會想挑選那些不需要太多麻煩就能測試的項目,這樣才能看到成果。
以我的終端機為例,我決定先從 VT parsing 開始,因為當時那是我對終端機比較不熟悉的部分,而且感覺是非常容易測試的項目:給它一些字串作為範例輸入,預期得到某個解析後的動作或事件作為輸出。
看到「1 項測試通過」、「4 項測試通過」、「13 項測試通過」這樣逐步累積的過程,對我來說非常令人興奮。我正在執行自己寫的程式碼,而且它真的動起來了。同時我也知道,自己正在大型專案中某個關鍵子元件上取得進展。
衝刺做出展示
我在早期子專案中的目標,並不是要打造出完整成品的子元件,而是要做出夠用就好的子元件,好讓我能繼續往通往展示的下一步前進。✨
這種取捨不僅體現在功能上,也可能體現在演算法或設計層面的考量。例如,你可能知道未來需要使用真正的資料庫、複雜的資料結構或支援串流資料。但在最初的一批工作中,你完全可以只使用記憶體內資料、內建的資料結構(例如 dictionary),並要求所有輸入/輸出一次到位。
我認為這是一個重要的取捨,所以我要再重複一次:別讓追求完美成為進步的阻礙。更進一步說,不要讓那些你知道未來勢必得做的改進阻礙你前進到下一件事。目標是做出展示。
無論我在做什麼,我都會試著每週做出一到兩個展示,並穿插如前一節所述的自動化測試回饋。
打造展示也能為你帶來無價的產品回饋。即使功能尚未完整,你也能很快直覺地感受到某個東西用起來是否順手。這些並不是「minimum viable products(最小可行性產品)」,因為它們其實還稱不上可行,但已足以讓工程師進行有價值的自我反思。
這是一個我認為經驗反而會造成傷害的領域。我見過資深工程師執著於打造完美的東西,等到終於做出展示時,才發現成果很糟。糟的不是實作本身,而是產品或功能本身就很糟。
回想一下,在終端機專案中我選擇的第一項任務是 VT parsing。在早期階段,我只看到自動化測試在運作。為了做出第一個展示,我寫了一個 shell script,它會執行某個指令、擷取其輸出、餵給我的 VT parser,然後輸出所有解析成功(或失敗)的內容。隨著時間推移,我把這個 CLI 當作最初的「UI」來反覆改進——我會用 ASCII 來渲染終端機格點。
這帶給我極大的滿足感,因為我可以執行像 man 或 ls 這類簡單的程式,或像 vim 這類較複雜的程式,並看到我的解析器如何運作(或崩潰,這本身也同樣令人興奮)。
在這個情境中,我寫的這個 CLI 從長遠來看相對沒什麼用(我很快就把它丟掉了)。但花上一、兩天把它當作展示做出來,卻帶給我一種重要的進展感,而親眼看到東西動起來,也有助於保持我的動力。
為自己而做
這個章節比較適用於個人專案,而非公司指派的工作專案。即使你期望將軟體發布給他人使用,也要只在需要時才做你需要的功能,並盡快開始使用自己的軟體。
我在處理自己親身遇到的問題時,總是更有動力1。而且,如果一個為你自己設計的產品連你自己都覺得不好用,那它很可能對別人來說也不好用。因此,我從展示到真正可在現實世界中使用的產品的路徑,就是找出只打造我認為自己需要的功能的最短路徑。
以我的終端機來說,這意味著要先能載入我的 shell 設定(fish),並從那裡啟動並使用 Neovim。所以我讓所有工作都直奔只為此所需的功能:只處理那些程式會用到的逸出序列、只渲染我日常使用的字型等等。我初期省略的功能範例包括:捲動、滑鼠選取、搜尋、分頁/分割視窗等等。
接著我開始把自己的終端機當作日常主力工具來使用。這個步驟通常會經歷幾次失敗的嘗試;你會發現自己其實需要某個被省略或遺忘的功能。在我最初幾次使用自己的終端機時,我發現方向鍵完全沒反應,還有一些細微(但會中斷工作流程)的渲染錯誤等等。於是我又暫時放棄使用它,但這些經驗給了我接下來要處理的具體任務。
此外,使用自己寫的程式碼所打造的軟體,總是讓我感到非常自豪,這通常也有助於我保持繼續投入的動力。
總結整理
將大問題拆解成小問題。重要的是,每個小問題都必須有某種能讓你清楚看到工作成果的方式。
只把小問題解決到足以推進大問題中與展示相關的部分,然後就轉向下一個小問題。
只解決足夠的小問題,讓你能開始打造可執行的軟體展示,接著再持續迭代更多功能。盡可能頻繁地做出展示。
優先實作那些能讓你開始採用自己軟體的功能(如果適用的話,例如個人專案、解決你自身實際問題的工作專案等)。接著持續優先解決你自己的問題。
視未來改進的需求,回頭迭代各個元件,並依需要重複此流程。
結論
大致上就是這樣。我在個人專案、團隊專案、工作專案、學校專案等各種情境中都遵循這個大致的模式,這也是我保持自我動力的方式2。
注意,我有很多東西都沒提到!我沒有談到發布(shipping)。我知道很多人覺得發布很能激勵人心。我認為一個專案不一定要發布才算成功。而且對我而言,發布是太大的事件,無法長期激勵我。我也沒有談到工具(Git 工作流程、CI 等)。我在多份工作中都使用這套流程,並讓它融入既有的各種流程中。諸如此類。
我想這有助於說明這在多大程度上是一個個人化的流程。我認為每個人都需要找到某種能以健康方式強化自身動力的方法。我意識到看到成果對我有非常強烈的激勵作用,我便圍繞這一點建立了自己的工作風格,而到目前為止,這對我來說效果很好。
註腳
隨機一篇部落格