CRAP 分析(Change Risk Anti-Patterns)

CRAP 是由 Alberto Savoia 和 Bob Evans 在 2007 年提出的程式碼品質指標,全名是 Change Risk Anti-Patterns(也有人稱 Change Risk Analysis and Predictions)。它的核心想法很直白:一段程式碼「危不危險」,取決於它有多複雜,以及它有多少測試保護。複雜又沒測試的程式碼,改起來風險最高——這就是所謂的 “crappy code”。

公式

CRAP(m) = comp(m)² × (1 − cov(m)/100)³ + comp(m)

其中:

  • comp(m):方法 m 的循環複雜度(cyclomatic complexity)
  • cov(m):該方法的測試涵蓋率(通常用 branch coverage,以百分比計)

一般慣例的門檻值是 30:CRAP 分數超過 30 的方法,就該被視為高風險,需要重構或補測試。

公式設計得很巧妙:

  1. 涵蓋率的懲罰是三次方——涵蓋率從 0% 提升到 50%,懲罰項就從 1 降到 0.125,效果非常顯著。這鼓勵你「先補一些測試」,而不是追求 100%。
  2. 複雜度是平方——複雜度翻倍,風險大約變四倍。
  3. 即使涵蓋率 100%,CRAP 仍等於複雜度本身——一個複雜度 35 的方法,就算測好測滿,分數還是 35,依然超標。這在提醒你:測試救不了過度複雜的設計,該重構就要重構。

實際範例

假設有一個計算運費的方法:

java

public int shippingFee(Order order) {
    if (order == null) throw new IllegalArgumentException();
    int fee = 0;
    if (order.total() < 1000) {          // 分支 1
        fee = 100;
        if (order.isRemoteArea()) {      // 分支 2
            fee += 150;
        }
    }
    if (order.isFragile()) {             // 分支 3
        fee += 50;
    }
    if (order.isVip() && fee > 0) {      // 分支 4 (含 &&)
        fee = fee / 2;
    }
    return fee;
}

這個方法的循環複雜度大約是 6(1 + 5 個判斷點)。

情境一:完全沒有測試(coverage = 0%)

CRAP = 6² × (1 − 0)³ + 6 = 36 + 6 = 42 → 超過 30,高風險

情境二:寫了兩個測試,涵蓋率達到 50%

CRAP = 36 × (0.5)³ + 6 = 36 × 0.125 + 6 = 10.5 → 安全

只補了一半的測試,分數就從 42 掉到 10.5——這正是三次方懲罰的威力,也說明 CRAP 分析對「遺留系統要從哪裡開始補測試」特別有指導性:優先處理複雜度高、涵蓋率低的方法,投資報酬率最高

情境三:方法膨脹到複雜度 20,但只有 40% 涵蓋率

CRAP = 400 × (0.6)³ + 20 = 400 × 0.216 + 20 = 106.4 → 嚴重超標

此時就算把涵蓋率拉到 80%,分數是 400 × 0.008 + 20 = 23.2,勉強及格,但只要涵蓋率稍微退化就會再度爆表。正確做法是先拆解方法降低複雜度。

下圖可以看出不同複雜度下,涵蓋率對 CRAP 分數的影響:

CRAP 分數 vs. 測試涵蓋率

CRAP 分數 by 測試涵蓋率

複雜度 5複雜度 10複雜度 20Use the arrow keys to move between data points

圖中可以清楚看到兩件事:複雜度 5 的方法即使零測試也只是剛好貼在門檻 30 上;而複雜度 20 的方法,涵蓋率要拉到 80% 附近才降得下來,而且永遠降不到 20 以下。

工具支援

  • Java:最早的實作是 Crap4j;現在多數人直接看 JaCoCo 的複雜度 + 涵蓋率報告自行判讀,或用 SonarQube 的類似指標
  • PHP:PHPUnit 的 code coverage 報告內建 CRAP index,每個方法旁邊直接顯示
  • 其他語言:即使工具沒直接算 CRAP,只要有 cyclomatic complexity 和 branch coverage 兩個數字,自己套公式就行

實務上怎麼用

CRAP 分析最大的價值不在於「追求所有方法都低於 30」,而是提供一個優先排序:面對一個遺留系統,你不可能一次補齊所有測試,但把方法按 CRAP 分數排序後,排在最前面的就是「最複雜 × 最沒保護 × 最可能改壞」的地方——先在那裡建立特徵測試(characterization tests),再進行重構,是最符合成本效益的切入點。這個思路和 Michael Feathers 在《Working Effectively with Legacy Code》裡的策略是相通的。

一個常見的提醒:CRAP 和所有指標一樣會被「玩弄」——為了降分數而寫一堆沒有斷言的假測試,涵蓋率上去了、分數下來了,但風險一點都沒少。指標是用來找熱點的,不是用來當 KPI 的。

發表迴響

探索更多來自 轉念學 - 敏捷三叔公的學習之旅 的內容

立即訂閱即可持續閱讀,還能取得所有封存文章。

繼續閱讀