漂移为何永不消亡 Image: AI generated

修好的地方又冒出来了

我造了一个封闭漂移的工具。

yongol 的论题很简单:决策如果不住在一个权威的单一来源(SSOT)里,就会漂流。所以把决策放进 SSOT,让代码变成每次生成时都重新绘制的一次性 projection。一个声明为 BIGINT 的列在一次重构后悄悄退回 INT——这种业务逻辑漂移,就这样被封闭了。

然而不久前,我在分析 yongol 生成的代码中的一批缺陷时,看到了奇怪的东西。那些缺陷用相同的句式自我告白:“import 收集与’处理器是否真的用了 time‘是分离的。““requiredness 推断与’目标 API 是否真的把这个参数标为 required’是分离的。“路径参数总是 required,import 仅在 token 被实际使用时才需要。同一类结构性决策,没有写进任何 SSOT,却以方便的本地代理(proxy)固化在生成器代码里。

漂移并没有消失。它上移了一层。 业务逻辑已被 SSOT 封闭,但读取 SSOT、输出代码的生成器自身的结构性决策却没有 SSOT。yongol 的论题原封不动地回弹到了 yongol 自己身上——恰好在理论预测的那个位置。

于是问题变了。漂移为什么会发生,谁都知道。真正的问题是:修好了为什么还会卷土重来。

根源:决策与细节是两回事

从物理开始重新构建。

决策是信息——而且是低熵的信息。“这个列必须是 64 位”,是从无数可能状态中有意挑出的一个。自然界厌恶低熵。放任不管,信息就会扩散成周围的噪声而消失。热力学第二定律同样适用于决策。

软件工程很早就观察到了这种坍塌。Lehman 的软件演化定律指出,E-type 系统的复杂度在没有明确削减努力时只增不减(1980)。信息物理学走得更深。Landauer 在 1961 年证明,哪怕只是擦除 1 比特,也需要最低限度的热力学成本(kT ln 2)。改变和保持信息在原理上从来不是免费的。让决策待在原位需要持续耗能。

信息要存活,需要两样东西:权威存储(authoritative store)和从中不断重绘的主动再投影(error correction)。我们身体里的 DNA 如此,数字存储的奇偶校验位亦然。把原本单独保存,每次以它为基准还原。

漂移就发生在这道还原链断裂之时。机制只有一个。我称之为代理绑定(proxy binding)。当介质无法区分并保存决策细节时,下一个人(或下一个 agent)就无法从权威来源读取决策,而只能从旁边方便的相关信号中重新推导。“这列是 timestamptz,那就该 import time 吧”——这种猜测。大多数时候是对的。正因如此才危险。偶尔猜错,错的那一刻,决策便无声地消失了。

raw 代码恰恰就是这样的介质。代码不会区分标注"这是决策"与"这只是在这里碰巧成立的”。所以更大的模型也解决不了问题。介质本身不能承载决策,读取方再聪明也无可读之物。

这一现象并非无名之物。在软件架构中,Perry 和 Wolf 把违反原则称为侵蚀(erosion),把对架构失去感知称为漂移(drift)(1992);Cunningham 把写得不到位的代码所附带的利息叫做技术债(1992)。各领域对症状的命名已经足够好。我要补充的,是其底层的单一机制(代理绑定),以及这个机制在封闭一层后会向上层递归的结构。追问的不是名字,而是因果。

为什么会向上迁移

到此为止,都是已知的故事。新东西在后面。

要封闭漂移,需要两样东西:一个权威地承载决策的存储(SSOT),和一个读取存储、输出产物的封闭主体(生成器)。然而,封闭主体自身也在做决策。 比如"路径参数一律视为 required"这样的结构性决策。承载那些决策的介质——生成器代码——同样无法区分决策与细节。

同一机制在上一层重复。封闭行为本身在上面制造了一个未被封闭的介质。漂移不是被消灭了,而是搬家了——搬到了没有权威的那一层。

把这个逻辑推到底,会得出一个令人不安的结论。给生成器加一个 SSOT?那么制造那个 SSOT 的某个东西又会把自己的决策存放在一个未被封闭的介质里。每上一层,表面积确实缩小,但最顶端总有一个没有权威的层——或是人,或是生成器的生成器。漂移在渐近意义上不可根除。(这与其说是证明,不如说是一个强有力的猜想。不过迄今为止我封闭的每一层,都在封闭的瞬间打开了上面那一层。)

