软件建模23:上下文地图与防腐层
维修接口返回 DONE,租赁系统能否立即接受这台设备的新预留?如果 DONE 只表示工单处理结束,设备可能尚未返还,也可能检验失败后被关单。把它直接复制成“可用”,会把维修流程里的一个结果扩大成租赁承诺。
上下文地图还要表达协作权责
两个方框之间画一条 HTTP 箭头,只能说明存在调用,不能说明下游能否影响上游排期。Evans 的《DDD Reference》把 Customer/Supplier、Conformist 和 Anticorruption Layer 分别放在具名上下文关系中:前者让下游需求进入上游计划;Conformist 接受上游模型;防腐层则用下游自己的模型表达上游提供的能力。2015年模式摘要,印刷页32—34
本例假设维修供应方独立维护工单系统,租赁方不能要求它按本方优先级发布功能。于是维修为上游,租赁为下游,租赁方承担防腐层的开发和兼容成本。这是用于教学的组织条件,没有真实供应商访谈或双方协议。
flowchart LR
U[维修上下文 上游] -->|版本化维修DTO| A[租赁方维护的防腐层]
A -->|维修限制评估| D[租赁上下文 下游]
U -.维护状态定义和版本说明.-> A
D -.维护设备映射和接收规则.-> A
图中的关系选择是下游采用防腐层。上游负责其协议和维修事实,下游负责解释这些事实对租赁有什么影响。图没有声称供应方已经承诺兼容窗口;如果真实合同要求兼容期,就应把具体期限、测试责任和变更通知方式补进协作约定。
若供应方愿意把租赁需求纳入排期,双方可以建立 Customer/Supplier 协作,讨论验收用例和变更时间。只是在消费一个接口,并不足以证明这种关系已经存在。下游若选择完全沿用维修模型,则更接近 Conformist,但它要接受工单语言对租赁设计的约束。
本例没有选择共享核心模型。工单状态和设备租期承诺的含义不同,共用一个 Status 枚举只会掩盖差异。也没有把防腐层单独部署成服务:Python 实验中它就是一个纯函数。上下文边界和进程边界可以重合,却没有必须一一对应的要求。
先冻结需要翻译的语义
附件的 contract.json 定义两个合成接口版本。第一版包含设备外部编号、工单状态、检验结果和返还标志;状态取 OPEN、IN_PROGRESS、DONE、CANCELLED。第二版把状态拆成阶段和处理结论,CLOSED 必须结合 REPAIRED、NO_FAULT 或 CANCELLED 理解。
这次变化有业务含义。第一版的 DONE 表示已完成维修处理,第二版的 CLOSED 可以表示处理完成,也可以表示取消结案。因此把 status 改名为 phase、把 DONE 改为 CLOSED,并不能完成版本迁移。仍然需要使用处理结论和检验信息。
设备编号也不能原样透传。本例显式维护 vendor-501 → E1、vendor-502 → E2 的对应关系。外部系统的主键属于它的身份空间,恰好长得像租赁设备编号也不构成同一身份的证据。未映射设备必须先核实,不能临时拼接一个新编号替代。
映射表还隐含了设备对应关系已经确认的前提。维修方把一个编号重新分配给替换设备时,旧映射可能继续通过字符串校验,却指向错误实物。当前实验没有模拟编号回收,因此接入时需要约定身份是否可复用,必要时把供应方名称和身份世代纳入映射。适配器能拒绝未知编号,无法仅靠格式判断已知编号背后的实物是否变了。
租赁侧只接收三种维修限制:BLOCKED、REVIEW_REQUIRED 和 RELEASED,分别表示维修条件仍阻止接收、信息需要核查、维修限制已解除。RELEASED 的范围很窄,既不表示租赁方验收已经通过,也不表示某个未来区间没有其他预留。
| 维修事实 | 租赁侧结果 | 翻译理由 |
|---|---|---|
| 检验失败 | BLOCKED | 关单不能覆盖检验失败 |
| 工单取消 | REVIEW_REQUIRED | 取消本身无法证明设备可恢复 |
| 处理未完成或尚未返还 | BLOCKED | 接收所需事实尚未成立 |
| 已完成、已返还,检验待定 | REVIEW_REQUIRED | 缺少明确检验结论 |
| 已完成、已返还且检验通过 | RELEASED | 仅解除维修侧限制 |
表中顺序属于本实验规则:失败检验优先于取消标志,所以“取消且检验失败”仍返回 BLOCKED。它不是行业统一政策。真实业务若允许另一个合格检测报告覆盖旧结果,需要定义报告身份、时间和优先级,再修改规则和测试,不能在适配器里猜测。
防腐层必须允许翻译失败
flowchart TD
DTO[外部维修DTO] --> V{版本和字段可解释?}
V -->|否| E[明确拒绝并保留错误原因]
V -->|是| ID{设备身份可映射?}
ID -->|否| E
ID -->|是| T[按版本解释处理与检验事实]
T --> A[Assessment 限制 原因 来源]
A --> C{限制已解除?}
C -->|否| R[阻断当前接收或进入核查]
C -->|是| N[继续租赁侧生命周期与租期检查]
translate 只接受版本1或2,未知版本、工单状态、检验结果和设备映射都抛出带原因的 ValueError。已知但不充分的事实才形成 REVIEW_REQUIRED。两者不能混在一个“其他”分支中:前者表示协议无法解释,后者表示协议能解释,但业务结论还不完整。
返还标志要求真正的布尔值。字符串 "false" 在某些语言判断中是真值,宽松转换可能把未返还误认为已返还。实验专门给这个输入设置拒绝断言,而没有使用字符串非空就算真的便捷写法。
防腐层也不应捕获所有异常再返回 RELEASED。代码缺陷、网络超时和未知协议不是维修通过的证据。本实验不发网络请求,因此没有重试和熔断逻辑;真正接入远程接口时,调用失败应在应用层形成可诊断结果,并由明确策略决定是否延期处理。
消费者函数 require_released 只接受 RELEASED,对 BLOCKED 和 REVIEW_REQUIRED 均拒绝继续。它尚未创建人工核查任务,所以“进入核查”只是应用必须实现的后续处理,不能靠一个枚举就声称工作流已闭环。
压缩信息时留下可解释的来源
OPEN 与 DONE 加失败检验都会产生 BLOCKED。租赁入口暂时可以统一阻断,但排查问题时必须知道是维修没结束,还是修完后仍不合格。因此 Assessment 还保存原因、来源版本、原状态、原检验结果和返还事实。
输出这些字段并不等于完整保存供应商模型。工时、维修成本和技术员备注没有进入本例协议,适配结果也不能反向还原完整工单。它是有明确用途的投影,保留当前接收判断需要解释的材料,而不是无损复制全部上游数据。
实验把两个 BLOCKED 结果作比较,确认限制相同但原因和来源状态不同。另一个负例只依据 DONE 生成布尔“可用”:当检验结果为 FAIL 时,简化算法仍返回真,而正式翻译得到 BLOCKED。这个差异由真实断言观察,不是把预期结论写成展示日志。
即使版本号没有变化,上游也可能偷偷改变同一个状态的含义。当前程序只能验证已冻结的协议,无法从字符串推断这种未声明变化。双方的样本验收、版本政策和领域交流仍然必要,防腐层不会自动识别所有业务误解。
版本测试也应保留旧样本。新增第二版分支时,第一版 DONE、失败检验和取消工单仍然要得到原先约定的结果;只测试新接口能解析,会漏掉无意改变旧协议的回归。这个实验把两版放在同一次运行中检查,失败信息直接指出版本或字段问题。接口升级的影响因而能在本次实际执行的有限样本内定位,但不构成对供应方全部可能输入的穷尽证明。
运行与后续接入
下载 适配器、契约与原始证据,在解压根目录执行。要求 Python 3.10 或更高版本,无第三方依赖:
1 | |
本次 Python 3.14.4 上执行24项检查,退出0,原始输出保存在 evidence/23/run.txt。脚本实际读取契约文件,执行两个版本的翻译、未知输入拒绝、消费者阻断和语义损失反例;没有调用真实维修系统,也没有更改 Java 租赁核心。
models/23/contracts.md 固定了后续接入需要保留的边界。维修解除只提供一个前置事实,租赁应用还需取得设备、检查其生命周期和请求区间。翻译输出尚无观察时间或事件顺序,因此不能直接作为永久有效的设备当前状态。
未来接入仓储和消息时,需要补充来源标识、过期策略、乱序和重复消息处理。旧的“已通过”消息晚到时是否能覆盖新的失败报告,是另一条必须明确的规则。把翻译做对是接入的起点,状态同步仍要按自己的可验证契约实现。






