IBM 的 Cleanroom software engineering process 實現了每千行程式碼 0.1 個 defect。當時的業界平均水準在 10 到 50 之間。該 process 被文件化,在多個專案和語言之間 replicate,並經過獨立驗證。然後它消失了。
不是因為更好的 method 取代了它。Zero-defect engineering 消失的原因是,使其具有價值的經濟條件發生了變化,而使其成為可能的條件從未超出少數 government contractors 的範圍。
問題:defect 從一開始就被設計進去了
大多數 software 都建立在一個預設假設之上:bug 是不可避免的。你寫 code,執行它,發現問題,修復它。這個 loop 讓人感覺很有生產力。這也是對你 process 將 defect 作為正常 output 生產的預設接受。
Cleanroom 完全拒絕了這一假設。由 IBM 的 Harlan Mills 在 1970 年代開發,該方法將 software 視為半導體製造。你不會在 chip 製造完成後再把品質測試進去。你要從源頭防止產生 defect 的條件。
該方法有三個相互關聯的 practice。在 statistical process control 下的 incremental development。使用 black-box、state-box 和 clear-box 結構的 function-theoretic design。以及最違反直覺的 rule:寫 code 的人被禁止執行它。
這不是 sadism。這是 forcing function。如果你不能執行 code 來驗證它是否工作,你就必須在敲鍵盤之前推理 correctness。
這些數字不是偶然
IBM Federal Systems Division 將 Cleanroom 應用於 NASA satellite control system,並在最終測試中測得每 KLOC 0.1 個 defect。一個 COBOL billing system 達到了 0.3。一個 Ada real-time system 達到了 0.4。這些結果在不同團隊、應用領域和程式語言中保持一致。
該 process 還壓縮了 schedule。當時的業界數據顯示,50% 到 70% 的 software effort 用於 testing 和 debugging。Cleanroom 團隊將時間花在 design 上。Code 在首次編譯時就正常運作。
為什麼一個有效的 process 被放棄了
如果該方法如此有效,為什麼它會消失?
簡短回答是,1990 年到 2010 年間 software economics 發生了逆轉。詳細回答是,zero-defect engineering 針對的是一個已經不存在的世界。
在 1970 年代和 1980 年代,software 透過實體媒介分發。production 中的一個 bug 需要 recall、patch disk 或現場技術人員。defect 的成本是巨大的。Zero-defect engineering 很昂貴,但避免一次 recall 就能支付整個 process 的費用。
Web 改變了 cost function。今天我們透過 over the wire 進行 deploy。bug 進入 production,我們在幾分鐘內 rollback,在幾小時內 patch。單個 defect 的成本下降了數個數量級。Zero-defect engineering 的成本卻絲毫沒有下降。它仍然需要 formal verification、statistical process control,以及開發人員不執行自己的 code。這些 practice 消耗著現代 product development 不願花費的時間。
相互依賴陷阱
Cleanroom 不是選單。你不能只挑自己喜歡的部分採納。
Statistical process control 只有在測量每個 increment 並在 defect target 未達成時停止的情況下才有效。Box structures 只有在編寫下一層之前驗證每一層的情況下才有效。No-execution rule 只有在絕對執行的情況下才有效。一個「只是快速檢查一下這能不能編譯」的開發人員會破壞 forcing function。
部分採納不會帶來任何 benefit,只會帶來全部 overhead。團隊無法在一個 sprint 內嘗試 Cleanroom。他們必須重構整個 cycle,重新培訓每個開發人員,並收集數月的 process data。面對這個 proposal 的大多數 manager 會選擇再雇一名 QA engineer。
速度溢價殺死了品質溢價
現代 software 在 time-to-market 上競爭。consumer software 中的 first mover advantage 比 launch 後修復 bug 的成本更有價值。投資者獎勵 growth curves,而非 defect density metrics。
Zero-defect engineering 針對的是不同的 scoreboard。它假設 correctness 是主要 constraint,schedule pressure 是次要的。對於 NASA satellite control systems 來說這是事實。對於試圖在競爭對手之前 ship 的 startup 來說則不是。
文化錯配更深。開發人員喜歡執行 code。write-run-fix 的 immediate feedback loop 令人滿足。Cleanroom 要求你將這種滿足感推遲到想清楚每個 edge case 之後。
業界選擇了不同的 social contract。我們用容忍 bug 換取 speed,用 patch 它們換取 continuous feedback。這是一種理性的 trade。這也是為什麼平均 web application 的 defect rate 更接近 1980 年代平均水準,而非 IBM Cleanroom 的數據。
我們用它換來了什麼
替代品是一系列優先考慮 iteration 而非 correctness 的 practice。
Continuous integration 讓發現 bug 更快,但並未讓 code 做到 correct by construction。Agile 縮短了 feedback loops,但也縮短了 design phases。Test-driven development 仍然假設著 Cleanroom 所拒絕的 write-test-fix loop。
這些都不是 bad practice。現代 stack 優化的是發現使用者想要什麼。Zero-defect engineering 優化的是正確實現已知的 specification。
不採納整個系統也能借鏡什麼
你可能無法在公司實施完整的 Cleanroom。但其 underlying principles 可以遷移,一些現代 tool 可以在沒有 overhead 的情況下近似該 method 的 rigour。
寫 contracts,不只是 comments。 顯式定義 preconditions 和 postconditions,並在 code 中 enforce。
from dataclasses import dataclass
from decimal import Decimal
@dataclass(frozen=True)
class Transfer:
from_balance: Decimal
to_balance: Decimal
amount: Decimal
def execute(self) -> tuple[Decimal, Decimal]:
# Preconditions stated and checked
assert self.amount > 0, "transfer amount must be positive"
assert self.from_balance >= self.amount, "insufficient funds"
new_from = self.from_balance - self.amount
new_to = self.to_balance + self.amount
# Postcondition: total value is conserved
assert new_from + new_to == self.from_balance + self.to_balance
return new_from, new_to
透過將假設 encode 為 executable checks,你將隱式的 reasoning 轉化為顯式的 guards,迫使你在 development 期間思考 edge cases。
將 property-based testing 用作 statistical process control。 Cleanroom 使用基於 usage profiles 的 statistical testing。現代 property-based testing tool 透過生成 random inputs 並驗證 invariants,做著類似的事情。
from hypothesis import given, strategies as st
from datetime import datetime, timedelta
@given(
start=st.datetimes(min_value=datetime(2000, 1, 1)),
delta=st.timedeltas(min_value=timedelta(0), max_value=timedelta(days=365))
)
def test_duration_roundtrips(start, delta):
"""Adding then subtracting the same duration must return the original."""
assert start + delta - delta == start
該 test 在巨大的 input space 上 assert 一個 mathematical property。當發現反例時,它會 shrink 到 minimal failing case。你檢查的是 computation 的結構,而非具體範例。
讓 invalid states 無法表示。 Cleanroom 的 box structure methodology 是關於在越來越詳細的層次上定義行為。現代等價物是使用 static types 來防止非法狀態。
from typing import NewType
UserId = NewType("UserId", int)
OrderId = NewType("OrderId", int)
def fetch_order(order_id: OrderId) -> dict:
...
# This will not compile in a typed codebase:
# fetch_order(UserId(42)) # type error: expected OrderId, got UserId
Type checker 成為在任何 test 之前執行的 verification layer。你無法將 user ID 傳入需要 order ID 的位置,因為 type system 從結構上讓這種錯誤不可能發生。
這仍然重要的地方
Zero-defect engineering 並沒有變錯。它變成了 niche。
在單個 bug 就會出人命的 safety-critical domain 中,它仍在使用。Medical devices、avionics 和 nuclear control systems 仍在使用衍生自 Cleanroom 的 process,因為 defect 的成本仍然是天文數字。FDA 和 DO-178C standards 以不同名稱保留了許多相同的思想。
對我們其他人來說,web application 中的 bug 成本是一張 support ticket 和一次 deploy。Pacemaker 中的 bug 成本是一條生命。Method 沒有停止工作。是我們不再需要它了。
令人不安的真相
Zero-defect engineering 消失不是因為它失敗了,而是因為業界重新定義了 success。目標從「ship 第一次就正常運作的 software」轉變為「ship 得足夠快,讓損壞的 version 在使用者察覺之前就被替換」。
這是一種可以辯護的 trade。它構建了現代 internet。這也意味著大多數 software 是用數學上保證會留下 defect 的 process 構建的,而我們接受這一點,是因為事後修復現在足夠便宜了。
你不需要採納 Cleanroom 就能寫出更好的 code。從一個 function 開始。在 body 之前寫它的 contract。針對 invariants 執行 property-based tests。使用 types 讓 illegal states 不可達。Track bugs 的來源並修復產生它們的 process。
KLOC 零缺陷可能不是你的目標。但理解為什麼該 method 有效,以及為什麼我們不再關心,會告訴你關於你的 process 真正在優化什麼的重要資訊。大多數團隊從未明確做出過這個選擇。他們從幾十年前選擇 velocity 而非 correctness 的業界繼承了它,並且從未回頭。