給 15 年前的自己一些程式設計建議
有效率地學程式、頻繁發版、勇於提問
作者: Marcus Buffett
我終於覺得自己算個稱職的程式設計師了,所以想寫些建議,回顧一下「怎樣才能讓我更快達到現在的水準?」這些建議不見得適用於所有人,但對我來說很有幫助。
如果老是出錯(你自己或團隊),就從根本解決問題
我遇過太多次,系統有些地方很容易出錯,卻沒人想辦法降低出錯的機率。
我開發 iOS App 時,用 CoreData ,很多畫面都訂閱了資料變更。訂閱的回呼函式會在觸發變更的執行緒上執行。
有時是主執行緒,有時是背景執行緒。 iOS 的 UI 更新只能在主執行緒上執行,不然 App 就會閃退。
所以新的訂閱功能可能一開始沒問題,但如果有人從背景執行緒觸發變更,或是後來加了 UI 更新,就會出錯。
大家都默默接受這個問題,審核菜鳥程式碼時也常看到。偶爾會漏掉一些,看到閃退報告時才加 DispatchQueue.main.async 。
我決定根治這個問題,只花了十分鐘更新訂閱機制,讓它在主執行緒上通知訂閱者,就此消滅了一整類的閃退問題,也減輕了不少心理負擔。
我不是想說「這些笨蛋都沒發現程式碼裡明顯的問題,只有我看到了」,因為稍微想想就能發現這個問題。
問題在於,一直沒找到合適的時機來處理。剛到新環境,你不會想改太多東西。
你可能覺得有些地方怪怪的,但你還在學習階段,不該亂改。等到你在團隊待了一陣子,這些問題就變成家常便飯了。
這需要轉換一下思維。你只要偶爾提醒自己,你有能力讓自己和團隊的生活更輕鬆。
評估品質和速度的取捨,找出適合當下環境的平衡點
開發速度和程式碼的正確性之間,永遠需要取捨。你應該問自己:在目前的環境下,出錯的後果有多嚴重?
我的第一份工作是開發全新的資料處理系統,有完善的機制可以重新處理資料。出錯的影響不大。
在這種環境下,可以適度仰賴安全機制,加快開發速度。不需要 100% 的測試覆蓋率,也不需要繁瑣的品管流程。
第二家公司,我開發的產品有數千萬使用者,處理高價值的財務資料和個資。即使是小錯誤,也要寫檢討報告。
我發布新功能的速度慢得像烏龜,但我想那一年應該沒出過什麼錯。一般來說,你不會在第二種公司工作。
我看過很多開發人員都偏好那種程式設計方式。如果錯誤的影響不大(例如 99% 的網頁應用程式),快速發布、快速修復,比追求第一次就完美更有效率。
花時間精進工具通常很值得
你會經常重新命名、查看型別定義、尋找參考等等,這些動作都應該要快。
你應該熟悉編輯器的所有主要快捷鍵。你應該要能快速且自信地打字。你應該熟悉你的作業系統。
你應該精通命令列操作。你應該知道如何有效地使用瀏覽器的開發者工具。
我知道有人會說「你不能整天在那邊調 neovim 設定,有時候你得做點正事」。但我還沒看過有人在這方面做得太過火。
對新手工程師來說,能否用心選擇和精熟工具,是很重要的指標。
如果你很難解釋為什麼某件事很難,那可能是意外的複雜度,值得好好處理
我最喜歡的主管有個習慣,當我說某件事很難做時,他會追問我。
他通常會說「這不就是把 X 傳給 Y 嗎?」或是「這跟我們幾個月前做的 Z 很像啊?」
這些都是很抽象的反駁,沒有深入到我們正在處理的函式和類別層級。
一般來說,主管這樣簡化問題會讓人很惱火。但神奇的是,當他追問時,我 often 會發現,我以為的複雜度,其實是意外造成的。
我可以先處理這些意外的複雜度,讓問題變得像他說的那麼簡單。這樣做通常也能讓後續的修改更容易。
試著深入一點解決錯誤
假設你有一個 React 元件顯示在儀表板上,它會處理從狀態中取得的目前登入使用者物件 User 。
你在 Sentry 看到錯誤報告,顯示渲染時 user 是 null 。你可以很快地加上 if (!user) return null 。
或者你可以深入調查,發現你的登出函式做了兩個狀態更新──第一個是把使用者設成 null ,第二個是重新導向到首頁。
你把這兩個操作的順序對調,現在所有元件都不會再出現這個錯誤,因為在儀表板上,使用者物件永遠不會是 null 。
一直用第一種方法修 bug ,你的程式碼最終會變成一團亂。一直用第二種方法,你會得到一個乾淨的系統,以及對程式碼不變量的深入理解。
別低估追溯錯誤歷史紀錄的價值
我一直很擅長用 println 和除錯器之類的工具來除錯。所以我從來沒真正用 git 來追溯錯誤的歷史紀錄。
但對某些錯誤來說,這很重要。我最近遇到伺服器一直漏記憶體,最後因為 OOM 被砍掉重啟。
我怎麼也找不到原因。所有可能的問題都排除了,我也沒辦法在本機重現,感覺就像在亂槍打鳥。
我查看了提交紀錄,發現這個問題是在我加入 Play Store 付款功能後開始出現的。這是我從來不會去看的地方,它就只是一些 HTTP 請求而已。
原來,第一個 access token 過期後,它就卡在無限迴圈,不斷嘗試取得新的 access token 。每個請求可能只增加幾 KB 的記憶體。
但在多個執行緒上,每 10 毫秒重試一次,記憶體很快就爆了。通常這種情況會造成堆疊溢位。
但我用 Rust 的非同步遞迴,不會堆疊溢位。我從來沒想到這點。
但當我被迫查看那段程式碼時,我知道一定是它造成的,這個想法就突然冒出來了。
我不確定什麼時候該這樣做,什麼時候不該。這是一種直覺,錯誤報告會觸發不同程度的「嗯?」來促使我去追溯歷史紀錄。
這種直覺會隨著時間慢慢培養,但知道這點很重要就夠了,如果你遇到難題的話。
如果情況允許,可以試試 git bisect ──這表示你的 git 紀錄要有小的提交、有自動化測試,而且你得知道哪個提交有問題,哪個提交沒問題。
爛程式碼會給你回饋,完美的程式碼不會。寫程式碼時,別太追求完美
寫爛程式碼很容易。寫出符合所有最佳實務的程式碼也很容易。
程式碼經過單元測試、整合測試、模糊測試、突變測試──搞不好你的新創公司還沒做完就倒閉了。所以程式設計的重點在於取得平衡。
如果你傾向快速寫程式碼,你偶爾會被技術債壓垮。你會學到「我應該好好測試資料處理的部分,因為以後可能很難改」。
或是「我應該好好設計資料表,因為不停機修改資料表很困難」。
如果你傾向寫完美的程式碼,你得不到任何回饋。做什麼事都很慢。
你不知道在哪裡花時間值得,在哪裡是浪費時間。學習需要回饋,而你沒有得到回饋。
我說的爛程式碼,不是指「我忘了怎麼建立雜湊表,所以我用兩個迴圈代替」,而是指:
與其重寫資料擷取程式碼來避免這種特殊狀態,不如在幾個關鍵點加上斷言來檢查不變量
伺服器模型跟我要寫的 DTO 一模一樣,所以我直接序列化,不寫樣板程式碼,之後需要 DTO 再說
我跳過這些元件的測試,因為它們很簡單,就算出錯也沒什麼大不了的
讓除錯更容易
多年來,我學到很多讓除錯更容易的小技巧。如果你不努力讓除錯更容易,隨著軟體越來越複雜,你會花費過多時間在除錯上。
你會不敢修改程式碼,因為即使只是幾個小 bug ,也可能要花一週的時間才能解掉。以下是一些例子:
Chessbook 後端:
- 我寫了一個指令,可以把使用者的所有資料複製到本機,這樣我只要知道使用者名稱,就能輕鬆重現問題。
- 我用 OpenTelemetry 追蹤每個請求,很容易看出請求的時間都花在哪裡。
- 我有一個測試檔,就像 REPL 一樣,每次修改程式碼都會重新執行。這讓我可以輕鬆測試程式碼片段,了解程式碼的行為。
- 在測試環境中,我把並行度設成 1 ,讓日誌更容易閱讀。
前端:
- 我有一個 debugRequests 設定,可以關閉樂觀載入資料,讓除錯請求更容易。
- 我有一個 debugState 設定,每次狀態更新後,都會印出整個程式狀態,以及清楚的變更差異。
- 我有一個檔案,裡面有很多小函式,可以讓 UI 進入特定狀態,這樣我除錯時,就不用一直點 UI 了。
注意一下,你的除錯時間有多少花在設定、重現和事後清理上。如果超過 50% ,你應該想辦法讓它更容易。
即使這次要花更多時間也值得。在其他條件相同的情況下,除錯應該要越來越容易。
團隊合作時,通常應該問問題
在「試著自己解決所有問題」和「什麼小事都問同事」之間,我認為大多數新手都太偏向前者。
總會有人對程式庫比較熟,或比較了解某個技術,或比較了解產品,或就是比較有經驗。
在一個地方工作的前六個月,很多時候,你可能花了一個多小時才解決的問題,其實問一下同事,幾分鐘就能得到答案。
問問題吧。只有當你自己幾分鐘就能找到答案時,才不該問。
發布節奏很重要。好好想想怎樣才能快速且頻繁地發布
新創公司的資金有限。專案有期限。如果你辭職創業,你的存款只夠撐幾個月。
理想情況下,你的開發速度應該要越來越快,直到你可以快速發布新功能。要做到這點,你需要很多東西:
- 一個不容易出錯的系統
- 團隊之間快速的溝通和協作
- 能夠捨棄新功能中不重要的 10% ,並且預見哪些部分不重要
- 一致且可重複使用的模式,可以組合成新的畫面/功能/端點
- 快速、簡單的部署流程
- 不要被流程拖累:不穩定的測試、慢吞吞的 CI 、吹毛求疵的程式碼檢查工具、慢吞吞的 PR 審核、過度依賴 JIRA 等等
- 還有其他數不清的因素
發布速度慢,應該要像系統當機一樣,需要寫檢討報告。雖然業界不是這樣運作的,但不代表你不能自己努力做到這點。