How To Review Code

Matthias Endler

如何審查程式碼

我審查他人程式碼已經有一段時間了,精確來說超過二十年。如今,我大約有 50-70% 的時間都在以各種形式審查程式碼。這是我受薪的工作內容之一,另一個則是系統設計。

時間一久,我也學到了一些關於如何有效審查程式碼的心得。我現在關注的重點,和剛入行時已經大不相同。

思考大局

糟糕的審查視野狹隘,只聚焦於語法、風格和細微問題,而忽略了可維護性與可擴充性。

好的審查不僅看變更本身,還會看這些變更解決了什麼問題、未來可能引發什麼問題,以及這項變更如何融入系統的整體設計。

我喜歡去看那些沒有被更動到的程式碼。它們往往才說出了真相。

舉例來說,人們常常忘了更新程式碼庫中相關的區段或是文件。這可能導致錯誤、混淆、破壞性變更或安全性問題。

要徹底一點,檢視新程式碼的所有呼叫位置。它們都有正確更新嗎?測試是否仍在測試正確的東西?變更是否放在對的位置?

這裡有一份我在審查程式碼時會自問的問題清單:

  • 這段程式碼如何融入系統的其他部分?
  • 它與程式碼庫其他部分的互動關係為何?
  • 它對整體架構有什麼影響?
  • 它是否會影響未來已規劃的工作?

這些問題與系統設計的關聯,比與變更本身的關聯更大。別忽略了大局,因為若接受了糟糕的變更,系統會變得脆弱。

程式碼不是在孤立環境中撰寫的。較資深開發者的角色是降低作業阻力並為專案進行風險管理。文件、測試與資料型別和程式碼本身同等重要。

隨著程式碼演進,要隨時留意更好的抽象化方式。

命名就是一切

我在審查程式碼時,有很大一部分時間都在思考好的命名。

為事物命名很難,正因如此,把它做好才如此重要。這往往是程式碼審查中最重要的一環。

這也是最主觀的部分,讓人覺得繁瑣,因為很難區分吹毛求疵和重要的命名決策。

名稱封裝了概念,並在程式碼中扮演「建構單元」的角色。糟糕的命名是一種程式碼異味,暗示著更深層的問題。它們會讓認知負荷增加一個甚至多個數量級。

舉例來說,假設我們有一個代表遊戲中玩家數據的 struct:

struct Player {
    username: String,
    score: i32,
    level: i32,
}

我經常看到像這樣的程式碼:

// Bad: using temporary/arbitrary names creates confusion
fn update_player_stats(player: Player, bonus_points: i32, level_up: bool) -> Player {
    let usr = player.username.trim().to_lowercase();
    let updated_score = player.score + bonus_points;
    let l = if level_up { player.level + 1 } else { player.level };
    let l2 = if l > 100 { 100 } else { l };
    
    Player {
        username: usr,
        score: updated_score, 
        level: l2,
    }
}

這段程式碼難以閱讀與理解。usrupdated_scorel2 到底是什麼?其目的並未清楚傳達。這會累積認知負荷,讓人更難跟上邏輯。

這就是為什麼我總會為變數思考最適當的名稱,即使感覺有點吹毛求疵。

// Good: meaningful names that describe the transformation at each step
fn update_player_stats(player: Player, bonus_points: i32, level_up: bool) -> Player {
    // Each variable name describes what the value represents
    let username = player.username.trim().to_lowercase();
    let score = player.score + bonus_points;

    // Use shadowed variables to clarify intent
    let level = if level_up { player.level + 1 } else { player.level };
    let level = if level > 100 { 100 } else { level };
    
    // If done correctly, the final variable names
    // often match the struct's field names
    Player {
        username,
        score,
        level,
    }
}

在較大的程式碼庫中,好的命名更為關鍵,因為值可能在離使用處很遠的地方宣告,而且許多開發者必須對問題領域有共同的理解。

別害怕說「不」

我經常必須拒絕變更,而且從來都不輕鬆。畢竟對方投入了很多心力,會希望自己的成果被接受。

不要粉飾你的決定或一味想當好人。要客觀,說明你的理由並提供更好的替代方案。不要糾結於此,而是聚焦於下一步。

與其接受不正確、日後會引發問題的東西,不如說不。一旦開了先例,未來要拒絕變更只會變得更困難。

這就是審查流程的目的:並不保證程式碼一定會被接受。

在開源專案中,許多人貢獻的程式碼並不符合你的標準。必須有人說「不」,而這是非常吃力不討好的工作(問問任何開源維護者就知道)。然而,優秀的專案需要守門人,因為替代方案就是品質低落的程式碼,最終導致專案無法維護。

有時候,人們會說「就先合併,之後再修吧。」我認為這是在走滑坡。這可能導致技術債和後續額外的工作。堅持立場很難,但很重要。如果你看到不對的地方,就要提出來。

當感到為難時,請記住你拒絕的是程式碼,而不是這個人。提醒對方你感謝他們的付出,並且想幫助他們改進。

即使你已經培養出審查時該關注什麼的直覺,仍應以事實作為依據。如果你發現自己一再對同樣的事情說「不」,可以考慮為團隊撰寫風格指南或一套規範。

要寬容但果斷;終究只是程式碼而已。

程式碼審查就是溝通

程式碼審查不只是關於程式碼,人也很重要。與同事建立良好關係很重要。

如果可能,我會刻意將最初的幾次審查以 pair programming(結對程式設計)的方式一起進行。

這樣一來,你們可以互相學習彼此的溝通風格。用這種方式建立信任、增進了解效果很好。如果之後發現溝通中斷或產生誤解,也應該再次採取這種做法。

