软件建模16:四色与领域无关组件
租赁 A1 原定在 11 点结束,客户申请延到 13 点,但同一设备 E1 已在 12 点承诺给 A2。若先生成新的租赁文档,再检查设备占用,延期虽然被拒绝,文档却可能显示 13 点。换一台相机也不能只改设备编号:候选设备可能已被占用,或者其实是一台投影仪。
这些变化需要同时考虑参与方、资源、活动和文档。领域无关组件可以提供候选关系,但租赁的冲突条件与更新顺序仍须另外定义。本篇用两个实际候选对照,检查延期和替换失败以后保留了哪些状态。
从原书选择需要的部分
出版社第 5 章包括项目活动、会计及文档管理。活动部分区分请求与活动,文档部分区分模板、具体文档及相关活动。这些分组是知识复用入口,并非可以直接调用的租赁库。出版社原章,§5.1、§5.3,印刷页154–181
本例只选活动和文档的部分关系:参与方提出租赁约定,约定引用具体资源;成功修改约定时,生成新文档修订并保留旧版。没有复制原书全部组件,也没有实现会计管理。代码中的 RentalService、资源冲突算法和模板选择政策都是本系列自己的教学适配。
“领域无关”在这里表示一种关系组织可迁移到多个业务。它不意味着每个领域都能共用相同字段与校验规则。会议室使用、设备租赁和人员排班都涉及期间及资源,但能否并行、是否允许超额以及如何替换,需要分别确认。
flowchart LR
P["参与方 P1"] --> A["租赁活动计划 A1"]
E["设备资源 E1"] --> A
A --> D["具体文档:A1 / 修订1"]
T["文档模板 / 修订1"] --> D
classDef green fill:#c8edcc,stroke:#32633a,color:#222;
classDef pink fill:#f8c8dc,stroke:#71334b,color:#222;
classDef blue fill:#c9e2ff,stroke:#355b80,color:#222;
class P,E,D green;
class A pink;
class T blue;
第一张图是租赁教学候选,不是原图翻绘。箭头表示引用或生成关系,未声明原书多重性、数据库外键或聚合所有权。A1 在代码中表示已经确认的计划;设备实际交付、使用和归还尚未执行,不能把计划期间直接当成实绩。
参与方在此仅保留标识,用于说明计划由谁关联。09 的角色有效期模型没有被隐式接入,代码也不据此作授权判断。组件之间画出一条关联,不等于前一章的全部保证都会沿着关联自动传递。
延期先改变候选计划
固定输入中,A1 使用 E1 的 [10,11),A2 使用 E1 的 [12,14)。时点均为同一天的 UTC 整数小时,区间包含起点、不包含终点。把 A1 延到 13 点会与 A2 重叠;延到 12 点则刚好相邻,可以接受。这项判断与“有没有新的文档模板”无关。
实现先构造新的不可变 Rental 值,再检查期间、设备类别及其他预留。比较时排除同编号的旧约定,否则一次延期会与自己的原区间相撞。排除自己只适用于修改既有约定;创建入口另外拒绝重复编号,避免把创建误当成更新。
只有全部检查通过,程序才构造新的文档快照,并替换当前计划。第一次申请延到 13 点后,A1 结束时刻仍为 11,文档仍只有一版。第二次延到 12 点成功后,当前计划结束于 12,文档列表保存原来的 11 点版本和新的 12 点版本。
这里没有按小时重新计费,也没有违约金或客户确认流程。延期能否接受取决于资源约束,是本次实验的边界;金额和法律约定如果加入,需要定义额外规则。不能把一条成功的期间修改当成完整续租办理。
替换必须重新检查资源
另一个计划 A3 已占用 E2 的 [10,12)。把延期后的 A1 改到 E2 会被拒绝,因为候选设备在同一期间忙碌。E3 是投影仪,时间上即使空闲,也不能满足相机类别要求。两次失败后,A1 仍指向 E1,文档数量仍是两版。
E4 是空闲相机,替换可以通过。当前 A1 改为引用 E4,新文档记录 E4;早期两版文档仍引用 E1,不能跟随当前字段被改写。随后使用新的租赁编号 A4 为 E1 申请 [10,12),能够成功,这个外部操作证明旧资源的该段占用已经释放。
这种实现用当前计划集合判断占用,未维护第二份库存计数。因此替换的状态变化只需更新 A1 的资源引用。若改成独立库存服务,还必须协调旧资源释放和新资源取得;当前单进程结果不能证明跨服务切换不会丢失或重复承诺。
替换规则也保持克制:仅检查固定类别相同,没有计算镜头兼容性、客户偏好或新旧程度。E4 合格只是因为实验声明的条件已满足。增加任何新的替换条件,都需要一组能区分“同类但不适合”的输入,而不是继续增加颜色标签。
文档组件保留哪些历史
原章的蓝色模板与绿色文档都可以连接版本,文档活动另外处理生成、批准等业务发生。因此模板、文档和生成行为需要区分。出版社原章,§5.3.1–5.3.2,印刷页177–179
教学代码把 Template 保存为编号、修订号和标题;Document 保存租赁编号、文档修订号、模板引用、资源及期间。它不是 PDF 渲染器,实验也没有产生签章或法律文件。所保留的文档是足以对照计划的内存快照。
模板发布第二版以后,A1 后续修订继续使用签订时的第一版模板,新租赁 A4 则使用第二版。这是明确规定的教学政策。它让“修改旧约定”与“按新模板创建新约定”得到不同结果,不是蓝色分类天然保证旧版必须冻结。
文档修订列表也没有记录修改操作者、理由或登记时间。它可以证明这次运行保留了旧设备和旧期间,却不能回答“昨天系统知道什么”,更不能证明审计链不可篡改。若需要这类追溯,应增加相应事实并检验查询,不能仅把列表命名为 history。
两个候选在失败处的差别
flowchart TB
C["构造延期候选:结束13点"] --> GOOD["候选A:先检查占用"]
GOOD --> REJECT["冲突:计划11点,文档11点"]
C --> BAD["候选B:先追加文档13点"]
BAD --> CHECK["再检查占用,发现冲突"]
CHECK --> SPLIT["计划11点,最新文档13点"]
第二张图只比较同一输入下的操作顺序。候选 B 真正执行了追加文档,然后才调用相同的冲突检查。它抛出异常后,计划仍结束于 11,最新文档却结束于 13。测试读取这两个实际字段并检查不一致,没有把“应该出现半更新”写成静态说明。
候选 A 对已测试的业务校验异常保持两份状态一致,因为异常发生在修改之前。这个局部保证来自函数顺序,不来自粉色和绿色位于同一个图框。程序没有数据库事务或并发锁,两次字典写入之间若发生未预期故障,也没有恢复机制。
由此只能形成一条明确的一致性需求:被拒绝的延期不能留下成功文档。后续可以由数据库事务、草稿与发布状态或失败恢复协议满足它,具体选择取决于存储和服务边界。四色关系图不能独自决定哪个对象必须成为聚合根,也不能证明跨聚合更新已经安全。
并发是另一个尚未验证的方向。两个调用若都在写入前读到同一资源空闲,分别通过顺序检查,仍可能共同建立重叠计划。把校验提到函数前面没有消除这段竞争窗口;需要把检查和写入放进可保证互斥或冲突检测的机制,再用并发输入验证。本篇没有启动多线程,不能从这批顺序场景推导并发安全。
如果业务只需要展示当前结束时刻且没有历史文档,直接更新一条经过校验的计划记录会更小。这里增加文档快照,是因为变化卡明确要求检查签订内容及后续修订。需求撤销后,这一层也可以删除;“组件可复用”不构成永久保留它的理由。
执行与复查
从仓库根目录运行:
1 | |
本次运行通过 16 个行为场景,包含延期冲突与相邻边界、失败状态保持、占用和类别错误的替换、成功换机后旧资源再预留、文档及模板修订,以及文档先写造成的不一致。负例必须实际捕获 ValueError,状态比较不符或错误输入意外通过都会使进程失败。
实验只需 Python 3.10 以上标准库和 models/16/fixture.json,没有依赖其他章节或修改 Java 核心。类别和时点来自合成夹具,不代表行业制度或真实设备台账。重复请求回执、并发冲突、跨进程持久化与实际交付均未实现。





