Code Review Guide

代碼審查者應該關注哪些方面?

代碼審查時應該關注以下方面:

  • 設計:設計是否合理?
  • 功能:是否滿足prd需求?是否滿足用戶流程交互合理性?
  • 復雜度:代碼能更簡單嗎?將來其他開發人員能輕松理解并使用此代碼嗎?
  • 測試:代碼是否具有正確且設計良好的單元測試?
  • 命名:開發人員是否為變量、類、方法、包等選擇了明確的名稱?
  • 注釋:注釋是否清晰有用?
  • 風格:代碼是否遵守了代碼規范
  • 文檔:開發人員是否同時更新了相關文檔?

設計

審查中最重要的是 CL 的整體設計。CL 中各種代碼的交互是否有意義?現在是添加此功能的好時機嗎?

單一職責原則 (Single Responsibility Principle)

  • 類中的代碼行數、函數或屬性過多,會影響代碼的可讀性和可維護性,我們就需要考慮對類進行拆分;
  • 類依賴的其他類過多,或者依賴類的其他類過多,不符合高內聚、低耦合的設計思想,我們就需要考慮對類進行拆分;
  • 私有方法過多,我們就要考慮能否將私有方法獨立到新的類中,設置為 public 方法,供更多的類使用,從而提高代碼的復用性;
  • 比較難給類起一個合適名字,很難用一個業務名詞概括,或者只能用一些籠統的 Manager、Context 之類的詞語來命名,這就說明類的職責定義得可能不夠清晰;
  • 類中大量的方法都是集中操作類中的某幾個屬性,那就可以考慮將這幾個屬性和對應的方法拆分出來。
違背SRP原則的案例

1.CreditSchemaAbilityImpl中函數、依賴過多,充斥著類轉換邏輯、訪問facade邏輯、工廠邏輯、私有方法過多、大量私有方法最終只是得到一個字段值等。

2.User類中大量方法操作的都是UserAddress成員變量對象,而這些方法散落在User類而不是委托UserAddress類。

開閉原則 (Open-Closed Principle)

  • 添加一個新的功能,應該是通過在已有代碼基礎上擴展代碼(新增模塊、類、方法、屬性等),而非修改已有代碼(修改模塊、類、方法、屬性等)的方式來完成。關于定義,我們有兩點要注意。第一點是,開閉原則并不是說完全杜絕修改,而是以最小的修改代碼的代價來完成新功能的開發。第二點是,同樣的代碼改動,在粗代碼粒度下,可能被認定為“修改”;在細代碼粒度下,可能又被認定為“擴展”。

  • 我們要時刻具備擴展意識、抽象意識、封裝意識。在寫代碼的時候,我們要多花點時間思考一下,這段代碼未來可能有哪些需求變更,如何設計代碼結構,事先留好擴展點,以便在未來需求變更的時候,在不改動代碼整體結構、做到最小代碼改動的情況下,將新的代碼靈活地插入到擴展點上。

最常用來提高代碼擴展性的方法有:多態、依賴注入、基于接口而非實現編程,以及大部分的設計模式(比如,裝飾、策略、模板、職責鏈、狀態)。

違背OCP原則的案例

public class Alert {
  private AlertRule rule;
  private Notification notification;

  public Alert(AlertRule rule, Notification notification) {
    this.rule = rule;
    this.notification = notification;
  }

  public void check(String api, long requestCount, long errorCount, long durationOfSeconds) {
    long tps = requestCount / durationOfSeconds;
    if (tps > rule.getMatchedRule(api).getMaxTps()) {
      notification.notify(NotificationEmergencyLevel.URGENCY, "...");
    }
    if (errorCount > rule.getMatchedRule(api).getMaxErrorCount()) {
      notification.notify(NotificationEmergencyLevel.SEVERE, "...");
    }
  }
}

上面這段代碼非常簡單,業務邏輯主要集中在 check() 函數中。當接口的 TPS 超過某個預先設置的最大值時,以及當接口請求出錯數大于某個最大允許值時,就會觸發告警,通知接口的相關負責人或者團隊。現在,如果我們需要添加一個功能,當每秒鐘接口超時請求個數,超過某個預先設置的最大閾值時,我們也要觸發告警發送通知。這個時候,我們該如何改動代碼呢?主要的改動有兩處:第一處是修改 check() 函數的入參,添加一個新的統計數據 timeoutCount,表示超時接口請求數;第二處是在 check() 函數中添加新的告警邏輯。具體的代碼改動如下所示:


public class Alert {
  // ...省略AlertRule/Notification屬性和構造函數...
  