这就是"修好了为什么还会卷土重来"的答案。它并没有回来。是我们封闭了一层之后,封闭工具打开了下一层。同一条河流,在更高的堤坝上重新渗漏。

处方的不对称:能声明的与只能验证的

那上面那层怎么封闭?关键的不对称性在这里浮现。

业务逻辑的决策大多是。列是 64 位,访问仅限拥有者,分页用游标。值可以声明。写在 DDL 里、OpenAPI 里、规格文件里,那就是 SSOT。用声明封闭。

生成器的结构性决策不同。“路径参数是 required"“import 绑定到实际 token 引用"“required(键存在)与非空是两回事。“这些不是值,而是贯穿所有输入的函数行为属性。行为属性无法用声明枚举——因为输入是无限的。没有办法在 YAML 的一个字段里写下"这个变换在所有情况下都必须如此行事”。

因此,这一层的决策只能用验证来封闭。类型检查器、属性测试、编译 gate。不是把决策钉死在数据里,而是用机器每次都能捕获违规的关卡来钉死。

我在另一篇文章里写到"把人工检查代码化”,说的就是这个位置。有些约定可以声明,由 SSOT 守护;有些约定无法声明,只能由 gate 守护。生成器输出的代码是否能编译,写不进任何 SSOT。唯有每次都跑一遍编译才能确认。没有那道关卡,“generate 成功 = 可构建"这个约定就漂浮在架构之外,validate 以 0/0 通过,产物却是坏的。

能声明的漂移用 SSOT 封闭,只能验证的漂移用 gate 封闭。把两者混淆,就会试图用声明去堵,永远在打地鼠。

同一条河,不同的堤

这个结构在代码之外同样反复上演。

在知识领域,漂移是出处的丧失。一个主张丢失了"谁、何时、基于什么依据说的"之后,它就扩散成名为"事实"的噪声。下一个人无法从权威(原始出处)读取,只能从周边情境重新推导。这就是我把 GEUL 设计成强制给所有信息附加出处、时间点和可信度的语言的原因。“没有事实,只有主张"的认识论,是针对知识层代理绑定的安全装置。

在法律领域,漂移是判例偏离原初决定。文明并没有把这件事交给每个案件中法官的良心,而是将规则成文化、定义违规、附上强制机制来封闭。好法官不是 SSOT,而是代理。成文法才是 SSOT。

同一条河。决策若不住在权威的位置上,介质若无法区分决策与细节,它就会漂流。无论代码、知识还是法律。

结论:不是根除,而是上推

与漂移的战斗不能以根除为目标。根除不可能——因为封闭工具总会打开下一层。

目标是另一件事。把漂移推到更高、表面积更小的层,再用机械化验证武装那一层。 把散落在数万行 raw 代码中的决策汇聚到一处 SSOT,可漂流的表面就急剧缩小。剩余的表面——生成器的行为不变式——用 gate 封堵。即便如此,最顶端依然留着一个无处再委托的最后一层——人的判断。在那里,我们每次重新验证,每次重新钉入约定。

这就是棘轮(ratchet)。只朝一个方向转。一旦上去的齿,就不会滑落下来。熵试图把决策拖下去,棘轮每次再推上一格。没有平衡点。停下就漂流。

漂移永不消亡。所以我们永不停歇。对抗熵、铸造约定这件事,不是一次胜利,而是永恒的棘轮。

相关文章

延伸阅读(外部)

参考文献

  • Perry, D. E. & Wolf, A. L. (1992). Foundations for the Study of Software Architecture. ACM SIGSOFT Software Engineering Notes, 17(4), 40-52. ACM — 侵蚀(erosion)与漂移(drift)的区分。
  • De Silva, L. & Balasubramaniam, D. (2012). Controlling software architecture erosion: A survey. Journal of Systems and Software, 85(1), 132-151. ScienceDirect
  • Lehman, M. M. (1980). Programs, Life Cycles, and Laws of Software Evolution. Proceedings of the IEEE, 68(9), 1060-1076. IEEE — 复杂度递增法则与持续变化法则。
  • Landauer, R. (1961). Irreversibility and Heat Generation in the Computing Process. IBM Journal of Research and Development, 5(3), 183-191. IBM — 信息擦除的最低热力学成本。
  • Shannon, C. E. (1948). A Mathematical Theory of Communication. Bell System Technical Journal, 27, 379-423. DOI — 信息、熵与纠错的奠基论文。
  • Cunningham, W. (1992). The WyCash Portfolio Management System. OOPSLA ‘92 Experience Report. c2.com — 技术债与"写得不到位的代码的利息”。