设备退役后不能重新审批为可用。这个规则可以写进 Java 方法,也可以放入状态迁移表,由解释器执行,或者转换成另一份程序。三种做法都使用模型,模型在执行链里的位置却不同。把它们都简称为 MDD,会掩盖谁负责实现规则、修改以后需要重新运行哪一步。

本篇沿用第24篇的设备生命周期合同,制作手写、解释执行、转换生成三条路径。实验只比较三个状态和三个操作,不把设备身份、描述、仓储与整个租赁应用一并生成。

相近名称下的不同问题

Evans 的 Model-Driven Design 强调设计与领域模型之间直接、清楚的对应,让术语和职责进入代码,同时让实现经验反馈到模型。这种关系可以通过手写实现维持,不以自动生成代码为成立条件。DDD Reference,2015,印刷页6

为避免缩写混淆,本系列将 MDD 用作 Model-Driven Development,讨论模型如何参与开发工件的产生;MDE 用作 Model-Driven Engineering,讨论建模语言、约束、转换和工具链的工程安排。这是行文约定,不把两者划成没有交集的标准分类。Schmidt 的原论文将领域专用建模语言、元模型约束与转换生成机制放在一起讨论。Model-Driven Engineering,2006,页26–27

OMG 的 MDA 则是一套由 OMG 推动的方法,强调将业务和应用逻辑与平台技术区分,以模型组织规格,并与 UML、MOF 等相关标准衔接。OMG MDA 官方说明

因此,“模型生成了一段 Python”只能证明某条转换发生了。它没有自动证明模型具备平台无关语义、转换覆盖完整,也不等于通过 OMG 工具兼容认证。这个小实验用来区分执行机制,尚未构造完整的 CIM、PIM、PSM 工件链。

先确定要保留的生命周期合同

第24篇新设备从 INSPECTION_REQUIRED 开始;审批通过变为 ACTIVE,重新要求检查则返回待检。待检和活跃设备都可退役为 RETIRED,退役后没有恢复操作。重复审批或重复退役都拒绝,不能默默保持原状并报告成功。

本篇直接阅读了该章 Equipment.java 和 models/24/contracts.md,再将生命周期子集写为 equipment.json。其中有三个状态、三个操作、四条允许迁移。期望结果另外列为九格矩阵,覆盖每个状态与操作的组合;没有把期望值从待测迁移表反向生成。

stateDiagram-v2
    [*] --> INSPECTION_REQUIRED
    INSPECTION_REQUIRED --> ACTIVE: approveInspection
    ACTIVE --> INSPECTION_REQUIRED: requireInspection
    INSPECTION_REQUIRED --> RETIRED: retire
    ACTIVE --> RETIRED: retire
    RETIRED --> [*]

图中的终点表达本章生命周期没有后续操作,不表示设备记录被删除。设备编号连续性、历史快照和相等语义仍属于原实体合同,迁移表没有存储这些内容。把状态图执行出来,并不能证明实体的全部行为都得到保留。

手写路径把规则放进分支

handwritten.py 按当前状态判断操作。待检状态遇到审批返回活跃,遇到退役返回退役;活跃状态遇到要求检查返回待检,遇到退役返回退役。其他组合统一抛出错误。这个函数是原 Java 生命周期的 Python 教学投影,不是对 Java 类进行反射或自动翻译。

模型在这条路径上指导程序员选择分支和命名,运行时不需要读取 JSON。若以后允许退役设备复原,手写函数与合同都需要重新讨论。只改图而不改代码,正在执行的行为不会发生变化。

手写并不意味着没有模型,也不必然比自动化落后。如果规则数量很少、变化牵涉复杂业务判断,直接方法可能更容易审阅。是否值得维护工具链,需要比较实际变化和维护成本,当前实验没有测量生产效率。

解释路径在运行时读取迁移

解释器遍历模型中的迁移,寻找“当前状态、操作”一致的一条边,找到就返回目标状态,否则拒绝。相同的程序可以执行另一份符合预期形状的迁移表,领域规则的具体内容因此从函数分支转移到了输入数据。

本章解释器只接受随实验提供的可信结构,尚未检查重复状态、未知引用或终态出边。这个限制被专门测试:向模型加入退役设备重新审批为活跃的边,解释器真的返回 ACTIVE,而手写合同仍拒绝。数据能够被执行,不代表数据表达了正确业务。

