Code Review Guide

代碼審查者應(yīng)該關(guān)注哪些方面?

代碼審查時應(yīng)該關(guān)注以下方面:

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

設(shè)計

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

單一職責原則 (Single Responsibility Principle)

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

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

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

開閉原則 (Open-Closed Principle)

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

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

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

違背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, "...");
    }
  }
}

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


public class Alert {
  // ...省略AlertRule/Notification屬性和構(gòu)造函數(shù)...
  
  // 改動一:添加參數(shù)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, "...");
    }
  }
}

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

里氏替換原則 (Liskov Substitution Principle)

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

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

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

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

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

依賴倒轉(zhuǎn)原則 (Dependence Inversion Principle)

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

1.在sop流程中提供通用功能,拓展點卻依賴了具體實現(xiàn)環(huán)節(jié)中的類,關(guān)心了具體實現(xiàn)。

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

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

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

接口隔離原則 (Interface Segregation Principle)

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

違背ISP原則的案例

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

2.由于接口設(shè)計不佳,授信領(lǐng)域間接被強迫依賴了支用領(lǐng)域接口。

3.一個接口方法功能太多,只能兼容一些入?yún)着芰撕芏喽嘤嗟倪壿嫛?/p>

迪米特法則(Law Of Demeter)

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

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

違背LOD原則的案例

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

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

功能

背景了解

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

邏輯分析

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

邊界檢查

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

異常監(jiān)控

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

非業(yè)務(wù)功能性檢查

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

復雜度

圈復雜度

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

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

方法長度

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

方法參數(shù)長度

方法參數(shù)過多會降低容錯性、模糊表達意圖,參數(shù)個數(shù)應(yīng)當在3個以內(nèi),可以通過靜態(tài)創(chuàng)建、參數(shù)合并為Model等方式降低長度。

最高結(jié)構(gòu)控制層數(shù)

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

設(shè)計復雜度

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

測試

case覆蓋率

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

可測性

Code Reviewer 檢查代碼中是否有環(huán)境、外部、靜態(tài)類等依賴,如有,會影響單測隔離性,單測應(yīng)該可以通過打樁隔絕任何依賴。

規(guī)范

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

命名、注釋、風格

代碼規(guī)范

文檔

文檔更新

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

如何撰寫 Code Review 評論

總結(jié)

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

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

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

引用

  • <設(shè)計模式之美> 王爭
  • <Code Review Developer Guide> Google
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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

友情鏈接更多精彩內(nèi)容