一个模型通过当前预订场景,还不能说明它能够承受变化。延期会改变占用窗口,换设备会改变资源身份,追溯改价会改变历史金额的解释。三者都可能破坏“原请求重放仍返回原结果”这条看似独立的规则。

本篇对冻结的27-v1施加这三张变化卡,在旁边增加教学用的版本二状态层。原核心没有被修改,也没有把新功能倒写成既有应用的能力。每张卡都有实际成功路径、反例和保留事实的检查。

冻结基线再定义变化身份

初始快照有两台在役设备。原申请占用 E1 的十点到十一点,E1 另有十二点开始的租期,E2 则在十点半到十一点二十分被占用。所有时间采用 UTC,报价沿用每小时十二元五角,按小时向上取整。

27-v1规定请求号标识完整的原申请和费率,不能拿同一个号改租期或设备。因此修订需要新的预订请求号;变化操作还需要自己的 operationId,以识别一次延期、换设备或调价的重试。这两个身份解决不同层次的重复,不可互换。

版本二的 State 包含原应用快照、变化回执、金额调整记录和前后请求关系。收到相同操作号与相同负载时返回历史结果;同号不同负载直接拒绝。变化的原因和修订目标由显式命令传入,不从当前目录或现有活动记录中猜测。

flowchart LR
  V1[27-v1完整快照] --> V2[版本二状态层]
  V2 --> B[原设备 日历 回执 合同]
  V2 --> R[变化操作回执]
  V2 --> S[旧请求到新请求关系]
  V2 --> L[金额调整记录]
  E[延期卡] --> S
  W[换设备卡] --> S
  P[追溯改价卡] --> L
  R --> RETRY[同操作重放历史结果]

这个状态层由 Amendments 持有,方法在单实例内同步。输入快照来自实验自己生成的可信对象,没有提供任意外部 JSON 的导入校验。构造时保留全部原事实并建立空的新集合,不把旧请求猜成曾经发生过的延期或改价。

延期和换机发生在预约开始之前,仍受原工厂的一小时提前量约束;租用中延长结束时间需要另定政策。两台样本设备类型相同,实验也没有把任意跨品类设备都视为可互换资源。

延期卡:失败不能丢掉原承诺

延期命令保持设备、开始时间和约定费率,只延长结束时间,并要求新的请求号。把原租期延长到十二点十分,会与十二点开始的下一笔租赁冲突。实验返回 OVERLAP_REJECTED,随后比较整个原应用快照,确认占用、草案与回执仍然一致。

实现没有直接在正式仓储中先取消,再尝试新建。它先由原快照构造候选仓储,在候选中调用未改动的27取消和预订用例;只有新预订确认后,外层才替换自己的完整快照。候选失败时,正式状态保留,变化层只增加这次失败操作的回执。

延长到十二点恰好与下一笔相邻,因此可以确认。新草案计两小时,总价二十五元;原草案仍保存十一点结束和十二元五角。旧活动占用被解除,successors 保存旧请求到新请求的关系,使历史与当前承诺各有明确去处。

再次提交相同延期命令,即使 now 已经向后移动,也只返回原确认,不追加新草案。若保持操作号却换成另一段租期,则拒绝。这里复用了基线的重放思想,但变化回执是新增结构,旧核心并不知道它的存在。

换设备卡:检查新资源再提交

换设备命令保持租期和费率,要求目标设备不同。把十点到十一点的原租赁移到 E2,会与其十点半开始的承诺冲突。实验再次断言失败后的原应用快照完整保留,不能为了“已经发起换机”先释放 E1。

在独立样本中先释放 E2 的冲突租赁,再从该快照建立新的变化层,同一换设备负载即可确认。这里是两份不同初始条件的对照,不能误读成原来失败的操作号在同一历史里重新成功;失败回执在原实例内仍然有效。

flowchart TD
  C[变化命令] --> H{已有操作回执}
  H -->|相同负载| OLD[返回历史]
  H -->|无| COPY[由正式快照构造候选]
  COPY --> CANCEL[候选取消旧占用]
  CANCEL --> RESERVE[27用例尝试新预订]
  RESERVE -->|拒绝| KEEP[保留正式快照]
  RESERVE -->|确认| SWAP[一次替换外层快照]
  KEEP --> RECEIPT[保存变化结果]
  SWAP --> RECEIPT

