软件建模00:模型帮助作出什么决定
软件建模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 | |
对 [10, 20) 和 [20, 25),第二个条件不成立,所以相邻可接受。对 [10, 20) 和 [12, 18),两个条件都成立,包含关系也会拒绝。只比较两个开始时刻,或者把端点相等直接算作重叠,都不能满足当前规则。
代码保留的是可复核契约。用另一种语言或普通表函数实现,只要相同场景得到相同结果,也可以作为候选。对象数量、类图大小和接口层数不属于这次验收的指标。
在仓库根目录运行,环境变量中的路径替换为本机 Java 21 安装目录:
1 | |
实际基线使用 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 分别按各自的关注点讨论。企业应用架构姊妹系列继续研究持久化、事务、消息、补偿、隔离与迁移,不要求这些机制都在初始租赁程序中出现。
判断速查
| 出现的疑问 | 先检查什么 | 当前边界 |
|---|---|---|
| 取消以后为什么重试又成功 | 回执和当前占用是否混用 | 只验证实例内重放 |
| 接上一笔结束时刻能否租 | 区间约定与交接规则 | 清洁耗时未实现 |
| 图已经画完为什么仍有并发冲突 | 检查和更新的一致性保证 | 顺序基线不承诺并发 |
| 模型增加了很多类是否更好 | 场景与变化卡能否检验决定 | 类数不能证明规则正确 |
教学附件与参考资料
下一篇:场景、用例与规则- 本篇三种模型草案与变化卡:具体场景和遗漏。
- Java 基线与检查程序:包含
RentalDesk.java、48 项检查、运行入口及实际实验记录。 - Fowler:Domain Model:PoEAA 对象式领域模型定义;未将该定义扩展为所有模型的定义。
- Java SE 21:Instant:基线的时刻类型。区间、取消和请求重放规则属于教学案例假设。






