How To Review Code

Matthias Endler

如何審查程式碼

原文由 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,
    }
}

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

別害怕說「不」

我經常得婉拒別人的改動,而這從來都不容易。畢竟對方投入了大量心力,自然希望自己的成果能被接受。

別粉飾你的決定,也別為了當好人而拐彎抹角。要客觀,說明你的理由,並提供更好的替代方案。別糾結在否決本身,而是把焦點放在下一步該怎麼做。

與其接受不對的東西、為日後埋下隱患,不如直接說不。一旦開了先例,未來要再拒絕類似的改動只會更難。

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

在開源專案中,很多人貢獻的程式碼並不符合你的標準。必須有人站出來說「不」,而這是非常吃力不討好的工作(問問任何一位開源維護者就知道)。然而,優秀的專案需要把關者,否則迎來的就是品質低落的程式碼,最終導致專案難以維護。

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

當覺得難以啟齒時,要記得你否決的是程式碼,而不是這個人。提醒對方你感謝他的付出,也想幫助他做得更好。

即使你已經培養出審查時該關注什麼的直覺,還是要用事實來支撐你的判斷。如果你發現自己一再因為同樣的問題說「不」,可以考慮為團隊寫一份風格指南或準則。

保持風度,但要果斷;終究只是程式碼而已。

程式碼審查就是溝通

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

如果可能的話,我會刻意把前幾次的審查拉到一起,用結對程式設計的方式進行。

這樣,你們可以互相學習彼此的溝通風格。用這種方式建立信任、增進了解效果很好。如果之後發現溝通卡關或產生誤解,也應該重複這個過程。

採用多輪審查

「可以幫我快速看一下這個 PR 嗎?我想今天就合併。」大家常常期待程式碼審查是一次搞定的事。但事實並非如此。程式碼審查其實是個反覆迭代的過程。想把程式碼做到好,就該預期會有多輪審查。

在第一輪審查中,我會聚焦在大局和整體設計。完成之後,才會深入細節。

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

審查不只是挑錯,也是讓團隊對程式碼建立共同理解的過程。我常常透過審查別人的程式碼,學到最多關於如何寫出更好程式碼的知識。我自己的程式碼也曾從優秀的工程師那裡得到非常棒的回饋。

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

別當個混蛋

有時,你會和作者意見不同。這時保持尊重與建設性很重要。避免人身攻擊或高高在上的語氣。別說「這是錯的。」改說「我會這樣做。」如果對方猶豫不決,可以問幾個問題來理解他的思路。

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

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

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

我偶爾也會加上一些正面的留言,像是「我喜歡這樣寫」或「這是個很棒的想法。」保持作者的動力、讓對方感受到你欣賞他的努力,影響是很深遠的。

如果可以,試著跑跑看程式碼

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

如果可以的話,我會試著跑程式碼、跑測試、跑 linter。把分支抓下來、動手改改看、故意弄壞點東西、試著理解它如何運作,這些都是我審查流程的一部分。

像是 UI 變更或錯誤訊息這類面向使用者的改動,通常實際跑起來並試著弄壞它時,會更容易發現問題。

之後,我會還原這些更動,並在需要時把發現寫成留言。透過這種方式,往往能獲得更深入的理解。

對自己的時間安排要開誠布公

程式碼審查常常是開發流程中的瓶頸,因為它無法完全自動化:中間必須有人去看程式碼並提供回饋。

但如果要一直等同事來審查你的程式碼,可能會讓人感到沮喪。別成為那個造成瓶頸的人。

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

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

永不停止學習

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

我試著在每次審查中至少學到一件新東西。只要能幫助整個團隊進步與成長,就不算浪費時間。

別吹毛求疵

格式化工具存在是有原因的:把空白與排版交給工具就好。把精力留給真正重要的問題。

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

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

聚焦於「為什麼」,而非「怎麼做」

審查程式碼時,要聚焦在改動背後的理由。這比只指出缺陷卻不說明原因,成功機率要高得多。

來看看下面兩則程式碼審查留言。第一則毫無幫助又帶有否定意味。

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

第二則則提出了替代方案、附上了文件連結,並解釋了為什麼這個改動日後可能會造成問題。

一則程式碼審查留言,解釋了婉拒改動的理由,並提供了有幫助的替代方案與文件連結

你會比較想收到哪一種?

我知道這需要花更多時間與心力,但絕對值得!大多數時候,作者會心存感激,未來也會避免再犯同樣的錯誤。有幫助的審查,久而久之會產生複利效應。

別害怕問笨問題

提問勝過臆測。如果你有不懂的地方,就請作者解釋一下。很可能不只你一個人沒看懂。

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

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

為你的審查風格尋求回饋

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

  • 你是不是太嚴苛/太吹毛求疵/太慢/太馬虎了?
  • 你有指出正確的重點嗎?
  • 你的回饋對他們有幫助嗎?
  • 他們對你有什麼改進的建議嗎?

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

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

  1. 謝謝 Lucca 向我指出這個詞!

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

留言