软件建模00:模型帮助作出什么决定

设备租赁系统收到两笔订单:一笔租相机 E1,从 10 点到 12 点;另一笔也租 E1,从 11 点到 13 点。登记两条记录很容易,决定能否接受第二笔才是业务问题。若第二笔从 12 点开始,还要确定上一笔的结束时刻是否占用资源,以及设备归还后是否需要清洁。

这些问题会改变字段、流程和代码。模型的用途,就是把影响决定的事实和约束表达出来,使场景能够被检查。一个模型可以很小:一张表、一段流程、一组概念关系,或者一段可执行规则。维护哪些表达,取决于哪些决定需要作出。

本系列使用合成的设备租赁案例,把分析模式、四色建模、FDD、DDD 和模型驱动开发放进同一条问题线。它们各自处理的问题、产物与验证方式会保留,比较时使用相同场景和变化卡。领域规则复杂到需要边界、存储和集成机制时,再连接独立的企业应用架构系列。

当前问题 需要留下的表达 检查方式
同一设备能否接受两笔预留 租期、设备标识、重叠约束 相邻与重叠区间反例
同一个请求再次到达怎么办 请求标识、载荷、处理回执 重试与新请求对照
取消后还保留哪些事实 当前预留和原请求结果的区别 取消后重放检查
哪种模型值得维护 模型能回答的问题及其遗漏 场景走查与变化卡

一个范围明确的租赁问题

案例包含设备、租赁、维修、报价与结算,但初始实验只实现指定设备的预留和取消。设备型号库存、押金、付款、配送以及维修资格暂不进入代码。这使每条已实现的规则都能找到输入和观察结果。

当前规则是:租期使用 UTC 时刻,采用包含起点、排除终点的区间 [start, end),并要求 start < end。同一设备的有效预留不能重叠,不同设备分别判断。相邻租期允许预留,取消释放所占区间。

“相邻可接受”是教学业务假设。Java 提供时间类型,却不会替租赁业务决定交接耗时。若真实场景要求清洁半小时,就需要表达合同租期和资源占用期之间的关系,不能把 API 的区间习惯当作业务事实。

重复请求还有一条独立规则:同一个请求标识只代表一次意图。同标识、同载荷的重试返回最初结果;同标识、不同载荷拒绝。一次已拒绝的请求不会因为设备后来空闲而自动变成成功,新尝试必须使用新请求标识。

取消也不会改写原请求结果。取消释放预留,随后重放原请求仍返回原确认,但不会重新占用设备。因此“这次请求曾被确认”和“设备现在仍被预留”是两个问题。把二者放在同一个可变状态字段里,会使重试的含义含混。

表模型:能登记什么,尚不能保证什么

一种直接的草案是三张概念表:

表 主要信息 用途
equipment 设备标识 区分具体资源
reservation 请求标识、设备标识、起止时刻 保存当前有效预留
receipt 请求标识、原载荷、原结果 保持重试结果稳定

这里的表是信息结构草案,实验还没有数据库。它说明查询需要哪些数据,也暴露了取消的影响:删除有效预留后,请求回执仍须保留,否则原请求再次到达就可能重新预留设备。

如果只保留一张预留表,业务也可能实现,但必须额外区分已取消、已拒绝和有效预留,并保证冲突查询只考虑有效占用。三张表没有天然更好;它把两个生命周期分开,便于当前小案例验证。

表模型尚未说明重叠的计算规则,也没有说明检查与写入如何共同完成。即使给请求标识加唯一约束,两条不同请求仍可能同时检查到“没有冲突”。数据库表设计需要在持久化篇中继续回答事务与并发问题。

分开当前状态与请求历史

可以迁移的表达是 当前占用 = 生效后尚未释放的承诺,而 请求回执 = 某次意图的原结果。两个集合根据不同事件更新,重试先读回执,新意图再检查当前占用。

场景 当前状态 需要保留的历史
设备预留 当前有效租期 预留请求的原结果
座位预订 当前被占座位 某次预订是否成功
额度申请 当前已承诺额度 某次申请的批准或拒绝结果