解释器本身也属于行为的一部分。若它在重复边中取第一条而另一个执行器取最后一条,两者会产生不同结果。下一篇将规定确定性要求,提前拒绝同一状态与操作出现多个目标,而不让数组顺序决定业务选择。

生成路径多出一个需要维护的工件

生成器按状态和操作排序,将迁移表转换为 Python 字典与固定的 step 函数,并写入 evidence/29/generated-equipment.py。测试随后从文件读取源码,编译并执行其中的函数,不以文件存在或语法正确代替行为验证。

生成物不再读取 JSON。运行时只查自己的迁移字典,未命中时抛出错误。文件头记录输入模型的规范化哈希;另一个 artifact-chain.json 保存模型、手写代码、生成器和生成物的文件哈希,便于检查本次证据由哪些内容产生。

flowchart LR
    C["24生命周期合同"] --> H["手写分支"]
    C --> M["equipment.json"]
    M --> I["解释器读取迁移"]
    M --> G["生成器"]
    G --> P["generated-equipment.py"]
    P --> RUN["编译并执行"]
    H --> TEST["独立九格合同"]
    I --> TEST
    RUN --> TEST

第二张图画出真实工件链。手写实现不由 JSON 生成,因而可以暴露迁移表的遗漏;解释器与生成器则共同依赖同一个模型。如果三条路径都从同一份错误表生成,结果一致也无法证明原业务规则被保留。

这里的哈希只能确认内容身份,不能证明内容正确,也不负责数字签名或可信来源认证。生成器输出稳定的排序,使相同输入重复生成得到相同文本;它没有承诺所有语义等价但数组顺序不同的输入都具有同一哈希。

删除一条边后分别会怎样

九格合同首先在三条路径上执行,四种允许迁移得到目标状态,五种禁止组合得到拒绝。生成物还执行一次完整路径:待检、活跃、待检、活跃、退役,核对每一步结果。

接着复制模型,删除 ACTIVE 状态的退役边。解释器立即拒绝该操作,原来的生成物却仍返回 RETIRED,因为它已经包含旧字典。用修改后的模型重新生成并执行,新的生成物才开始拒绝退役。手写路径仍然遵守原合同,返回退役状态。

这不是把删除边当成正确需求变更。它刻意制造模型与既有合同的偏差,说明每条路径的更新点。解释执行的变化发生在所读取的数据中;生成执行还需要重新构建工件;手写实现需要修改代码。验收时必须先确认业务规则是否改变,再判断哪个结果应该保留。

旧生成物没有自动监控输入文件,也没有热更新和版本迁移能力。如果应用仍装载旧文件,仅提交新模型不会改变运行结果。对应的部署和兼容责任,不能用“模型是唯一事实源”一句话代替。

转换覆盖到哪里

这次转换保留了来源状态、操作名称与目标状态,使用 Python 字典查找表达确定的单步迁移。它没有输出设备类、身份字段或业务异常类型。生成函数拒绝操作时只给出通用错误,无法像原实体一样区分重复审批、已退役或描述修改受限。

这种信息减少是可审阅的取舍:九格合同只比较下一状态或拒绝,不比较异常文案、对象身份和副作用。若把验收扩大到整个设备实体,就必须增加这些观测项,而不能继续沿用同一份九格结果宣称完整等价。生成代码的用途应与已经核对的合同保持一致。

模型指导仍然需要应用边界

设备状态判断只回答某次生命周期操作是否合法。预留还需要设备身份、维修限制、租期与占用检查,应用层也要处理请求重放、仓储和发布失败。把这张小表接入累计应用之前,必须决定谁读取状态、谁提交变化,以及生成代码与手写实体怎样避免各自保存一份状态。

本篇独立运行,不将第27篇应用实验作为前置运行依赖,也不声称已经替换其 Java 实体或准入逻辑。设备领域合同与有限状态模型之间保留显式的子集关系,自动化结果不会自动继承整套应用保证。

复跑三条路径

1
bash examples/software-modeling/labs/29/run.sh

运行需要 Python 3.10 及以上和 Bash,无第三方库。实际得到 16 条场景记录,全部 pass=true,退出码为 0。原始输出、生成源码与哈希链保存在 evidence/29/;每次复跑会重新生成该章的输出文件。

第29篇实验包 保留仓库路径前缀,解压后可运行同一命令。前置语义可对照 第24篇:Entity、Value Object与身份;本章只复用已声明的生命周期合同,没有重新执行或取代该章的 Java 验收。