IBM 交付了一套 NASA 衛星控制系統,每千行程式碼僅有 0.1 個缺陷。當時的業界平均水準在 10 到 50 之間。他們並非依靠招聘更聰明的工程師或延長工作時間來達成這項目標。他們靠的是禁止開發者執行自己的程式碼。

這就是 Cleanroom 軟體工程,由 IBM 的 Harlan Mills 於 1970 年代開發。名稱源自半導體製造業,其目標是預防產生缺陷的條件。品質不是測進去的,而是設計進去的。

問題:測試只能發現缺陷,不能預防缺陷

大多數軟體工程都預設遵循寫-測-修的循環。你寫程式碼,執行它,發現缺陷,修復它。這感覺很有效率。但從數學上講,它注定會留下缺陷。

測試只能證明缺陷存在,永遠無法證明缺陷不存在。Dijkstra 在 1969 年就說過這一點。如果你的程式碼有一千條可能的執行路徑,而測試套件只覆蓋了五十條,那麼就有九百五十條路徑未經測試。當時業界平均 10-50 個缺陷每 KLOC 的缺陷率,並不是測試不夠嚴格造成的。它是一個允許缺陷存在的流程所必然輸出的結果。

Cleanroom 反其道而行。不是先寫程式碼再測試,而是寫出構造即正確的程式碼。

Cleanroom 究竟如何運作

這一方法包含三項嚴格且相互配合的實踐。

第一,在統計流程控制下的增量式開發。 專案被拆分為多個小增量,每個增量增加一組定義明確的功能。每個增量都作為一個完整的單元,經歷規格說明、設計、驗證和測試。關鍵在於按增量測量缺陷率。如果某個增量超出缺陷目標,流程即被叫停,團隊去查找方法本身出了什麼問題,而不是追究個人責任。

第二,使用 box structure 的函數理論設計。 每個軟體元件都在三個層次上定義:

  • Black box: 純數學函數形式的外部行為,從輸入到輸出。沒有狀態,沒有實作細節。
  • State box: 內部狀態轉換函數。元件維護什麼狀態,狀態如何變化?
  • Clear box: 實際實作,由已驗證的元件建構而成。

在繼續下一層之前,每一層都必須針對上一層進行驗證。State box 未經證明正確,就不寫 Clear box。Black box 未經證明正確,就不寫 State box。這聽起來很慢。其實不慢,因為你不需要除錯。你不會在除錯器裡耗掉整個下午。程式碼第一次編譯就能跑通。

第三——這也是最讓人想不通的一點:開發者不能執行測試。 寫程式碼的人不能編譯、執行或單元測試自己的程式碼。測試由一個獨立的團隊使用統計方法進行。他們把軟體當作 Black box,基於使用輪廓而不是程式碼結構來產生測試案例。

這種分離並非官僚主義的殘忍。它是強制構造正確性的核心機制。如果你知道自己無法執行程式碼來「看看它能不能工作」,你就被迫在敲鍵盤之前把每種情況都想清楚。你不能指望編譯器幫你抓錯,也不能指望快速跑一下 python script.py 就能暴露顯而易見的缺陷。你必須一次性寫對。

重要的數字

IBM 聯邦系統事業部在 1980 年代和 1990 年代的多個專案中使用了 Cleanroom。結果在不同團隊、不同語言和不同應用領域都保持一致。

NASA 衛星控制系統:最終測試每 KLOC 0.1 個缺陷。COBOL 計費系統:每 KLOC 0.3 個缺陷。Ada 即時系統:每 KLOC 0.4 個缺陷。對比當時的業界平均:單元測試階段每 KLOC 10-50 個缺陷,交付時仍殘留 5-10 個。

Cleanroom 團隊的交付速度也更快。當時的業界資料顯示,50-70% 的軟體工作量花在測試和除錯上。Cleanroom 團隊把這段時間用於設計,程式碼一次就能跑通。

為什麼幾乎沒人使用

既然 Cleanroom 如此有效,為什麼不是人人都在用呢?

你無法在一個衝刺裡「隨便試試」。這套方法是相互依賴的。統計流程控制只有在測量每個增量時才有效。box structure 只有在進行形式化驗證時才有效。禁止執行規則只有絕對執行時才有效。部分採用帶來的不是任何好處,而是全部開銷。

