企业应用架构00:模式地图与租赁基线
两套租赁程序都返回“预留成功”,不表示它们做了同一件事。一套可能保留重复请求回执,另一套可能重新占用;一套把取消解释为释放设备,另一套可能顺便删除报价。比较架构之前,需要先确定这些结果究竟包含哪些业务保证。
本系列从 软件建模27的累计应用 导入固定源码,重跑原有契约,再与一个直接过程比较。基线先回答设备、金额、租期、维修限制和请求历史的含义;后续章节才逐项改变代码组织、持久化和系统集成方式。
模式地图按问题组织
Fowler 的 PoEAA 目录将领域逻辑、数据源访问、对象与关系数据之间的行为、展示和会话等问题分别组织。目录是模式摘要入口,并没有要求每个企业应用把所有模式都装进去。原作者模式目录
租赁程序可以先用一个过程完成规则判断,也可以把行为分到设备、日历和合同对象。把对象保存到数据库,又会产生身份映射、事务和加载问题;把成功结果发给维修系统,则需要面对消息、重试和恢复。它们沿着同一条业务路径出现,却不属于同一种选择。
flowchart LR
B[业务语义与场景] --> L[领域逻辑如何组织]
L --> U[应用用例边界]
U --> D[对象如何保存和并发修改]
U --> I[接口与消息如何跨边界]
D --> F[失败后保留和恢复什么]
I --> F
F --> V[用原场景重新验收]
这张图回答问题的依赖关系,省略部署和运行时组件。画出数据库或消息节点不能证明它们已经存在。本章只有 Java 内存实验,后续涉及 SQL、进程重启和消息运行时的保证,需要各自运行对应环境,不能沿用这里的通过数字。
固定一个已经运行的上游
本次导入来源提交为 ed5a420a0189945118b8b0211f6da39e989a453d,语义标识为27-v1。副本位于新工程的 rental-core/frozen-27,保留上游相对目录;它包含旧核心、设备和值对象、预留日历、报价与工厂、累计应用、原检查程序及契约文件。
导入脚本没有仅凭文件名复制。它先读取指定提交中的 Git 内容,与上游工作文件逐字节比较,再写入副本并计算 SHA-256。十九个文件的来源路径、副本路径和散列保存在 models/00/baseline-manifest.json,导入输出另外保存。
读者运行时不需要上游 checkout。校验程序只读取本包清单和副本,检查内容是否与冻结散列一致;原仓库的导入脚本则用于维护者重建来源链。这两个入口的责任不同,不能要求下载实验的人拥有相同本机目录或 Git 历史。
后续软件建模系列即使继续调整模型,本工程也不会自动跟随。升级应产生新的来源清单,列出契约差异,并在新旧两侧回归。否则一次价格规则变化可能被误算成某种架构模式带来的正确性提升。
哪些规则已经确定
设备编号沿用非空白值类型,实体按稳定编号相等。设备须处于 ACTIVE 才能接受新预留;维修评估必须为 RELEASED,但它只解除维修侧限制。已经退役的设备不能因为维修完成就自动恢复出租资格。
金额支持人民币和美元,精确保留两位小数,不自动舍入或换汇。租期是非空的 Instant 半开区间,相邻窗口不重叠;教学应用要求至少提前一小时,最长四十八小时。报价沿用按时长向上取整到完整小时的规则。
这些值来自已执行的源码和断言,不是计划阶段擅自填入的行业政策。维修端口接收可信翻译结果,没有真实网络调用和新鲜度保证;金额可以表示负值,也不代表已经发生退款。语义与外部业务事实必须分开记录。
价格有效期是一个明确的空缺。上游保存当次费率、修订号和草案总价,但没有一张按有效起止时间查价的表。后续分段计费实验若增加这种结构,应宣布新规则与新的对照范围,不能说它从一开始就在冻结基线里。
合同同样只有不可变草案,没有签署、交付、履约完成的完整状态机。设备状态迁移不能代替合同状态迁移。取消释放活跃承诺,保留原草案与历史回执;退款尚未实现。这些未决项与已确认规则一起冻结,避免读者从类名推导不存在的行为。
历史回执是比较的一部分
RequestId 在一个仓储状态内覆盖所有设备。相同编号和完整负载返回历史结果;更换设备、租期、费率或修订号会被拒绝。当前时间和维修评估属于环境输入,重放历史时不会重新用它们改变过去结果。
因此取消成功以后,旧成功请求重放仍返回确认,却不会再次占用。一个因冲突失败的请求,在冲突释放后重放,仍返回旧拒绝;换一个编号才代表新的预留意图。只比较第一次方法返回值,会遗漏这两条重要的持续行为。
实验按同一顺序调用冻结应用和一个直接过程:预留、重叠请求、重试、取消、取消后回放、旧拒绝回放、新编号再次预留,最后比较活跃数。报价通过真实工厂结果和定价计算核对,维修限制与币种错误也分别进入相应行为。
sequenceDiagram
participant T as 场景检查
participant A as 冻结应用
participant S as 直接过程
T->>A: 同一设备与租期请求
T->>S: 相同输入
A-->>T: 回执与状态
S-->>T: 回执与状态
T->>T: 比较并继续取消和重放
T->>S: 故意关闭重叠检查
S-->>T: 错误确认
T->>T: 断言失败,进程非零退出
直接过程只覆盖固定为 ACTIVE 的 E1 和这组共同场景,没有实现完整设备生命周期。它的价值是提供一个可读的小对照,并非宣称已经复制上游全部行为。更广的生命周期、快照与故障检查,仍由原始程序原封不动复跑。
让错误能够被检查发现
正常模式下,本章十二项对照通过;同时复跑旧核心四十八项及累计应用三十六项。每项本章断言带 E00 场景编号,models/00/contracts.md 将编号映射到规则、调用路径和预期结果,输出不是一张没有来源的通过清单。
故意违规模式会让直接过程跳过占用判断。同一个重叠请求,冻结应用返回 OVERLAP_REJECTED,变异过程却返回 CONFIRMED;E00-02 比较立即抛出断言错误,进程退出一。失败来自业务差异,不是缺文件、编译错误或依赖未下载。
混币反例也执行真实对象行为。人民币草案收到美元金额后,covers 抛出异常;实验没有只比较两个货币代码然后打印“已拒绝”。错误样本必须走过被声称验证的边界,日志才有明确含义。
副本散列检查与业务断言解决不同问题。前者发现源文件被改动,后者发现某种运行行为不符合契约;散列一致不能替代业务执行,业务通过也不能说明复制来源正确。二者同时成立,才形成可比较的起点。
用例结束后还要检查保存内容。两个实现都返回确认,其中一个若只写回执、没有保留活跃占用,下一个请求就可能重复出售窗口。反过来,只存占用而不存历史,重试时又无法辨别旧意图。测试顺序把这些遗漏转换为后续可观察差异。
小基线保留哪些限制
当前仓储只在单个实例中恢复工作副本并一次替换快照。提交前异常可以丢弃候选变更,提交后发布异常却不能撤销已保存结果;旧请求回放不会自动补发丢失事件。这些缺口已经由上游实验显示,导入没有顺便修好它们。
没有数据库,就没有跨连接回滚或隔离级别的证据;没有消息运行时,就没有离线消费者恢复的证据。Evans 的仓储摘要关注领域对象的访问与存储隔离,具体运行保证仍取决于实现。DDD Reference,印刷页17
本系列会把这些技术边界拆开验证。选择模式时先提出一个可以观察的差异,例如换连接能否读回、重复消息是否增加金额,再决定要引入什么机制。不能用包层数、接口数或模式名数量代替业务保证。
同题比较也不要求每种方案长成同样的类图。直接过程可以使用集合,领域模型可以通过对象方法保持约束,只要比较范围内的输入与结果相同即可。若方案需要放宽某条规则,先把它标为规则变更,再讨论换来的收益,避免把少做检查误当成设计更简洁。
运行固定契约
使用 JDK 21,将 JAVA_HOME 指向安装目录,在仓库或下载包根目录运行:
1 | |
使用同一入口加 --mutant 可观察预期失败;all 运行当前已实现章节。实际采用 Corretto 21.0.11,以 --release 21 -Xlint:all -Werror 编译,没有第三方 Java 依赖。环境、输入位置、命令、原始输出和退出码在 evidence/00。
下载 第00篇固定基线与实验包。正常检查应退出零,违规样本应退出一;两类结果共同界定本章完成的工作,页面能够显示代码或图表并不替代实验。




