软件复用的理想一直是:写一次,处处用。但现实是,当你拿到一个已有的软件产品(比如银行核心系统、电商平台),要把它适配到不同的客户场景时,往往会陷入两难。
如果直接修改源码:每个客户一个分支,基础产品升级时,必须手动合并代码冲突。随着客户增多,维护成本呈指数增长。
如果抽象出扩展接口:需要预判所有可能的定制点,对架构能力要求极高。大部分扩展点在设计时根本无法预见。
组件理论的隐含前提是“相同才可复用”,但任意两个业务系统 A 和 B 之间的公共部分,往往既小于 A 也小于 B。这意味着组件理论在系统级的粗粒度复用面前,理论上有局限性。
近年来,业界大量涌现出基于差量(Delta)思想的实践:
这些实践各自解决了局部问题,但它们背后是否存在一个统一的软件构造规律?
要回答这个问题,先要拆解“软件定制化”的本质。
假设有一个基础产品 A,客户需要的定制版本 B。传统思路是“修改 A 得到 B”。问题在于:修改后的 B 和 A 之间的差异,没有人显式记录下来。下次 A 升级到 A',原来的修改必须重新手工合并——因为那些修改隐含在 B 的代码里,没有独立存在。
如果换一个思路:把差异显式提取出来:
Delta = B - A
那么 B 就可以表示为:
B = A + Delta
这个 Delta 就是一个独立的结构,可以在 A 升级后重新应用:A' + Delta = B'。
拆解下来,问题有三个关键点:
如果这三个问题能解决,软件定制化就不再是修改源码,而是叠加差量。
回到拆解出的三个问题,可逆计算在每个问题上都给出了明确的回答。
如何提取差量?
差量必须有明确的提取对象。散乱的代码难以作为提取对象——你很难说清楚“这行代码相对于那行代码变了什么”。
可逆计算的答案:用 DSL(领域特定语言)作为提取对象。DSL 是结构化的、严格定义的模型(如 Excel 数据模型、XML 配置、XDef 定义)。因为结构清晰,两个 DSL 之间的差异可以被精确描述。用公式表达就是:
Delta = App - Generator⟨DSL⟩
即:最终应用减去生成器对 DSL 的映射结果,剩下的就是差量。
如何存储差量?
差量必须独立于原始代码存在,不能侵入源码,否则升级时会被覆盖。
可逆计算的答案:差量存储在独立的 Delta 层中。在文件系统中,差量文件放在独立的目录(如 _v1/、_v2/),与基础产品目录(如 _app/)完全隔离。手写的 Delta 代码从不混入自动生成的代码中,基础产品升级时只需重新生成,Delta 层自动叠加。
如何合并差量?
多个差量层叠加时,需要明确的规则处理优先级和冲突。
可逆计算的答案:x-extends 按优先级合并。框架按目录优先级自动合并多层 Delta,上层覆盖下层,同层按文件路径匹配。特别地,“可逆”意味着差量不仅可以增加内容,还可以通过标记来删除或替换已有结构。
三个问题回答完毕,完整公式即:
App = Delta x-extends Generator⟨DSL⟩
翻译成白话:一个应用 = 差量定制 × 对领域模型的生成器。
分解来看:
这套公式统一解释了 Docker(Dockerfile + OverlayFS = Delta × BaseImage)、React(VDOM diff = Delta × Render)、Kustomize(Overlay = Delta × Base)等实践的内在逻辑。Nop 平台是这个理论在 Java 服务端最完整的落地实现。
| 传统面向对象 | 可逆计算 |
|---|---|
| 继承是代码级复用 | 差量是结构级复用 |
| 修改源码来实现定制 | 叠加差量层来实现定制 |
| 组件 = 封装好的黑盒 | 模型 = 可差量化的白盒 |
| 运行时多态 | 编译期结构变换 |