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 的行业继承了它,并且从未回头。