这些场景共享“资源可以释放,处理结果仍需可查”的关系。真实系统还要规定回执保留时长、作用域及失效后的重试协议;本实验只在一个内存实例的存活期间保留回执。

流程模型:一次请求走哪条分支

预留过程先验证身份和区间,再查请求回执。已有回执时比较载荷,返回原结果或拒绝载荷冲突。首次到达时检查当前预留,记录确认或拒绝结果;只有确认才加入有效预留。

flowchart TD
    A["请求:设备、租期、请求标识"] --> B["校验输入"]
    B --> C{"请求回执已存在?"}
    C -->|是| D{"载荷相同?"}
    D -->|是| E["返回原结果"]
    D -->|否| F["拒绝载荷冲突"]
    C -->|否| G{"同设备有重叠预留?"}
    G -->|是| H["记录拒绝回执"]
    G -->|否| I["记录确认回执与有效预留"]

这是一次顺序调用的流程示意,使用 Mermaid 表达分支。它没有 BPMN 的完整记法,也没有表示跨进程投递、并发执行和故障恢复。

流程能帮助发现顺序错误。如果先查资源再查回执,同一请求的结果就可能随资源状态变化。如果确认后只登记预留而不登记回执,重试可能误判自己造成的占用为冲突。把分支画出以后,这两种错误可以转成检查。

流程图还不能回答所有问题:取消与预留同时执行怎么办,写回执成功而写预留失败怎么办,服务重启后怎样恢复?这些问题需要一致性边界和存储机制;顺序箭头无法提供相应保证。

概念模型:同一个词是否承担了两种含义

“订单”容易同时指申请、确认结果、合同和实际占用。在这个初始问题里,先拆出请求、回执和有效预留,比直接建立一个带几十个字段的 Order 类更容易讨论取消和重试。

设备标识回答“占用的是哪一件资源”;租期回答“哪些时刻被占用”;请求标识回答“是不是同一次意图”;回执回答“当时得到什么结果”。有效预留把资源与时间联系起来,参与冲突检查。

flowchart LR
    R["请求:标识、设备、租期"] --> P["原结果回执"]
    R -->|首次确认| B["有效预留"]
    B --> E["设备标识"]
    B --> T["租期"]
    C["取消"] -->|释放| B
    C -. 不删除 .-> P

图中的箭头表达概念联系与处理影响,没有声明数据库外键、UML 多重性或 DDD 聚合边界。回执和预留在代码中都是 Map 的条目,也不意味着它们已经成为独立的领域实体。

Fowler 的 PoEAA 将 Domain Model 描述为同时包含数据和行为的对象模型。这个定义可用于后续的实现候选,但表、流程和约束等其他表达仍可以作为建模工件存在。是否选择对象式 Domain Model,要根据规则的交互复杂度讨论。Domain Model 模式说明

概念模型的收益在于区分容易混用的事实。它尚未决定哪些规则放在对象方法,哪些数据合并为事务边界,以及哪些模块需要独立部署。这些决定分别在 CRC、DDD 与架构篇继续展开。

让规则进入一个可运行的基线

基线位于 examples/software-modeling/,使用 Java 21 标准库。它只有内存中的回执与预留,尚未引入框架、仓储接口或消息系统。RentalDesk.reserve 负责一次顺序处理,cancel 释放指定请求的预留。

完整程序包括输入校验与检查入口。租期的重叠条件如下;片段来自实际实现,完整代码由文末教学附件提供:

1
2
3
4
public boolean overlaps(Period other) {
return start.isBefore(other.end)
&& other.start.isBefore(end);
}

对 [10, 20) 和 [20, 25),第二个条件不成立,所以相邻可接受。对 [10, 20) 和 [12, 18),两个条件都成立,包含关系也会拒绝。只比较两个开始时刻,或者把端点相等直接算作重叠,都不能满足当前规则。

代码保留的是可复核契约。用另一种语言或普通表函数实现,只要相同场景得到相同结果,也可以作为候选。对象数量、类图大小和接口层数不属于这次验收的指标。

在仓库根目录运行,环境变量中的路径替换为本机 Java 21 安装目录:

1
JAVA_HOME=/path/to/jdk-21 bash examples/software-modeling/run-lab.sh 00

