参与方 P1 在 10 点开始代表组织 O1 办理设备租赁,12 点又承担 O1 的验收职责,15 点结束租赁经办任职。查询 14 点时,P1 有两项职责;查询 15 点时,应只剩验收职责。P1 同时为另一组织 O2 办理租赁,这项关系也不能随 O1 的任职终止而消失。

这组新增教学场景沿用设备租赁案例,但不属于现有 RentalDesk 的能力。Java 基线尚未接收操作者身份。这里用独立实验检查个人、组织和带期间的责任关系,检验继承、角色字段与关系记录分别能回答什么。

参与方身份与业务角色

Fowler 的 Accountability 补充材料第 5—6 页把 Party 用于个人与组织的共同抽象,并说明共同部分值得提取时才采用。组织可以是业务组织单位,不能把 Party 自动解释为具有法律资格的法人。材料也提醒,参与方的角色并不意味着必须采用 Role Object。Party 原始说明,第 5—6 页

本篇采用 Party(id, kind),kind 只允许 person 与 organization。P1 与 O1 的区别由种类表达;“租赁经办”和“验收”则依赖某一组织及一段时间,不写进 P1 的永久种类。如果任职结束就把 P1 换成另一种人,历史单据引用哪个身份会变得难以解释。

个人和组织也不是本系统中全部对象的总父类。设备 E1 不属于这里的 Party,预留请求 A 也没有必要继承 Party。共同抽象应服务于参与方之间的关系与查询,不能只因为几个对象都有编号,就让它们共享业务身份体系。

实验根据 id 比较身份,名称变化没有被实现。Python 的数据记录相等仍比较完整字段,不能据此推导“字段相同就是同一个参与方”。若后续增加名称、证件或地址,身份连续性的判断仍应使用稳定标识;跨系统合并重复身份则需要额外规则。

三种候选放到同一项变化里

把 Renter 和 Inspector 定义成 Person 的互斥子类,能够表达简单分类,却很难承载同一个人同时担任两职的场景。再增加 RenterInspector 会把角色组合写成新的永久类型,组合改变时还要决定对象是否换身份。多重继承也没有自动给出角色的起止时间。

一个较小的替代方案是给个人增加角色集合。在系统只关心“当前具有什么角色”且角色没有组织范围时,这已经可以回答多角色问题。但本例必须区分 O1 与 O2,还要追溯 14 点的职责。集合里只存 renter 无法说明它是谁赋予的、在哪个范围有效、何时终止。

因此实验使用带类型的双参与方关系。principal 是赋予职责的组织,agent 是承担职责的个人,role 是该关系的性质。这个方向是案例自己的约定,并不是所有组织模型都必须采用的命名。关系还保存独立编号和有效期间,使任职变化能够独立于个人身份被记录。

Fowler 的补充材料第 17—18 页将 Accountability 表达为两个 Party 之间的有类型关系,用于多条组织关系不足以由简单层级表达的情况。本例把这种关系结构用于经办和验收责任,是教学迁移;并不因此取得法律责任划分或真实授权规则。Accountability 原始说明,第 17—18 页

期间改变的是关系

教学记录的结构如下,图只表达本章的有效时间关系,不包括登录账户、授权令牌、合同和设备库存。

flowchart LR
    O["O1:组织 Party"] -->|principal| R["R1:责任关系"]
    R -->|agent| P["P1:个人 Party"]
    R --> K["role = renter"]
    R --> T["有效期 [10,20),15 点终止"]

R1 的原定有效期为 [10,20)。终止发生在 15 点后,查询使用的实际结束时刻是 15;10 点已有效,15 点不再有效。这个半开约定和租期表达一致,但两者是不同的业务期间。任职终止不自动取消预留,设备占用释放仍应由租赁用例定义。

代码将终止后的记录构造成新值,保留原定结束时刻和 ended_at。列表中更新的是 R1 的当前版本,记录依然存在。按业务时点 14 查询,仍可看到租赁经办职责;按 15 查询则排除它。这种“历史”只表示业务有效时间,没有保存系统在不同登记时刻知道什么。

例如,若 18 点才补录“15 点已经结束”,当前实验可以回答 16 点是否有效,却不能复原 17 点时系统曾显示什么。后一个问题需要登记时间与版本链。给字段取名为 history 不会补出未存下来的版本,本章也不宣称完整双时态。