  // 改動一:添加參數timeoutCount
  public void check(String api, long requestCount, long errorCount, long timeoutCount, long durationOfSeconds) {
    long tps = requestCount / durationOfSeconds;
    if (tps > rule.getMatchedRule(api).getMaxTps()) {
      notification.notify(NotificationEmergencyLevel.URGENCY, "...");
    }
    if (errorCount > rule.getMatchedRule(api).getMaxErrorCount()) {
      notification.notify(NotificationEmergencyLevel.SEVERE, "...");
    }
    // 改動二:添加接口超時處理邏輯
    long timeoutTps = timeoutCount / durationOfSeconds;
    if (timeoutTps > rule.getMatchedRule(api).getMaxTimeoutTps()) {
      notification.notify(NotificationEmergencyLevel.URGENCY, "...");
    }
  }
}

這樣的代碼修改實際上存在挺多問題的。一方面,我們對接口進行了修改,這就意味著調用這個接口的代碼都要做相應的修改。另一方面,修改了 check() 函數,相應的單元測試都需要修改(關于單元測試的內容我們在重構那部分會詳細介紹)。

里氏替換原則 (Liskov Substitution Principle)

  • 子類對象(object of subtype/derived class)能夠替換程序(program)中父類對象(object of base/parent class)出現的任何地方,并且保證原來程序的邏輯行為(behavior)不變及正確性不被破壞。

  • 判斷子類的設計實現是否違背里式替換原則,一個小竅門,那就是拿父類的單元測試去驗證子類的代碼。如果某些單元測試運行失敗,就有可能說明,子類的設計實現沒有完全地遵守父類的約定,子類有可能違背了里式替換原則。

違背LSP原則的案例
  1. 子類違背父類聲明要實現的功能父類中提供的 sortOrdersByAmount() 訂單排序函數,是按照金額從小到大來給訂單排序的,而子類重寫這個 sortOrdersByAmount() 訂單排序函數之后,是按照創建日期來給訂單排序的。那子類的設計就違背里式替換原則。

  2. 子類違背父類對輸入、輸出、異常的約定在父類中,某個函數約定:運行出錯的時候返回 null;獲取數據為空的時候返回空集合(empty collection)。而子類重載函數之后,實現變了,運行出錯返回異常(exception),獲取不到數據返回 null。那子類的設計就違背里式替換原則。在父類中,某個函數約定,輸入數據可以是任意整數,但子類實現的時候,只允許輸入數據是正整數,負數就拋出,也就是說,子類對輸入的數據的校驗比父類更加嚴格,那子類的設計就違背了里式替換原則。在父類中,某個函數約定,只會拋出 ArgumentNullException 異常,那子類的設計實現中只允許拋出 ArgumentNullException 異常,任何其他異常的拋出,都會導致子類違背里式替換原則。

  3. 子類違背父類注釋中所羅列的任何特殊說明父類中定義的 withdraw() 提現函數的注釋是這么寫的:“用戶的提現金額不得超過賬戶余額……”,而子類重寫 withdraw() 函數之后,針對 VIP 賬號實現了透支提現的功能,也就是提現金額可以大于賬戶余額,那這個子類的設計也是不符合里式替換原則的。

依賴倒轉原則 (Dependence Inversion Principle)

  • 高層模塊(high-level modules)不要依賴低層模塊(low-level)。高層模塊和低層模塊應該通過抽象(abstractions)來互相依賴。除此之外,抽象(abstractions)不要依賴具體實現細節(details),具體實現細節(details)依賴抽象(abstractions)。
違背DIP原則的案例

1.在sop流程中提供通用功能,拓展點卻依賴了具體實現環節中的類,關心了具體實現。

2.在某個類中依賴了某個接口具體實現類,而不是依賴接口。

3.沒有按照DI方式分層,單測困難。

4.沒有通過spring ioc容器控制反轉,直接在類中new成員變量,單測困難 , 依賴細節。

接口隔離原則 (Interface Segregation Principle)

客戶端不應該被強迫依賴它不需要的接口。其中的“客戶端”,可以理解為接口的調用者或者使用者。

違背ISP原則的案例

1.為了程序分層方便api將對數據庫的邏輯DELETE方法也暴露對外了,造成風險。

2.由于接口設計不佳,授信領域間接被強迫依賴了支用領域接口。

3.一個接口方法功能太多,只能兼容一些入參白跑了很多多余的邏輯。

迪米特法則(Law Of Demeter)

每個模塊(unit)只應該了解那些與它關系密切的模塊(units: only units “closely” related to the current unit)的有限知識(knowledge)。或者說,每個模塊只和自己的朋友“說話”(talk),不和陌生人“說話”(talk)。

