软件建模E09:4+1 与 C4 的多视角表达
租赁确认只有一个入口,也会产生不同问题:客服关心重复提交会不会预留两次,开发者关心规则在哪个类里,运维关心服务跑在哪台机器、重启会丢掉什么。把这些答案挤到一张图中,类、进程和节点很容易变成同一种方框,图越完整,含义反而越不确定。
第 38 篇提供了可执行基线:真实 HttpClient 经回环网络调用 HttpServer,模块门面调用 RentalApplication,后者使用内存仓储。客户端与服务器位于同一个 JVM,维修检查是本机固定函数,发布操作只写内存轨迹。本篇从这份代码建立多视图目录,并用实际运行日志检查图中的身份、所有权、边界和请求路径。
视角与抽象层次分开选择
Kruchten 的 4+1 视图模型以逻辑、进程、开发、物理视图描述不同关注点,关键场景把它们联系起来。逻辑视图说明业务抽象及关系,进程视图关注运行时并发与同步,开发视图说明源码组织,物理视图说明软件怎样映射到硬件。场景既能说明既有决定,也能暴露需要修改的决定。
C4 的上下文、容器、组件和代码属于不同抽象层次。它首先帮助读者逐步放大系统,而不是给运行、安全、开发等关注点各分一层。组件图可以表达逻辑分解,部署图则另外描述实例落位;不能把“上下文等于逻辑、容器等于进程”当成固定对应关系。
选择视图前应写下要回答的问题。这个租赁样例需要回答重复请求、代码定位和运行边界,因此逻辑及开发视图用同一组稳定身份,进程视图补充同步关系,物理视图只展示本机节点。没有多机部署需求和实验,就不为凑齐方框而添加数据库服务器或消息代理。
视图也不需要全部采用同一种记法。源码归属可以用路径表,实际调用可以用时序图,部署边界可以用框图。更重要的是同一个元素在不同工件中仍能被识别,且“属于某模块”“调用某方法”“部署在某节点”这些关系各自有明确名字。
逻辑视图保留业务职责
逻辑视图中的 module 是请求翻译门面,application 是租赁用例,repository 是状态访问端口对应的内存实现。门面校验受限请求号,把它转换成固定设备、租期和费率的 Command;应用服务执行预留规则,再通过仓储更新回执与占用。当前 HTTP 接口并非通用租赁 API。
元素身份使用 http、module、application、repository。展示标签可以改成中文,但机器检查的身份不随排版变化。若把同一个仓储画成“租赁存储”和“结算数据库”,读者可能误以为有两份状态;若它们实际是同一个对象,跨图目录就应该给出明确对应,而不是让读者猜测。
下图回答逻辑职责与开发位置怎样对应,省略类的全部字段、继承和辅助方法。方框是软件职责,不能解读成四个部署单元。
flowchart LR
H["http:/reserve适配器"] --> M["module:Module.submit"]
M --> A["application:RentalApplication"]
A --> R["repository:MemoryRepository"]
H -.源码.-> S["labs/38/ArchitectureCheck.java"]
M -.源码.-> S
A -.源码.-> P["rental-application/RentalApplication.java"]
R -.源码.-> P
逻辑分解不要求每个方框都有一个接口文件。第 38 篇的 Module 是 ArchitectureCheck 的内部类,不是 Java 模块系统中的模块,也不是独立微服务。把教学名称当作技术部署事实,会让后续性能、安全和恢复判断失去依据。
进程视图说明共享状态与同步
HTTP 客户端通过本机网络栈发送 POST,服务器处理回调后进入同一个 endpoint 对象。它们共享 JVM 堆中的应用与仓储对象。网络协议确实被执行,但这不产生跨进程故障隔离,也不证明客户端与服务器可以分别重启。
MemoryRepository 的 read 和 transact 使用 synchronized。一次事务在同一个仓储实例上串行执行,并以快照更新已提交状态;源码中出现 commit 轨迹,含义是这段内存事务完成,不是数据库 COMMIT,更不是磁盘持久化。跨视图术语表需要保留这一限定。
轨迹集合使用 CopyOnWriteArrayList,允许服务器与测试侧共同观察事件。它只解决这份观测列表的并发访问,不能据此推断整个应用无竞争。实验按顺序发出确认、重放与非法输入,没有进行吞吐量压测,也没有验证多仓储实例之间的协调。
线程数量不应凭框图猜测。源码创建了 HttpServer 与 HttpClient,但没有固定写出完整运行时线程清单。本篇的进程视图只确认同 JVM、HTTP 回调与同步仓储这一层事实;具体执行器线程名称、线程池饱和与调度公平性继续列为未测项,避免把库内部实现当作稳定业务契约。
开发与物理视图如何互相约束
开发视图把 http、module 定位到 ArchitectureCheck.java,把 application、repository 定位到 RentalApplication.java。检查器除了确认文件存在,还核对指定符号,如 server.createContext(“/reserve”)、Module 和 synchronized transact。仅有路径字符串容易指向陈旧文件,符号核对能发现一部分重构后失配。
路径存在仍不是完整依赖分析。检查器没有解析 Java 抽象语法树,也不能证明所有调用都经过应用服务。它检查的是本案例中声明的有限映射;如果系统引入反射、动态代理或更多适配器,应扩展事实提取方式,而不是把字符串检查包装成全工程架构治理。
物理视图只有一台本机节点、一个 JVM 和内存状态。动态端口在启动后从实际日志读取,不能提前编造一个固定端口。进程退出后内存状态消失的风险来自存储选择,图上应直接注明,不能因为持久化在未来计划中就提前画成已经运行的数据库容器。
这里的“一个 JVM”限定这次实验,不是产品永远只能单进程运行。目标部署可以另画,但要标明目标和 NOT_RUN,并列出从当前状态到目标状态需要解决的连接、身份、持久化及恢复问题。现状图与目标图混在一起,会让评审错误估算已经具备的能力。
用关键场景贯穿全部视图
正常请求体为 http-r1,服务器返回 200 和 CONFIRMED,仓储的有效预留数变成一。再次发送相同请求,返回仍为 CONFIRMED,有效预留数保持一;请求体改为感叹号后返回 400,快照保持不变。检查器直接调用第 38 篇入口获得这些断言,没有自己打印三行“成功”代替真实 HTTP。
下图回答同一请求怎样跨越已命名职责。外框是同一 JVM;客户端与服务器之间的箭头实际经过回环 HTTP,其余箭头是进程内协作。省略网络超时与认证,因为当前接口未实现这些能力。
sequenceDiagram
box 本机同一JVM
participant C as 测试HttpClient
participant H as http
participant M as module
participant A as application
participant R as repository
end
C->>H: POST /reserve,http-r1
H->>M: submit
M->>A: reserve
A->>R: transact
R-->>A: 内存快照提交
A-->>H: CONFIRMED
H-->>C: HTTP200
实际轨迹中 repository.commit 先于 publisher.ReservationConfirmed,而且确认事件只出现一次。这个顺序支持“本机正常路径先提交再记录事件”,却不能保证退出窗口不丢事件。发布器只是内存轨迹,进程恰在两步之间退出后的恢复需要可靠消息方案,当前证据对此没有覆盖。
C4 上下文层把租赁系统视为一个整体,业务使用者是外部角色;实验用同 JVM 的测试客户端代替这个角色发起请求。容器层将当前可运行应用描述为一个 JVM 程序,内存仓储是程序内部状态而非独立数据库容器。组件层再展开 http、module、application、repository。代码层可直接查看 Java 文件,没有必要为了四层齐全再复制一张类图。
可执行目录为外部角色 caller、租赁系统 rental-system、单个 rental-container 分别声明 Person、SoftwareSystem、Container 类型,再把四个源码元素声明为 Component。包含关系只有系统包含容器、容器包含组件两层。上下文关系是角色请求系统,容器视图展开角色到容器的请求,组件视图才展开内部调用。逻辑外部角色并不意味着实验客户端另起了进程。
检查器逐一验证层次类型、包含关系和每条连线两端是否属于相应视图。把容器类型改成组件,或者把包含关系终点改成不存在的容器,都会独立退出一。这些规则能够拒绝把同四个组件复制到三层后只更换图名的模型,但仅覆盖本案例,不能替代通用 C4 建模工具。
让错误视图也能失败
views.json 保存元素、所有权、视图引用和请求路径。运行入口先执行真实第 38 篇,再验证每个元素引用合法、源码符号存在、声明事件确实出现在轨迹中,请求路径的事件位置按观察顺序前进。固定场景集合必须包含确认、重放与非法输入,不能删除难解释的场景后继续通过。
四个独立错误文件分别把部署改为两个进程、把应用源码改成不存在的文件、把仓储所有权改成结算、把请求路径改成先提交仓储再进入门面。每个文件都使用同一个检查程序并得到退出码一,原始错误说明保存在对应 stderr 文件中。这些是模型错误的实测拒绝,不是静态文档上的红色标记。
检查器并不判断架构是否优秀。它只能识别已经写成规则的不一致;错误的规则本身仍可能稳定通过。因此评审需要同时审阅规则、正常样本和有区分力的反例,确认检查没有退化成“只允许当前文件逐字不变”,也没有宽松到任何图都能通过。
独立运行与范围
1 | |
错误样本可作为同一入口的参数,例如 models/E09/invalid-boundary.json 的仓库相对路径。需要 JDK 21、Python 标准库与本机回环监听权限。初次沙箱禁止 bind 的错误已单独保留;在允许回环监听的环境重新运行完整场景,不能跳过 HTTP 部分只检查 JSON。
原始命令、版本、stdout、stderr 和退出码位于 examples/software-modeling/evidence/E09/。源码摘要可以核对这次检查所依据的具体文件,读者修改视图或源码后应重新运行,而不是继续引用旧 PASS。实验下载包包含第 38 篇及其实际编译依赖,解压后使用相同相对路径入口。
本篇不声明数据库持久化、独立客户端进程或多机容灾。它交付的是一组与当前实现相符的多视图表达,以及能拒绝有限不一致的检查器。后续架构描述可以在这份事实底稿上补充干系人与决策,而不用重新猜测图中的方框究竟表示什么。
下载累计实验源码与证据参考资料
- Philippe Kruchten:Architectural Blueprints—The 4+1 View Model,1995,第1–5页用于视图关注点。
- Simon Brown:C4 Diagrams,抽象层次与支持图类型。
