设备已经被预留,客户还没有来取。柜台需要知道能否继续交付,仓库需要知道接下来由谁处理,报价需要知道八天租期适用哪档单价。这三个问题发生在同一笔租赁中,却不能只靠一个“处理中”字段回答。字段混在一起以后,调价可能改动流程分支,补上超时又可能意外允许已经归还的设备再次交付。

UML 类图与交互图 讨论对象之间的关系与消息。本篇把观察范围缩到一台设备的一轮交付,用状态、流程和决策分别检查允许的变化、协作顺序与计算条件。示例仍是合成教学场景,增加的规则需要单独说明。

设备状态记录什么

系列中的 RentalDesk 已有指定设备预留、重叠拒绝、取消和原回执重放。它没有交付、验收或报价功能。本篇新增独立候选模型:设备可处于可用、待领取、已交付、待验收四种状态;归还后必须验收才能再次交付使用。这里的“必须验收”是教学假设,并非从既有代码或行业惯例推导出的事实。

状态描述当前设备在这个模型里的业务处境。事件表达发生了什么,迁移决定事件能否把设备带到另一个状态。可用设备收到预留事件后进入待领取;待领取设备在截止时刻之前才能交付。归还进入待验收,验收完成后恢复可用。只有待领取状态接受超时释放。

stateDiagram-v2
    [*] --> AVAILABLE
    AVAILABLE --> HELD: RESERVE
    HELD --> OUT: HANDOVER [now < deadline]
    HELD --> AVAILABLE: TIMEOUT [now >= deadline]
    OUT --> INSPECTION: RETURN
    INSPECTION --> AVAILABLE: INSPECT

图观察单台实物当前的一次处理,箭头上的方括号表示守卫条件。它采用 Mermaid 状态图语法,仅作示意,没有表达 UML 的完整状态机语义,也没有包含并发区域、事件池、历史伪状态等机制。UML 2.5.1 第14章区分状态、迁移、触发及守卫,适合用来核对这些概念;一张相似的图形不足以说明工具或代码符合规范。OMG UML 2.5.1,第14章

“可用”还有一个容易误解的范围:实物当前可以操作,不代表它在所有未来租期都没有预留。基线通过时间区间检查未来占用,本篇状态模型没有复制这项能力。把两者直接合并成一个 AVAILABLE 布尔值,会失去“今天空闲、明天已经预留”的情况。整合时至少要明确物理状态和时间上的可预留性分别来自哪里。

超时需要明确边界和触发来源

实验把截止时刻设为固定的 2026-10-03T10:00:00Z。交付条件是 now < deadline,超时释放条件是 now >= deadline。两者在端点上没有交叉:早一秒可交付但不能超时,恰好到点可超时但不能交付。时刻由调用方传入,因此可以稳定地重放端点情形。

代码采用状态枚举和一个迁移函数,未定义的“状态加事件”组合抛出 IllegalStateException。这个选择比公开 setState 更容易守住允许迁移的范围。一次迁移产生新的不可变记录,失败不会留下半更新的对象。它仍然只是内存中的顺序调用,并不保证两个线程同时读到待领取状态时只会有一个操作成功。

时间达到截止点也不会自动执行 Java 方法。实验显式提交 TIMEOUT 事件,检查接收事件时的守卫。真实运行需要调度来源、任务丢失恢复和重复触发策略;这些机制没有进入本实验。图中画出超时箭头,只说明在什么条件下接受释放,没有证明定时任务一定按时运行。

流程补充角色之间的顺序

同一租赁的流程可以描述成柜台报价、柜台预留、仓库交付。如果客户未领取,则走超时释放分支。流程观察的是这次办理如何推进,状态观察的是设备当前允许什么操作。报价是流程中的一个动作,却不一定改变设备状态;归还后的验收则可以由另一轮流程完成。

flowchart LR
    A[柜台:报价] --> B[柜台:预留]
    B --> C{领取条件}
    C -->|截止前领取| D[仓库:交付]
    C -->|达到截止且未领取| E[超时事件:释放]

这是一张带角色标签的 Mermaid 流程示意图,不是 BPMN 文件。BPMN 2.0.2 §8.4.13 的 Sequence Flow 表达流程元素的先后关系。正式 BPMN 建模还需要遵守它的元素、连接与执行语义;图中普通箭头、菱形和角色文字不能直接当作规范里的事件、网关或泳道。OMG BPMN 2.0.2,§8.4.13