不該有直接依賴關系的類之間,不要有依賴;有依賴關系的類之間,盡量只依賴必要的接口(也就是定義中的“有限知識”)。

違背LOD原則的案例

1.文件上傳接口FileUploadInterface依賴了OSS的Bucket對象,而不是定義抽象的文件上傳參數,文件上傳不一定要用OSS實現的。

2.還款實體為了得到某個字段,依賴了征信實體,只因為征信實體里面有這個字段,獲取方便,但是征信不屬于還款領域概念。

功能

背景了解

Code Reviewer 要知道改動的背景,本次改動代碼是為了做什么?不能做什么?會影響什么?

邏輯分析

Code Reviewer 根據需求背景,對代碼邏輯進行分析,是否滿足需求,是否有業務邏輯漏洞。

邊界檢查

Code Reviewer 檢查改動影響的功能模塊,是否有邊界問題,如果是refactor change,應當檢查兼容性,如果是feature change,需要檢查領域邊界是否合理。

異常監控

Code Reviewer 檢查日志處理、異常處理、監控埋點方式等代碼,是否存在缺陷、丟失。

非業務功能性檢查

Code Reviewer 檢查多線程、并發、代碼安全、性能等角度,是否存在漏洞,是否可以優化。

復雜度

圈復雜度

Code Reviewer 檢查代碼圈復雜度情況(if while分支過多、方法分支太多),圈復雜度在1-10為合理范圍。

改進方法
  • 簡化、合并條件表達式
  • 將條件判定提煉出獨立函數
  • 將大函數拆成小函數
  • 以明確函數取代參數
  • 替換算法、策略模式

方法長度

代碼長短可以直接說明它復不復雜、閱讀成本高不高,長度度量往往是復雜度分析里最簡單直接的手段。此處,有效代碼長度 = 代碼行 - 空白行 - 注釋行。當前,集團95%的方法的有效代碼行數在42以內。

方法參數長度

方法參數過多會降低容錯性、模糊表達意圖,參數個數應當在3個以內,可以通過靜態創建、參數合并為Model等方式降低長度。

最高結構控制層數

花括號 { } 的最大嵌套層數。深度嵌套的代碼總是像俄羅斯套娃一樣晦澀難懂,可讀性和可維護性大打折扣。當前,集團95%的方法的最大嵌套層數在3以內。

設計復雜度

是否有over設計的情況,簡單的兩個分支,拓展性低的case,做成了策略模式之類的。

測試

case覆蓋率

Code Reviewer 檢查單測case覆蓋情況,是否覆蓋了主要業務流程,嚴格來說每個邏輯分支都需要覆蓋。

可測性

Code Reviewer 檢查代碼中是否有環境、外部、靜態類等依賴,如有,會影響單測隔離性,單測應該可以通過打樁隔絕任何依賴。

規范

單測代碼也需要規范,清晰易讀,重復性低為準。

命名、注釋、風格

代碼規范

文檔

文檔更新

Code Reviewer 提醒 developer 在二分庫、應用等升級之后更新文檔。

如何撰寫 Code Review 評論

總結

  • 保持友善。
  • 解釋你的推理。
  • 在給出明確的指示與只指出問題并讓開發人員自己決定間做好平衡。
  • 鼓勵開發人員簡化代碼或添加代碼注釋,而不僅僅是向你解釋復雜性。

糟糕的示例:“為什么這里使用了線程,顯然并發并沒有帶來什么好處?”

好的示例:“這里的并發模型增加了系統的復雜性,但沒有任何實際的性能優勢,因為沒有性能優勢,最好是將這些代碼作為單線程處理而不是使用多線程。”

引用

  • <設計模式之美> 王爭
  • <Code Review Developer Guide> Google
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 轉載自Jim's blog 關于Code Review的重要性,我相信好的工程師都能認識到。 參考 讓Code R...
    Vinc閱讀 1,549評論 0 14
  • 通常在企業開發中,會定期面對面(face to face)對代碼進行評審 1.Code Review的意識 作為一...
    IIronMan閱讀 1,048評論 1 6
  • 架構/設計 單一職責原則這是經常被違背的原則。一個類只能干一個事情,一個方法最好也只干一件事情。比較常見的違背是一...
    苦笑男神閱讀 285評論 0 1
  • CodeReview iOS App 穩定性指標及監測(轉載) 代碼規范及CodeReview要點 iOS-程序員...
    不吃雞爪閱讀 1,388評論 0 1
  • 通常多人協作開發App,為了提高主庫代碼的質量,保證良好軟件架構,代碼審核這個環節就顯得尤為重要。 代碼審核方式 ...
    雨田_Toping閱讀 956評論 0 1