软件建模38:从领域模型到应用架构——哪些决定还没有作出
明确设备身份、半开租期和取消历史以后,应用仍然可以采用不同的代码组织与部署方式。领域模型没有决定请求从哪里进入、状态存在哪里、消息失败由谁处理,也没有决定需要几个进程。
本篇以冻结的27-v1为参照,执行事务脚本、领域实现和模块门面三种候选,再通过一次真实的本机 HTTP 请求贯穿入口、用例、仓储和发布。实验只在单个 JVM 中运行,逻辑边界与部署边界分别说明。
三个候选保持哪一段行为
共同轨迹包含首次确认、重叠拒绝、相邻确认、取消、晚时钟下重放旧确认、重放旧失败、未知设备、提前量不足,以及复用请求号却改变设备。比较对象包括每一步结果、最终活动数量、完整回执以及历史报价,不能只比较第一条请求返回的字符串。
事务脚本候选把判断集中在 reserve 中:读取设备数据,检查状态与租期,扫描当前占用,计算报价,保存活动记录和回执。它仍复用标识、租期与报价值规则,因此不是故意把所有基础类型退回字符串;主要差别是业务流程的判断顺序集中在一个过程里。
领域候选直接调用27的 RentalApplication,再由真实的设备、Specification、Factory 和 ReservationCalendar 承担各自规则。模块候选在它外面增加一个用例入口,隐藏组装对象,并在传输边界把请求标识翻译为领域命令。它没有复制另一套预订规则。
Fowler 将 Transaction Script 描述为按请求组织业务过程,将 Domain Model 描述为同时包含数据与行为的对象模型。这是组织逻辑的两种模式,不能仅依据类的数量区分。Transaction Script、Domain Model。
三组实际轨迹一致,取消后只剩相邻那笔承诺,旧确认和旧拒绝均保持原结果。但这只是有限子集的等价:脚本候选固定维修放行,没有实现领域候选的全部维修故障、事务恢复和提交后发布行为。代码比较没有替它补齐未运行的契约。
脚本变短,也有当前样本固定的原因。它只读取预置设备集合,没有维护设备生命周期的用例。若增加维修限制与生命周期变化,就必须把这些输入接入脚本并扩展对照场景。因此,这次实验不能用文件长度推导长期维护成本,只能说明相同的一段行为有不止一种可运行组织方式。
模块边界与部署边界不同
模块门面承担请求翻译和调用方向控制,不因此获得独立进程、独立数据库或网络隔离。当前实现是一个 Java 类封装;没有 module-info、编译期模块可见性规则或跨团队发布协议,不能把类名 Module 当作已经实施了模块治理的证据。
flowchart LR
H[HTTP适配器] --> M[模块门面]
M --> A[RentalApplication]
A --> D[设备 规则 工厂 日历]
A --> R[Repository端口]
A --> X[MaintenancePort]
A --> P[Publisher]
R --> IM[MemoryRepository]
X --> LOCAL[本机固定放行函数]
P --> LOG[内存轨迹记录]
这张逻辑图帮助开发者定位变更:传输格式变化从适配器开始,租期规则变化进入领域规则,存储变化沿仓储端口检查。它没有说每个框都需要一个服务。把全部框部署到同一 JVM,依然可以保持这些调用关系。
代码组织还存在另一种混合:事务脚本可以位于清晰的模块边界内部,领域对象也可以被无边界的调用者随意绕过。因此“脚本还是领域对象”与“是否划分模块”是两条独立选择轴。实验中的第三个候选保留第二个候选的领域结构,只增加入口职责,正好显示两者可以组合。
一条真实请求经过哪些位置
Java 标准库 HttpServer 绑定本机回环地址和临时端口,HttpClient 通过网络栈发送 POST。请求体仅接受一个受限格式的请求号;设备、租期和费率固定为教学样本。这个接口用于观察调用链,不是完整的租赁 HTTP API。
首次请求的轨迹依次包含 HTTP 入口、模块翻译、应用调用、仓储开始、维修读取、仓储提交、确认事件发布和响应。第二次发送相同请求,仍返回 CONFIRMED,但没有再次读取维修,也没有再次发布确认事件。仓储中活动承诺仍然只有一笔。
无效请求号返回 HTTP 400,实验比较调用前后的领域状态,确认没有写入回执或占用。这个反例把格式校验的责任定位在传输翻译边界;它没有把技术格式错误伪装成业务租期拒绝。
有效请求的业务结果目前统一放在响应正文中,尚未制定完整状态码映射。不同业务拒绝是否对应相同状态码、客户端依据什么字段重试,需要成为接口合同的一部分;这些问题属于传输设计,不能从领域枚举名称直接推导。
sequenceDiagram
box 本机同一JVM
participant C as HttpClient
participant H as HTTP适配器
participant M as 模块门面
participant A as 应用与内存仓储
participant P as 内存Publisher
end
C->>H: POST请求号 经过127.0.0.1
H->>M: submit
M->>A: reserve 领域命令
A->>A: 规则检查与提交快照
A->>P: ReservationConfirmed
A-->>M: CONFIRMED
M-->>H: CONFIRMED
H-->>C: HTTP 200
这张图同时标注部署事实:客户端与服务端都在同一进程,维修函数和发布器也在进程内,仓储只是内存快照。只有客户端到服务器适配器这一段真实经过本机 HTTP;其余箭头都是方法调用。测试结束后监听器关闭,没有部署长期运行服务。
C4 的动态视图用于表达某个用例中元素的运行协作,部署视图说明实例落在哪个环境或节点上。两者解决的问题不同;本例使用 Mermaid 表达相应信息,没有把两张图宣称为完整 C4 文档。动态视图、部署视图。
哪些关注点需要另一种证据
业务负责人关心失败重试会不会重复占用,当前轨迹能回答选定场景。开发者关心哪些规则集中在哪个入口,源码和逻辑图能定位。运维人员关心重启后状态是否还在,内存仓储显然不能满足;这项问题不应被“模型已经正确”遮蔽。
调用者还关心提交成功却没有收到响应时怎样重试。27-v1保留完整命令回执,可以在同一仓储状态内重放,但进程退出后这些事实会消失。要维持同样语义,需要持久化回执、占用与草案,并重新验证原子提交边界,不能只把某一个 Map 换成数据库表。
发布责任更独立。轨迹证明本次运行先提交再调用发布器,没有证明消息一定送达。冻结基线的故障实验已经显示提交与发布之间存在缺口;HTTP 200的正常路径不能消除这个缺口。可靠发布需要另一项设计和故障验证,例如提交后恢复策略或事务性发件箱。
arc42 把构建块、运行场景、部署、架构决定和风险分别组织,提供的是文档结构。它能帮助把上述证据放在合适位置,不能通过填完模板替代故障实验。arc42 概览。
尚未作出的选择怎样记录
当前最直接的未决项是持久化与多实例并发。MemoryRepository 的锁只保护一个实例;启动第二个仓储或第二个进程,不会自动共享锁和回执空间。选数据库约束、乐观版本还是其他协调方式,应以同设备重叠与全局请求身份为验收条件,而非只比较吞吐数字。
维修端口现在固定返回放行,没有认证、超时、新鲜度或降级策略。发布器只把事件写入本机轨迹,没有外部消费者。HTTP 入口没有用户身份和授权、稳定错误协议或请求限额。这些都是当前实现范围之外的工作,不能因为端口类型已经存在就计为已交付。
对于仍需单进程运行的小型用例,领域实现加模块门面可以先保留现有结构。若团队边界、吞吐或可用性要求推动拆分,需要重新检查同步事务是否跨越部署边界。部署选择会改变故障方式,不能只把图里的两个框画到两台机器上而保持原来的正确性结论。
计划中的 E09 可继续用4+1与C4组织视图,E10处理架构描述与质量场景,E11讨论跨应用的企业架构范围;这些选修尚未作为本文实验交付。企业应用架构姊妹系列则应复用27-v1契约,分别替换持久化、通信和恢复机制,避免重复定义租赁语义。
运行与证据范围
设置 JDK 21 后运行:
1 | |
严格编译后执行十项断言,其中两项比较完整候选轨迹,其他检查固定结果、本机 HTTP、状态变化、重放、无效输入与提交发布顺序。机器必须允许程序临时监听127.0.0.1;不允许监听时实验会失败,不会跳过网络阶段后报告通过。
源代码与范围说明位于 examples/software-modeling/labs/38/ 和 models/38/,原始端口、调用链与退出码位于 evidence/38/。下载候选实现和真实HTTP实验。关于发布缺口与仓储合同,参见 27:应用基线;本章没有修改这份冻结源码。