终止同一关系两次还需要确定重试契约。实验接受相同终止时点的重复操作,返回同一个值;第二次提交不同终止时点则拒绝。这个策略来自教学设定,方便暴露冲突,不是原书对全部责任关系的规定。真实纠错流程可能允许修订,但必须保留足以解释改动的依据。

多重责任必须带着查询范围

固定样本含三条关系:R1 是 O1 的租赁经办,R2 是 O1 的验收,R3 是 O2 的租赁经办。P1 在 12 点对 O1 的查询返回两项角色。R1 在 15 点终止以后,对 O1 查询只返回验收,对 O2 查询仍返回租赁经办。

这里的查询条件包含个人、组织和业务时点。若忽略组织,只问 P1 是不是租赁经办,R3 会使答案继续为真;拿这个答案去允许 P1 代表 O1 操作,就扩大了职责范围。实验检查组织隔离,但没有接入访问控制系统,查询结果也不能被当成一张真实权限凭证。

验收是否必须由经办以外的人承担,同样属于待确认业务规则。本章允许同一人兼任,是为了检验多角色;若真实制度要求职责分离,需要在同组织、同业务单据或重叠期间范围内增加禁止条件。不能看到类图有两个角色,就声称系统已经实现相互独立的复核。

责任记录与某次实际行为还需要区分。P1 在 14 点有经办资格,不等于 P1 在该时点真的提交过请求 A。若需要追查谁完成了操作,必须由操作记录引用参与方和相关责任关系,并保留实际发生时点。用任职列表反推全部历史操作者,会把“当时可以做”误当成“当时确实做过”。

反过来,操作曾经完成也不能证明今天仍有任职。历史单据可以继续指向 P1,而新的请求必须按新的时点核对范围。这两个查询可共享稳定身份,却有不同的时间条件。当前测试只验证任职关系,没有生成带操作者的请求回执。

多条责任关系也不一定构成树。当前限定组织指向个人,未提供任意参与方之间的递归层级,因此不需要在这个闭合样本中实现组织环路检测。若以后允许组织委托组织,再用当前函数处理所有关系,会漏掉方向、循环及权限传递的新规则。

实际执行终止与反例

从仓库根目录运行:

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

本次输出包含 13 个通过的场景,以 PASS 09: 13 scenarios 结束。每个正常场景输出实际结果与期望值;非法输入场景必须捕获到 ValueError 才会标记通过。只打印一句“应该失败”不会满足检查。

场景 固定输入 二值判据
起点与多角色 9、10、12 点查询 O1 空、经办、两职依次匹配
终止与历史 R1 在 15 点终止 14 点保留,15 点排除
组织隔离 同一时点查询 O2 R3 仍有效
重复及冲突终止 再终止于 15、16 点 同值接受,不同值拒绝
删除的反例 从列表移除 R1 再查 14 点 经办历史消失

另有空期间、参与方方向颠倒和开始前终止三个拒绝场景。方向颠倒时,构造器收到个人作为 principal、组织作为 agent,必须拒绝。这证明本例明确规定的配对规则被执行,不证明所有合法企业委托都符合这条方向约束。

删除反例与终止实现使用同一查询。删除 R1 后,14 点查询只返回验收;这是对丢失历史的主动检查,预期就是少一项。终止保留的记录与删除形成可运行对照,避免把“保留历史”仅写成模型说明。

与租赁基线的连接仍需用例

03 的候选 ER 模型允许回执关联参与方,当前 Java 核心却没有对应字段。要把本章关系用于真实预留,至少还需决定由哪次请求引用哪个任职、预留之后任职结束是否影响合同、取消是否可由继任者发起。职责查询通过并未回答这些问题。

当前实现也没有全局编号注册、重复任职去重、数据库持久化或并发更新保护。R1、R2、R3 的唯一性由固定测试夹具保证,不能推断生产入口会阻止重复编号。代码保持为独立的 Python 标准库实验,只让已经写出的规则具有可检查行为。

原始命令、环境、输出与退出码位于 examples/software-modeling/evidence/09/。下载本章模型、实验与证据 保留仓库目录前缀;解压根目录运行上述命令即可,不依赖 Java 核心或其他章节。

模式采用条件见 软件建模08:分析模式的复用;参与方与回执的候选数据关系见 软件建模03:ER 与数据建模。