实验用同步函数执行两个分支,并断言整条轨迹。领取分支必须出现报价、占位、仓库交付,结束于 OUT;超时分支出现报价、占位、释放,结束于 AVAILABLE。这里的柜台与仓库只是轨迹标签,没有人员认证、消息交互或流程引擎。函数把等待压缩为一个输入条件,也没有模拟真实等待经过的时间。

若只需这两个分支,普通函数已经足够清楚。引入 BPMN 工具之前,应有更具体的需求,例如让业务人员审核跨角色长流程,或者由引擎管理等待与恢复。否则需要额外维护图文件、部署版本和引擎配置,却未必增加当前两个分支的可检验内容。

决策表说明条件与结果

报价新增另一组教学假设:输入是已经确定的整数计费天数,范围为一至三十天;一至七天每天一百元,八至三十天每天八十元;适用单价乘以全部天数。币种固定为人民币,没有押金、税费或阶梯分段累加。计费天数如何从租期得到,尚未由本篇定义。

规则 输入:计费天数 输出:人民币元/天
R1 1 至 7,含端点 100
R2 8 至 30,含端点 80

八天命中 R2,结果为八乘八十,即六百四十元。这里并不是前七天一百元、第八天八十元的累计算法;后一种解释会得到不同金额,需要另立规则。表格的输出单位同样属于模型,不写“元/天”就容易把单价误用成总价。

DMN 1.5 §8.2.11.1 定义了命中策略。Unique 要求规则互不重叠,对一个输入至多匹配一条;它本身没有保证所有输入都被覆盖。First 则取按规则顺序遇到的第一条匹配结果,顺序因此属于含义的一部分。本例选择“不重叠且覆盖一至三十的所有整数”作为额外教学约束,代码据此要求恰好命中一行。OMG DMN 1.5,§8.2.11.1

实验只使用闭合整数区间、一个整数输入、一个单价输出和 Java 条件判断。没有解析 FEEL,没有导入 DMN XML,也没有运行 DMN 引擎。表格帮助讨论决策语义,但不能因此声称实现了 DMN 一致性或兼容性。

让错误模型产生可见反例

源文件位于 labs/06/BehaviorCheck.java,模型约定位于 models/06/semantics.md。在仓库根目录、Java 21 的 java 与 javac 可用时运行:

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

脚本也支持通过 JAVA_HOME 选择 JDK。它在临时目录编译,退出时清理编译产物,不需要 Maven、网络服务或额外依赖。完整材料可下载 第06篇实验包,解压后在包含 examples 的目录运行同一命令。

第一次负例把第二行起点从八改成七。输入七天时命中两行,输出记录 overlap_day_7 rejected=matches=2 days=7。若用普通的“命中第一条就返回”,这个错误不会暴露;程序还实际交换两行顺序,确认七天的输出从一百变为八十。

第二次负例把第二行起点改成九。输入八天时没有匹配,输出 gap_day_8 rejected=matches=0 days=8。仅检查行与行之间不重叠会漏掉这个问题。因此实验遍历声明域的一至三十天,逐一检查命中数和单价;零天与三十一天在域外,明确拒绝,不用默认价格掩盖未定义情况。

报价决策失败时,流程函数会在预留之前停止,避免先占位再发现天数没有定义。若产品希望将域外天数转交人工报价,应增加明确的决策结果和流程分支,不能让“没有匹配”自动变成零元。输入域扩展到小数天数时,逐个枚举整数的覆盖检查也需要重新设计。

状态负例另外检查未经预留直接交付、待验收时再次预留、提前超时和到点交付。负例必须抛出指定类别的异常,意外接受会使进程失败。正常循环、端点释放和两个流程分支也要通过,避免实现只会拒绝全部操作却被当成正确。

本次在 Amazon Corretto 21.0.11 上实际执行,十四项检查通过,进程退出码为零。原始输出、环境与命令保存在 examples/software-modeling/evidence/06/。它证明这些给定场景及有限输入域内的行为;没有证明分布式交付、定时器可靠性或任意规则表达式的完整性。

规则变化应改动哪些材料

如果七天优惠改成六天,决策表、Java 区间以及边界断言需要一起变化,设备状态不应跟着增加一个“优惠待领取”。如果归还后允许免检,迁移模型与流程路径才会变化。若业务提出迟到五分钟仍可领取,则必须重新明确交付与超时的竞争关系,不能只在流程图旁添一句说明。

把状态、流程和决策分别记录后,评审可以直接定位变化对象。一个普通函数加枚举仍可承载全部实现;有明确审阅或运行需求时,再引入模型文件和专门工具。当前最需要保留的是端点、异常与输入域的对应关系,使修改后的图和程序仍然回答同一个问题。