成功换机后,新合同草案指向 E2,旧草案仍指向 E1。两者不能被一条可修改的 equipmentId 覆盖,否则历史回执重放虽然仍打印 CONFIRMED,所指的商业事实已经变化。对比两份合同的设备身份,比只检查活动设备数量更能说明这项约束。

候选内的维修端口在本实验固定放行,没有把真实远程维修调用纳入原子变化。候选产生的内存事件也不向外部发布。若直接复用真实发布器,候选尚未提交就可能对外发送“取消成功”,从而破坏这种隔离方式;可靠发布仍是后续架构工作。

追溯改价卡:保存差额与依据

第三张卡把原来一小时十二元五角的草案调整为二十元。实现追加七元五角的调整记录,保存目标请求、目标费率版本与原因,原合同仍然为十二元五角。有效金额由原金额加全部调整计算,不以覆盖原记录隐藏变化。

再调整为十元时,差额应从当前有效金额二十元计算,得到负十元;不能再次从原十二元五角计算负二元五角后累加,否则结果会错误地变成十七元五角。实验实际断言两条调整保留、最终有效金额为十元,以及重复提交第一条修订不会再次加钱。

Fowler 的2005年 Accounting Entry 草稿讨论通过新增记录表达数量变化、关联引起变化的事实;Retroactive Event 则讨论历史修正引起的后续处理。本例只借用显式保留调整依据的思路,没有实现完整事件重放、账户借贷平衡或账务系统。Accounting Entry 草稿、Retroactive Event 草稿。

跨币种调价直接拒绝,避免把两种金额机械相减。已经调价的草案随后再延期,也明确拒绝:延长时应使用原费率、调整后费率还是另行报价,当前合同没有决定。这个缺口是评审发现的未决业务规则,不能为了三张卡都显示绿色就默认任选一种解释。

“追溯”在本实验只指对既有草案追加修订,不含交易发生时间与记录时间的双时间模型。调整不是支付、退款或会计分录,金额得到正确结果也不能推导真实财务义务已被改变。

迁移成本如何留下证据

版本一迁入版本二时,原应用快照逐值相等,新集合为空。两次调价以后,新增两条变化回执和两条调整记录,原事实没有改写。将完整版本二快照恢复到新实例后,有效金额与回执仍保留,旧修订再次重放也不会产生第三条调整。

旧核心仍能读取其中的原应用快照,但它看不到调整和前后请求关系。因此,只把这个内部快照交回旧程序,会丢失版本二语义。可读不等于可以无损降级;新增数据需要保留、导出或显式转换,不能把“旧代码没抛异常”作为回滚成功标准。

实现还付出了复制成本。候选仓储恢复会复制设备、日历、回执和草案,随后取消与新预订又分别建立会话快照。实验没有测量吞吐或内存峰值,不能把常数个命令调用说成常数空间。规模扩大后应评估复制、事务和持久化方式,而不是先修改业务不变量迁就当前结构。

哪些工件值得继续维护

本次追踪链从三张变化卡连接到模型约束、命令类型、测试断言与原始输出。延期对应新身份、候选快照和冲突保留;换设备对应同租期、资源身份和旧草案保留;调价对应差额、币种与重复修订。每个保留工件都应能定位这些检查中的至少一项。

图保留两个用途:解释版本二新增事实,以及标注候选与正式提交的分界。代码和日志承担真实结果,图不重复抄录每一个字段。临时编译目录在入口退出时删除,交付目录不保存编译产物;没有独立用途的额外总览和逐行代码摘录也不进入维护集合。

这不是为整个系列打一个总分。已完成场景说明当前合同成立,失败反例说明边界确实起作用,尚未决定的规则需要继续保留为问题。把未决项删掉只会让模型看起来完整,并不会降低下一次变化的代价。

复跑与实验边界

源码与变化卡合同位于 examples/software-modeling/labs/39/ 和 models/39/。设置 JDK 21 后运行:

1
2
bash examples/software-modeling/labs/39/run.sh
bash examples/software-modeling/labs/39/run.sh unsupported

正向入口完成二十七项检查;第二条在正常检查后实际触发“调价后再延期”的未决政策拒绝,退出非零。原始结果、依赖哈希、迁移观察与退出码保存在 evidence/39/,下载三张变化卡及可执行评审 可按原目录复跑。

27-v1基线 在临时复制目录中另行通过原有回归。当前新增的是可信快照上的单实例教学状态层,没有数据库迁移、崩溃恢复、跨进程事务或生产修改。模型是否值得保留,最终要由下一次具体变化继续检验。