大多数程序员的一天是这样度过的:打开 IDE,写函数、调接口、修 bug、合分支。如果有人问"你在做什么",答案通常是"写代码"。Peter Naur 在 1985 年的论文《Programming as Theory Building》里给出了一个不同的回答——程序员真正在做的事情,是在头脑中构建关于程序的理论,代码只是这个理论的外化痕迹。
这个观点听起来像哲学闲谈,但它直接解释了为什么有些项目换了一批人之后就再也改不动了,为什么"代码能跑"和"项目可维护"之间隔着一条鸿沟。
Naur 说的"理论"不是学术论文那种理论
Naur 借用了哲学家 Gilbert Ryle 的区分:knowing that(知道事实)和 knowing how(知道怎么做)。一段程序的"理论",指的是程序员对以下问题的完整把握:
- 这个程序为什么要这样设计,而不是那样?
- 它面对的现实问题域有哪些边界条件?
- 哪些改动是安全的,哪些改动会牵一发动全身?
这种把握不是文档能写清楚的,更不是注释能替代的。它类似于一个老厨师对一道菜的"手感"——你可以把食谱印成书,但只有做过几百次的人才知道"火候到了"是什么状态。
Naur 的核心论点是:程序的生命力取决于是否有人持有这套理论。代码仓库、文档、测试用例都可以完整保留,但如果没有人内化了这套理论,项目就已经死了——剩下的只是可以编译的化石。
一个反直觉的推论:重建理论比重建代码难得多
日常经验似乎在说相反的话——代码丢了很麻烦,理解丢了可以重新读代码嘛。Naur 指出这正是误区所在。他举了一个例子:一个程序的原作者离开了团队,新接手的人即使有完整代码和文档,面对修改需求时仍然会做出笨拙甚至危险的改动,因为他们缺少原作者头脑中的那套理论。
这解释了一个常见现象:技术债的本质不是烂代码,而是理论缺失。一段写得不好但作者还在的代码,往往比一段写得漂亮但作者已走的代码更容易维护——因为前者有人能告诉你"这里为什么这样写,改的时候要注意什么"。
理论构建在日常开发中的痕迹
理论构建不是某个神秘的高阶活动,它就藏在程序员每天做的事情里:
- 命名选择:把一个字段叫
pending_until而不是flag2,是在表达对业务语义的理解。 - 模块划分:把支付逻辑和物流逻辑分开,是在声明这两个问题域有不同的变化节奏。
- 测试用例的选取:测试"用户在退款中又下单"这个场景,说明你理解了业务上的并发冲突点。
这些决策的累积,就是理论。代码是这些决策的执行结果;文档最多是决策的摘要;而真正的理论,只存在于做出这些决策的人的头脑中。
实践:把理论从头脑里"卸载"出来
既然理论如此关键又如此脆弱(它绑定在人身上),一个务实的做法是:尽可能把理论的推理过程记录下来,而不是只记录结论。Architecture Decision Records(ADR)是目前最轻量的实践方式。
下面是一个可直接使用的小项目骨架,用 ADR 把理论推理固化在代码仓库里:
# 创建项目目录和 ADR 目录
mkdir -p my-project/docs/adr
cd my-project
# 安装 adr-tools(macOS 可用 brew,Linux 可从 GitHub 下载)
# brew install adr-tools
# 或直接用下面的手动方式
手动创建第一个 ADR——记录一个真实的设计决策及其推理:
<!-- docs/adr/0001-use-event-driven-order-state.md -->
# ADR 0001: 采用事件驱动管理订单状态
## 状态
已接受
## 背景
订单状态从"待支付"到"已发货"需要经过多个中间状态。
之前的做法是在 Order 模型上直接修改 status 字段,
导致状态转换逻辑散落在多个服务中,难以追踪非法跳转。
## 决策
采用事件驱动模式:每次状态变更发布一个 OrderStateChangedEvent,
由 OrderStateMachine 趈费事件并校验转换合法性。
## 理由
1. 状态转换规则集中在一处,新接手的人读 StateMachine 就能理解全图。
2. 事件可被其他服务订阅(如通知服务),无需耦合。
3. 非法跳转在事件消费层被拦截,不会写入数据库。
## 后果
- 增加了一个 Event bus 组件,运维复杂度上升。
- 调试时需要追踪事件链路,日志必须包含 event_id。
对应的 Python 代码骨架,让 ADR 中的决策落地:
# order_state_machine.py
"""ADR 0001 的落地实现:事件驱动的订单状态机"""
from enum import Enum
from dataclasses import dataclass
from typing import Optional
class OrderStatus(Enum):
PENDING_PAYMENT = "pending_payment"
PAID = "paid"
SHIPPED = "shipped"
DELIVERED = "delivered"
REFUNDING = "refunding"
# 合法转换表——这就是"理论"的核心:哪些跳转是允许的
VALID_TRANSITIONS = {
OrderStatus.PENDING_PAYMENT: [OrderStatus.PAID, OrderStatus.REFUNDING],
OrderStatus.PAID: [OrderStatus.SHIPPED, OrderStatus.REFUNDING],
OrderStatus.SHIPPED: [OrderStatus.DELIVERED],
OrderStatus.DELIVERED: [OrderStatus.REFUNDING],
OrderStatus.REFUNDING: [OrderStatus.PAID], # 退款后恢复
}
@dataclass
class OrderStateChangedEvent:
order_id: str
from_status: OrderStatus
to_status: OrderStatus
reason: Optional[str] = None
class OrderStateMachine:
"""消费事件,校验并执行状态转换"""
def can_transition(self, from_status: OrderStatus, to_status: OrderStatus) -> bool:
return to_status in VALID_TRANSITIONS.get(from_status, [])
def handle_event(self, event: OrderStateChangedEvent) -> OrderStatus:
if not self.can_transition(event.from_status, event.to_status):
raise ValueError(
f"非法状态跳转: {event.from_status.value} -> {event.to_status.value}"
f" (order={event.order_id})"
)
print(f"[OK] order={event.order_id} "
f"{event.from_status.value} -> {event.to_status.value}")
return event.to_status
# 使用示例
if __name__ == "__main__":
machine = OrderStateMachine()
# 合法跳转
e1 = OrderStateChangedEvent("ORD-001", OrderStatus.PENDING_PAYMENT, OrderStatus.PAID)
machine.handle_event(e1)
# 非法跳转——会被拦截
e2 = OrderStateChangedEvent("ORD-001", OrderStatus.PENDING_PAYMENT, OrderStatus.SHIPPED)
try:
machine.handle_event(e2)
except ValueError as err:
print(f"[BLOCKED] {err}")
运行结果:
[OK] order=ORD-001 pending_payment -> paid
[BLOCKED] 非法状态跳转: pending_payment -> shipped (order=ORD-001)
注意 VALID_TRANSITIONS 这个字典——它不是"代码逻辑",它是业务理论的直接表达。一个新接手的人读这个字典,就能在 30 秒内理解"订单能不能从待支付直接跳到已发货"。这就是 Naur 所说的理论的可传递形式,虽然它仍然不如原作者头脑中的理解完整,但比散落在十几个函数里的 if-else 要好得多。
接手别人的代码时,先找理论
Naur 的论文给日常开发提供了一条实用的诊断线索:当你觉得一段代码"难以理解"时,问题往往不在代码本身,而在你缺少构建这段代码的理论。
几个可操作的习惯:
- 接手项目时,先问"为什么这样设计"而不是"代码怎么跑"。找原作者或老同事聊 30 分钟设计理由,收益大于读 3 天代码。
- 写 ADR 或设计笔记时,重点写"为什么不选 B"。被否决的方案比被采纳的方案更能揭示理论。
- 在关键模块的文件头写一段"理论摘要"——3 到 5 行,说明这个模块存在的理由和它不该做的事情。这比 200 行注释更有理论价值。
- 团队里有人离职时,做一次"理论交接":不是交接代码权限,而是让他讲一遍核心模块的设计理由和踩过的坑。
理论构建的边界与风险
Naur 的观点也有需要警惕的地方:
- 理论不可完全文档化——ADR、设计笔记都是"理论摘要",不是理论本身。不要以为写了文档就等于传递了理论。
- 过度理论化会拖慢交付——不是每个模块都需要深度理论。工具类、CRUD 页面这类确定性高的代码,理论成本很低,不值得花大量时间做决策记录。
- 理论可能过时——业务变了,原来的设计理由不再成立,但 ADR 还在。定期回顾和标记废弃的 ADR,否则它们会误导新人。
一句话总结 Naur 给开发者的提醒:你写的代码会过时,你建的 theory 才决定项目的寿命。下次面对一个"代码能跑但没人敢改"的模块时,问题多半不是代码写得差——是理论已经没人持有了。