善用多輪審查

「可以幫我快速看一下這個 PR 嗎?我想今天就合併。」人們常常期待程式碼審查是一次就能完成的事。但事實並非如此。相反地,程式碼審查是一個反覆迭代的過程。應該預期需要多輪迭代才能把程式碼做到正確。

在第一輪審查中,我專注於大局與整體設計。完成之後,我才會深入細節。

目標不該是盡快合併,而是接受高品質的程式碼。否則,一開始做程式碼審查的意義何在?這是一個重要的心態轉變。

審查並非在於指出缺陷,也是為了在團隊內建立對程式碼的共同理解。我往往透過審查他人的程式碼,學到最多關於如何寫出更好程式碼的知識。我也曾從優秀的工程師那裡,獲得對自己程式碼的絕佳回饋。

這些都是無價的「頓悟時刻」,能幫助你作為開發者成長。專家們花了寶貴的時間審查我的程式碼,而我從中學到了很多。我認為每個人在職涯中都應該體驗一次。

別當個混蛋

你不時會與作者意見相左。保持尊重與建設性很重要。避免人身攻擊或居高臨下的語氣。不要說「這是錯的。」而要說「我會這樣做。」如果對方猶豫不決,可以問幾個問題來了解他們的思考邏輯。

  • 「如果我們這樣做,會破壞現有的工作流程嗎?」
  • 「你考慮過哪些替代方案?」
  • 「如果用空陣列呼叫這個函式會發生什麼事?」
  • 「如果我沒有設定這個值,呈現給使用者的錯誤訊息會是什麼?」

這些「蘇格拉底式提問」1能幫助作者思考自己的決策,並導向更好的設計。

人們應該樂於收到你的回饋。如果不是這樣,就該重新檢視你的審查風格。只留下那些你自己也會樂於收到的評論。

我不時會加上正面的評語,像是「我喜歡這樣」或「這是個很棒的想法。」讓作者保持動力、讓他們感受到你欣賞他們的付出,會有很大的幫助。

如果可能,試著實際執行程式碼

當你盯著程式碼看太久,很容易忽略細微的細節。手邊有一份可以把玩的本地程式碼副本對我幫助很大。

如果可以,我會試著執行程式碼、測試和 linter。切換到該分支、四處移動、試著弄壞東西、試圖理解其運作方式,都是我審查流程的一部分。

像是 UI 變更或錯誤訊息這類面向使用者的變更,往往在你實際執行程式碼並試圖找出問題時更容易發現。

之後,我會還原變更,並在需要時將發現寫成評論。透過這種方式可以獲得更深入的理解。

坦誠說明你的時間安排

程式碼審查往往是開發流程中的瓶頸,因為它無法完全自動化:需要有人實際檢視程式碼並提供回饋。

但如果你一直在等同事來審查你的程式碼,可能會讓人感到沮喪。避免成為那樣的人。

有時候你沒時間審查程式碼,這也沒關係。如果無法在合理的時間內完成審查,請讓作者知道。

我自己也還在努力,但我試著更主動地說明自己的時間安排,並設定明確的期待。

永不停止學習

程式碼審查是我最喜歡學習新事物的方式。我學到新的技巧、模式、新的函式庫,但最重要的是,學到其他人如何處理問題。

我試著在每次審查中學習一件新事物。如果這能幫助團隊整體改進與成長,就不算是浪費時間。

別吹毛求疵

格式化工具存在的自有其道理:把空白與排版交給工具處理。把精力留給真正重要的問題。

專注於邏輯、設計、可維護性與正確性。避免那些不影響程式碼品質的主觀偏好。

問問自己:這會影響功能嗎?或是會讓未來的開發者感到困惑?如果不會,就放手吧。

關注「為什麼」,而非「怎麼做」

審查程式碼時,請關注變更背後的理由。這比毫無理由地指出缺陷,更有可能獲得成功。

看看以下兩個程式碼審查留言。第一個沒有幫助且帶有否定意味。

一則只寫著:「別這樣做。」的程式碼審查留言

第二個則提出了替代方案、附上文件連結,並解釋為什麼這項變更日後可能導致問題。

一則解釋拒絕變更理由的程式碼審查留言,提供了有幫助的替代方案並附上文件連結

你會比較想收到哪一個?

我知道這需要更多時間與精力,但這是值得的!大多數時候,作者會心存感激,並在未來避免再犯同樣的錯誤。有幫助的審查隨著時間會產生複利效應。

別害怕問蠢問題

提問比臆測更好。如果你有不懂的地方,請作者解釋。很可能不只你一個人不懂。

通常作者會很樂意解釋他們的思路。這麼做可以讓你對程式碼和整個系統有更深入的理解。這也能幫助作者從不同角度看事情。也許他們會發現自己的假設是錯的,或是系統本身不夠淺顯易懂。也許是缺少了文件?

提出好問題是一種超能力。

針對你的審查風格徵求回饋

不時向作者徵求對你回饋的回饋:

  • 你是否太嚴厲/太吹毛求疵/太慢/太草率?
  • 你有指出正確的重點嗎?
  • 你的回饋對他們有幫助嗎?
  • 他們有沒有改進的建議?

基本上,就是請他們來審查你的審查流程,呵。

學習如何審查程式碼是一項需要不斷練習與精進的技能。祝你早日找到屬於自己的風格。

  1. 感謝 Lucca(盧卡)向我指出這個詞!

原文由 Matthias Endler 發布

本文章由 muse-spark-1.2-contributor 進行翻譯