機械約束買到被量測的那個數字,代價落在沒被量測的維度
論述基礎與限制
證據來自 #277 引用的同一份實驗(Robert C. Martin 的 negative-test-experiment)的第二個變因:CRAP 度量是否強制壓到低於 4。CRAP(m) = C²(1 − cov)³ + C,在滿覆蓋率下等於圈複雜度,因此上限 4 等於要求每個函式的複雜度不超過 3。這個約束在該行的程式與測試都寫完之後才施加。
八列各自從空目錄獨立重寫,所以開啟與關閉是兩支獨立產物的並列,不是同一支程式的前後對照——summary 的原句是 Each CRAP-on program is a new write plus those splits, not a copy of its CRAP-off twin。在這個前提下,四個開啟該約束的版本相對於同一紀律關閉時的版本:行數增加 82 到 218、函式數大致翻倍(40 到 88、33 到 86、52 到 118、41 到 95)、行覆蓋率上升到 97% 以上,其中原本零測試的那一行被迫生出 40 個範例與 150 個斷言。作者給出的可讀性評分在四個開啟的版本全部是 1 或 2,關閉時是 3 或 4;設計評分一次都沒有上升。他記下的兩個實例:一個 WHERE TO? 輸入迴圈被拆成三個互相遞迴的單條件函式,一個 land 函式變成六個。他的描述是這個約束「不做簡化,只是繁殖名字」。
限制有四層。單一產品、單一度量、單一語言;設計與可讀性評分是作者本人的主觀量表;每一格只執行一次,而開啟與關閉的兩列是獨立重寫,那些行數與函式數的差值同時含重寫變異,把它們全部歸因於這個度量會高估它——作者對成長的歸因來自他讀過八棵樹之後的判讀,屬於事後解讀;此外實驗只施加了一種度量,不同度量的代價會落在不同維度。可重用的是機制——約束指定可量測目標、達成路徑不只一條——而不是這些數字本身。
核心原則
可機械檢查的品質約束改變程式碼的外形,不改變它的結構品質。 約束指定一個可量測的目標,而達成該目標的路徑通常不只一條:重新設計會讓數字下降,把違規的地方切碎也會讓數字下降,而兩者在指標上無法區分。切碎比較便宜,於是它是預設的達成路徑。
這解釋了實驗裡那組看起來矛盾的數字——覆蓋率上升、函式數翻倍、設計分不動、可讀性下降。函式變多本身既可能是好的分解(每個新名字對應一個領域概念),也可能是壞的分解(新名字只是「原函式的第三段」);度量看到的都是函式變多。
代價的落點可以事先推出來:它落在沒有被量測的維度上。複雜度上限量的是單一函式的分支數,沒有量的是概念的完整性,於是一個表達單一意圖的迴圈被拆成三個各自無意義的片段,總分支數不變而閱讀時要重建的脈絡變多。覆蓋率門檻量的是執行足跡,沒有量的是斷言強度,於是補上的測試把行跑過而不檢查結果。
這條原則不否定機械約束的價值,它界定那個價值的位置:約束守的是下限,不是上限。它擋得住三百行的單一函式與零覆蓋率的模組,擋不住把三百行切成一百個各三行的函式。
修法
- 每個機械約束配一個它量不到的維度的人工檢查點。(閘門配對的操作表在 品質閘門的更替。) 複雜度上限配一次命名檢查,覆蓋率門檻配一次斷言強度檢查(突變測試是後者的自動化形式,因為空斷言灌不高突變分數)。沒有配對的約束會被最便宜的路徑達成。
- 用命名判斷一次「符合約束」的改動是分解還是切碎。 問新產生的每個名字指不指涉一個領域概念。實驗裡那個
land函式被拆成六個就是反例:原本一個名字對應「玩家進入一個房間之後會發生什麼」這件事,拆完之後六個名字各自對應原函式的第幾段,沒有一個對得回遊戲規則裡的任何概念。機械做得到的是觸發——每個新產生的名字都被問一次;判定那個名字對不對得回領域概念仍然是人的工作,可以要求它在需求文件或領域詞彙表裡找得到對應,找不到的算切碎。 - 不要把約束的達成當成品質改善的證據。 「全部函式複雜度都在 3 以下」是一個關於外形的事實陳述,把它當成「設計變好了」是把量測到的維度當成全部維度。要宣稱設計改善,需要獨立於該度量的說法。
- 施加的時機影響代價。 實驗裡的約束在程式寫完後才施加,於是它只能切割既有結構。在設計進行中作為回饋使用時,同一個訊號有機會改變結構選擇;作為事後的閘門使用時,它只剩下切割這一條路。
- 既有指標的達成率不能當成放寬人工檢查的理由。 這是這條原則最實用的一面:指標全綠時最容易發生的判斷是「品質有人守著了」,而全綠正好是切碎路徑的產物與重新設計的產物長得最像的時候。
跟其他原則的關係
| 原則 | 關係 |
|---|---|
| #277 通過關卡不等於通過的是同一個程式 | 同源實驗的另一軸——#277 談關卡放過了什麼(等價類的大小),本卡談關卡逼出來的改動長什麼樣;兩者合起來是「通過」這個訊號的兩種空洞方式 |
| #222 約束要讓違反路徑走不通 | 補一個限制——#222 主張約束要有執行層,本卡指出執行層裝上之後違反路徑會被繞道,而繞道的產物在指標上合規;#222 的判準要加問「達成路徑有幾條」 |
| #267 入口由違規形態寫不寫得成 pattern 決定 | 同一個機制在寫作規則上——可機械偵測的形態只是違規集合的一個投影,投影之外的部分不會因為規則存在而被涵蓋 |
| #221 檢查規則的作用域要顯式列舉 | 相鄰——#221 處理「規則沒涵蓋到」,本卡處理「規則涵蓋到了但被最便宜的方式滿足」;兩者的零錯誤訊號同樣與真正合規無法區分 |
| #249 對當下段落沒有收益的標註不會自發發生 | 同一個成本結構的反向——#249 說沒有立即收益的動作不會發生,本卡說有立即收益(讓指標變綠)的最便宜動作一定會發生 |
同一個機制在別的地方
這條原則的載體不限於程式碼度量。任何「指定一個可量測目標、而達成路徑不只一條」的約束都會走同一條路(經濟學裡的 Goodhart’s law 講的是同一件事:一個量測一旦成為目標,它就不再是好的量測),判別它只需要問一句:最便宜的達成方式跟你想要的那個結果,是不是同一件事。
寫作規範的 keyword 清單是最近的一個親戚——它量的是特定句型出不出現,量不到的是那句話有沒有把重點放在前面,於是最便宜的達成方式是換一個沒被列進清單的同義句型。招募的面試場次下限量的是評估次數,量不到的是評估的獨立性,最便宜的達成是同一組人多開一場。文件覆蓋率量的是有沒有那一節,量不到的是那一節有沒有回答問題。
三個例子的共通點是代價都落在沒有被量測的那一側,而且合規的產物與真正達成目標的產物在指標上長得一樣。這使得「指標全綠」永遠不能單獨當成改善的證據——它在兩種情況下都會出現。
這條原則什麼時候不成立
達成路徑只有一條時不成立。編譯器的型別檢查、加密演算法的金鑰長度下限,這類約束沒有便宜的繞道版本,滿足它就是滿足它,不必配對人工檢查點。
被量測的維度本身就是目標時也不成立。API 的 p99 延遲門檻量的正是使用者感受到的那件事,壓低它沒有「假的達成方式」——除非門檻被改成只量部分流量,而那已經是把約束換掉了,不是繞過它。
判別的問題只有一個:這個約束有幾種達成方式,它們的產物在指標上分得出來嗎。 分不出來就要配對;只有一種達成方式就不必。