实际基线使用 Amazon Corretto 21.0.11 编译运行,48 项显式检查通过,进程退出码为 0。另一轮使用 Java 8,入口退出码为 2,并提示要求 JDK 21。检查函数主动抛出异常,不依赖 JVM 是否启用语言级 assert。

实际检查 观察结果 能支持的结论
零长度、逆序、空字段 拒绝输入 已实现的构造约束成立
相邻、分离、部分重叠、包含和相同租期 按规则确认或拒绝 区间比较覆盖这些具体形态
不同设备相同租期 确认 冲突范围限定为同一设备
同请求同载荷、同标识改载荷 原结果或异常 实例内重试有稳定含义
取消后重放、拒绝后资源释放 保留原结果,不重新占用 回执与有效预留生命周期分离

这些结果只证明本地、单线程、内存调用下的检查。没有并发预留实验,也没有持久化、崩溃恢复或跨服务消息验证。后续增加这些机制时,需要新的失败场景及独立证据。

三种模型如何接受同一张变化卡

第一张变化卡要求归还后清洁半小时。表模型需要增加或计算占用结束时刻;流程模型需要把资格和资源占用计算放进接受预留之前;概念模型需要区分合同租期与占用窗口。若只给重叠公式加半小时,报价和合同也容易误用延长后的区间。

第二张变化卡允许同型号任一设备履约。设备实例的标识仍然有用,但请求不再必然指定实例。候选模型需要区分型号容量、分配和具体设备占用,当前基线不能仅靠换一个字段就证明正确。

第三张变化卡要求追溯改价,同时保留曾经向客户展示的金额。预留表和回执没有表达价格版本,流程也没有报价步骤。模型需要新增事实和时间语义,不能从当前实验推断已经支持历史结算。

三张卡目前只做工件走查,没有对应的运行实现。它们让比较具有具体尺度:候选模型能否指出需要改变什么,原有场景是否仍成立,哪些新约束尚未表达。

按决定选择表达

选择可以写成 需要维护的工件 = 当前决定所需事实 + 约束 + 可检查场景。同一事实可以进入不同表达,但每种工件应有明确用途。

需要作出的决定 优先检查 不足时追加
哪些数据必须保留 表与生命周期 约束、历史与状态
一次调用如何分支 流程与回执 失败恢复和并发模型
同一个术语是否有多种含义 概念及反例 上下文与责任分配

本例的三种草案共同解释了同一个小用例。实际项目不必为每个问题都维护三套图;某个表达长期没有支持任何决定,就需要检查它是否仍有维护价值。

系列中的方法如何落位

基础篇 01 至 07 使用场景、数据流、ER、CRC、UML、状态和时间约束建立检查方法。08 至 14 从分析模式复用领域知识;15 至 18 研究四色 archetype 与 FDD 的建模及增量交付过程。19 至 28 用 DDD 处理语言、上下文和一致性边界。

29 至 34 分开讨论 Evans 的 Model-Driven Design 与模型驱动开发中的元模型、校验、解释及生成。35 至 39 用函数式建模、遗留改造和三张变化卡比较这些候选,检查模型与实现如何共同变化。选修覆盖事实建模、维度建模、形式化方法、产品线、REA 和可执行流程等问题。

4+1、C4、ISO 42010、Views and Beyond 与 arc42 在 E09 至 E10 处理架构表达;TOGAF、ArchiMate、Zachman 在 E11 连接企业层面的目标与迁移。4+1 与 TOGAF 分别按各自的关注点讨论。企业应用架构姊妹系列继续研究持久化、事务、消息、补偿、隔离与迁移,不要求这些机制都在初始租赁程序中出现。

判断速查

出现的疑问 先检查什么 当前边界
取消以后为什么重试又成功 回执和当前占用是否混用 只验证实例内重放
接上一笔结束时刻能否租 区间约定与交接规则 清洁耗时未实现
图已经画完为什么仍有并发冲突 检查和更新的一致性保证 顺序基线不承诺并发
模型增加了很多类是否更好 场景与变化卡能否检验决定 类数不能证明规则正确

教学附件与参考资料

下一篇:场景、用例与规则