综合架构练习:C4 模型 + ADR 实战
这一课你将能做到
- 用 C4 模型的四个层级(系统上下文、容器、组件、代码)画出判题平台架构图
- 用 ADR(架构决策记录)格式记录"为什么选六边形架构而不是分层"
- 把前面 10 课的知识整合成一个可落地的架构方案
为什么要学
前面学了整洁代码、设计原则、OOP、范式、模式、架构原则、架构风格、架构模式、DDD、企业模式,这一课要把它们串起来。C4 模型是"怎么画架构图",ADR 是"怎么记录决策"。两者结合,让架构不再是"脑子里的想法",而是"可评审、可追溯、可传承"的文档。
先建立图像
C4 四层
─────────
L1 系统上下文:判题平台在用户、判题引擎、DB 之间的位置
L2 容器:Web 应用、判题服务、数据库、消息队列
L3 组件:Web 应用内的 Controller、Service、Repository
L4 代码:具体类和方法(可选,一般用 IDE 生成)
ADR 格式
─────────
标题:选择六边形架构
状态:已接受
上下文:判题平台需要支持多语言判题和多种存储
决策:采用六边形架构,核心领域不依赖框架
后果:代码量 +20%,但测试和框架迁移成本降低
跟着做
步骤 1:C4 L1——系统上下文图
判题平台在整体生态中的位置:
┌─────────┐ ┌─────────────┐ ┌─────────┐
│ 用户 │────→│ 判题平台 │────→│ 判题引擎 │
│ (Web) │ │ (Spring Boot)│ │ (Docker)│
└─────────┘ └─────────────┘ └─────────┘
│
↓
┌─────────────┐
│ MySQL │
│ Redis/MQ │
└─────────────┘
判题平台是中心,上游是用户,下游是判题引擎和存储。
步骤 2:C4 L2——容器图
┌─────────────────────────────────────┐
│ 判题平台系统 │
├─────────┐ ┌─────────┐ ┌─────────┐│
│ Web 应用 │ │ 判题服务 │ │ 通知服务 ││
│(Spring) │ │(Spring) │ │(Spring) ││
│ :8080 │ │ :8081 │ │ :8082 ││
└────┬────┘ └────┬────┘ └────┬────┘│
│ │ │ │
└────────────┴────────────┘ │
│ │
┌─────────────┐ │
│ RabbitMQ │ │
│ (事件总线) │ │
└─────────────┘ │
│ │
┌─────────────┐ │
│ MySQL │ │
│ Redis │ │
└─────────────┘ │
└─────────────────────────────────────┘
Web 应用接收提交,发事件到 MQ;判题服务消费事件,执行判题;通知服务消费结果事件,推送 WebSocket。
步骤 3:C4 L3——组件图(Web 应用内部)
┌─────────────────────────────┐
│ Web 应用 │
├─────────┐ ┌─────────────┐ │
│Controller│ │ Service │ │
│Submission│→│ Submission │ │
│Controller│ │ Service │ │
└─────────┘ └──────┬──────┘ │
│ │
┌─────────────┐ │
│ Domain │ │
│ Submission │ │
│ Aggregate │ │
└─────────────┘ │
│ │
┌─────────────┐ │
│ Repository │ │
│ Interface │ │
└─────────────┘ │
└─────────────────────────────┘
Controller → Service → Domain → Repository,依赖方向向内,符合六边形架构。
步骤 4:ADR 实战——为什么选六边形架构
# ADR-001: 采用六边形架构而非传统分层
## 状态
已接受 (2025-01-15)
## 上下文
判题平台需要支持:
- 多语言判题(Java/Python/C++),未来可能加 Rust/Go
- 多种存储(MySQL 主库,Redis 缓存,ES 搜索)
- 高并发判题回调(WebSocket 推送)
- 单元测试不依赖 Spring 容器
## 决策
采用六边形架构(端口适配器),核心领域层不依赖任何框架。
## 理由
1. **可测试性**:领域层纯 POJO,不启动 Spring 就能测试
2. **框架无关**:换 Spring → Quarkus 或 Micronaut,核心业务不动
3. **端口隔离**:判题引擎、存储、通知都通过端口接入,可插拔
## 后果
- 正面:测试速度提升 3 倍,框架迁移成本降低
- 负面:代码量增加 20%,需要维护更多接口
- 缓解:用 Spring 的 `List<Port>` 自动注入,减少手动装配
## 备选方案
- 传统分层架构:简单但框架耦合,测试慢
- clean architecture:与六边形类似,但层次更多,复杂度更高
讲清楚要点
- C4 模型:L1 系统上下文 → L2 容器 → L3 组件 → L4 代码,层层递进。
- ADR:标题、状态、上下文、决策、理由、后果,让决策可追溯。
- 架构图不是艺术品:是给团队看的沟通工具,重点是"谁调谁、谁依赖谁"。
- ADR 不是马后炮:是决策时的记录,包括备选方案和后果。
- 综合应用:整洁代码 + 设计原则 + 模式 + DDD + PoEAA,最终体现在 C4 图和 ADR 里。
你可能会懵的地方
- C4 和 UML 什么关系? C4 是 UML 的简化版,专为架构设计。不学 UML 也能画 C4,重点是"分层抽象"。
- ADR 要写多少? 关键决策写,显而易见的不用写。判题平台里"选六边形"是关键决策,"用 Lombok"不是。
- L4 代码层要不要画? 一般不画,IDE 可以生成。关键算法或复杂流程可以画时序图补充。
- C4 图和代码不同步怎么办? 用工具(如 Structurizr)从代码生成 C4 图,或者定期评审更新。图是文档,不是一次性产物。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| 画一个巨大的类图 | 信息过载,没人看 | C4 分层,每层聚焦一个抽象级别 |
| ADR 只写"选了 A" | 无法追溯为什么 | 写清上下文、备选、后果 |
| 架构图和代码脱节 | 图是艺术,代码是现实 | 从代码生成图,或定期评审更新 |
| 所有决策都写 ADR | 文档爆炸 | 只写关键、有争议、影响大的决策 |
| C4 图用错箭头方向 | 依赖方向反了 | 上层指向下层,或外部指向端口 |
卡点急救
- C4 图不知道从哪层开始:先画 L1 系统上下文,确定系统边界;再画 L2 容器,确定技术选型;最后画 L3 组件,确定代码结构。
- ADR 写不出来:问自己"这个决策有备选吗?为什么选这个不选那个?"有备选就值得写。
- 架构评审没准备:把 C4 图和关键 ADR 打印出来,逐层讲解,重点讲"为什么这样设计"。
面试常问
- C4 模型四个层级分别是什么? 答:L1 系统上下文(系统在生态中的位置)、L2 容器(技术选型,如 Web 应用、数据库)、L3 组件(代码结构,如 Controller、Service)、L4 代码(具体类,一般用 IDE 生成)。
- ADR 的核心要素? 答:标题、状态、上下文、决策、理由、后果、备选方案。判题平台"选六边形架构"的 ADR 要写明"为什么选、放弃了什么、有什么代价"。
- 怎么把 DDD、设计模式、架构风格整合到一个项目里? 答:DDD 提供领域模型(实体、值对象、聚合),设计模式提供协作方式(策略、观察者、工厂),架构风格提供骨架(六边形),C4 提供文档,ADR 提供决策记录。判题平台就是这套组合。
自测三问
- 画出判题平台的 C4 L2 容器图,标出每个容器的技术选型。
- 写一个"为什么选 RabbitMQ 而不是 Kafka"的 ADR。
- 判题平台的 Submission 聚合根包含哪些对象?为什么 JudgeResult 不能是聚合根?
小结一句话
C4 让架构可视化,ADR 让决策可追溯;两者结合,让架构从"脑子里的想法"变成"团队可评审的方案"。
延伸
- 《架构整洁之道》Robert C. Martin,第 22-24 章
- C4 模型官网
- ADR 模板 - Michael Nygard
- 下一课:路线图对照复习——与 software-architect-essentials 的分工对照。