市場也變了。1970 年代和 1980 年代,軟體透過磁帶交付。生產環境的缺陷修復成本高昂。今天我們透過網路交付,幾分鐘就能修補。對零缺陷軟體的經濟誘因已經減弱。

大多數人也喜歡執行自己的程式碼。寫-跑-修的即時回饋循環令人滿足。Cleanroom 要求你把這種滿足感推遲到徹底想清楚問題之後。對很多開發者來說,這感覺就像在不許回讀的情況下寫散文。

不用全盤接受,也能借鏡的要點

你大概無法在公司裡實施完整的 Cleanroom。你的上司會用奇怪的眼神看你。但你可以採納其中的部分做法,獲得實際收益。

先寫介面,再寫實作。 在寫函式本體之前,先定義輸入、輸出和前置條件。說明函式承諾什麼,而不只是它做什麼。

# precondition: items is a non-empty list of comparable elements
# postcondition: returns the smallest element in items
# raises: ValueError if items is empty
def min_item(items: list) -> any:
    if not items:
        raise ValueError("items must not be empty")
    smallest = items[0]
    for item in items[1:]:
        if item < smallest:
            smallest = item
    return smallest

這是個再簡單不過的函式,但這種紀律可以擴展。更複雜的例子是:在實作之前先顯式定義 state machine。

from enum import Enum, auto

class ConnectionState(Enum):
    DISCONNECTED = auto()
    CONNECTING = auto()
    CONNECTED = auto()
    CLOSING = auto()

# Allowed transitions:
# DISCONNECTED -> CONNECTING (on connect())
# CONNECTING -> CONNECTED (on handshake complete)
# CONNECTING -> DISCONNECTED (on timeout/error)
# CONNECTED -> CLOSING (on close())
# CLOSING -> DISCONNECTED (on ack received)
# Any other transition is illegal and raises StateError

class StateMachine:
    _transitions = {
        ConnectionState.DISCONNECTED: {ConnectionState.CONNECTING},
        ConnectionState.CONNECTING: {ConnectionState.CONNECTED, ConnectionState.DISCONNECTED},
        ConnectionState.CONNECTED: {ConnectionState.CLOSING},
        ConnectionState.CLOSING: {ConnectionState.DISCONNECTED},
    }

    def __init__(self):
        self.state = ConnectionState.DISCONNECTED

    def transition(self, new_state: ConnectionState) -> None:
        if new_state not in self._transitions.get(self.state, set()):
            raise StateError(f"Illegal transition: {self.state.name} -> {new_state.name}")
        self.state = new_state

透過顯式編碼合法轉換,你讓非法狀態無法表示。程式碼不可能進入無效狀態,因為state machine會阻止它。這就是 box structure 哲學的微縮版。

讓別人測試你的程式碼。 作者與驗證者的分離是 Cleanroom 最激進、卻也最容易移植的想法。當你測試自己的程式碼時,你只是在驗證自己的假設。別人會試到你沒考慮到的那個情況,因為你覺得它「顯然」不會發生。

按工作單位測量缺陷密度。 追蹤每個流程階段漏掉了多少缺陷。如果你的衝刺總是帶著整合缺陷交付,問題不是粗心的開發者。問題在於你的流程允許整合缺陷存在。修復流程。

哪裡會失效

Cleanroom 不是萬能解藥。它在需求穩定、正確性比上市速度更重要時效果最好。它不適用於探索性開發和快速原型,因為那裡的目標是發現使用者想要什麼,而不是正確實作一份已知的規格說明。

它還需要管理層支持。你無法偷偷實施 Cleanroom。統計流程控制需要跨團隊收集資料。禁止執行規則需要管理層真正強制執行,哪怕 deadline 逼近,開發者只想「快速驗證一下這個能不能跑」。

結論

IBM 的每 KLOC 0.1 個缺陷不是奇蹟。它是一個旨在預防缺陷而非檢測缺陷的流程的產物。box structure、統計測試、開發者隔離這些具體實踐,對現代軟體文化來說可能顯得陌生。但底層原則是永恆的:動手前想清楚,比事後除錯更便宜。

你不必全盤接受 Cleanroom。從一個函式開始。先寫契約,再寫函式本體。讓同事在執行前審閱邏輯。追蹤缺陷來源,修復產生缺陷的流程。每 KLOC 零缺陷可能不是你的目標,但從 50 降到 5 是可達成的,方法也一樣。先想。再敲。最後跑。