1. 为什么我们需要C4模型来绘制软件架构图?
十年前我刚入行时,第一次被要求画系统架构图,随手在白板上画了几个方框加箭头就交差了。结果技术总监看完直接问我:"你这图里所谓的'服务'是指物理服务器还是Docker容器?箭头表示的是HTTP调用还是消息队列?"当场被问得哑口无言。这正是传统架构图的通病——缺乏统一语义,不同角色看到的是不同理解。
C4模型由Simon Brown提出,其核心价值在于提供了一套标准化的架构描述语言。就像建筑行业有施工图、水电图等不同视角的图纸一样,C4模型通过四个层次(Context, Containers, Components, Code)来展现系统的不同抽象级别。我参与过的一个电商平台重构项目就深刻体会到了这点——当产品经理、架构师和开发人员都看着同一套C4图讨论时,沟通效率提升了至少50%。
关键提示:C4不是另一种图形标准,而是一套思考框架。它不限制你使用UML、流程图或任何其他图形符号,而是规范了应该展示哪些信息以及如何组织这些信息。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. C4模型的四层导航体系详解
2.1 第一层:Context(上下文) - 系统与外界的关系图
Context图是最高层次的抽象,相当于系统的"世界地图"。在这个层级,我们只关心两件事:
- 目标系统是什么(用一个方框表示)
- 系统与哪些外部角色/系统交互(用人和外部系统图标表示)
以在线教育平台为例:
plaintext复制+---------------+ +----------------+ +-------------+
| 学生(角色) |<----->| 在线教育平台 |<----->| 支付网关 |
+---------------+ +----------------+ +-------------+
^ ^
| |
v v
+---------------+ +----------------+
| 教师(角色) | | CMS系统 |
+---------------+ +----------------+
这张图不需要任何技术细节,但却能清晰回答:"系统为谁服务?依赖哪些外部系统?"这类战略问题。我在需求评审会上发现,用Context图向非技术干系人解释系统定位时,他们理解的速度比看文字需求文档快得多。
2.2 第二层:Containers(容器) - 技术选型全景图
Containers不是指Docker容器,而是指系统的"大陆板块"。一个Container是可独立执行/部署的单元,比如:
- 移动应用APP
- 前端SPA网站
- 后端微服务
- 数据库
- 消息队列
继续以在线教育平台为例:
plaintext复制+-------------------+ REST API +-------------------+
| Web前端(React) |<-------------->| API服务(Java) |
+-------------------+ +-------------------+
^ ^
| |
gRPC | | Kafka
v v
+-------------------+ +-------------------+
| 移动端(Flutter) | | 批处理服务(Python) |
+-------------------+ +-------------------+
这个层级的技术总监最关心的是:
- 我们用了哪些技术栈?
- 各模块间通信机制是什么?
- 系统边界在哪里?
我在设计评审时发现,明确标注每个Container的技术栈(如React/Java/Python)能有效避免开发团队对技术方案的误解。
2.3 第三层:Components(组件) - 核心逻辑单元拆解
Components层展示的是单个Container内部的"城市布局"。这里的Component是指具有明确职责的代码模块,比如:
- Controller
- Service
- Repository
- 第三方SDK适配器
以API服务Container为例:
plaintext复制+-----------------------+
| 课程管理Controller |
+-----------------------+
|
v
+-----------------------+
| 课程服务(Service) |
+-----------------------+
|
v
+-----------------------+
| 课程Repository(MyBatis)|
+-----------------------+
|
v
+-----------------------+
| MySQL数据库 |
+-----------------------+
这个层级对开发团队最具指导意义。我曾在一个项目中发现,由于没有清晰的Components图,两个团队重复开发了功能相似的Service模块。通过维护Components图,可以显著减少这类浪费。
2.4 第四层:Code(代码) - 实现细节的显微镜
Code层是C4模型中最少使用的层级,它对应的是具体的类、方法实现。在现代IDE如此强大的今天,除非是要解释某个复杂算法设计,否则很少需要画这一层的图。典型的应用场景包括:
- 向团队讲解某个设计模式的具体实现
- 说明一个复杂状态机的流转逻辑
- 关键算法的流程图解
例如支付状态机的代码级描述:
java复制// 状态枚举
enum PaymentState {
INIT,
PROCESSING,
SUCCESS,
FAILED,
REFUNDED
}
// 状态转换规则
stateMachineBuilder.transition()
.from(INIT).to(PROCESSING).on("submit")
.from(PROCESSING).to(SUCCESS).on("confirm")
.from(PROCESSING).to(FAILED).on("timeout");
3. 实战:用C4模型重构一个遗留系统文档
去年我主导了一个老牌CRM系统的重构项目,原始文档只有一份200页的Word和几张混乱的Visio图。我们采用C4模型进行知识重建的过程如下:
3.1 逆向工程:从代码到C4图
-
Context层识别:
- 通过接口日志分析找出所有调用方:Web Portal、Mobile App、第三方合作伙伴
- 从数据库审计中发现还在使用的老报表系统
-
Containers层梳理:
bash复制# 通过部署脚本发现的服务模块 find /deploy -name "*.war" -exec ls -l {} \; # 输出: # web-ui.war # api-core.war # report-generator.jar # integration-adapter.jar -
Components层提取:
- 使用ArchUnit验证架构约束:
java复制@ArchTest static final ArchRule service_should_only_be_accessed_by_controller = classes().that().resideInAPackage("..service..") .should().onlyBeAccessed().byAnyPackage("..controller..", "..service..");
3.2 工具链搭建:从绘图到活文档
我们采用的工具组合:
- 绘图工具:Structurizr(专为C4模型设计的工具)
- 代码即文档:将架构图与代码关联
plantuml复制@startuml !include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml Person(admin, "系统管理员") System(crm, "CRM系统") System(mail, "邮件服务") Rel(admin, crm, "管理用户") Rel(crm, mail, "发送通知") @enduml - 文档生成:通过Maven插件将架构图集成到站点
xml复制<plugin> <groupId>org.asciidoctor</groupId> <artifactId>asciidoctor-maven-plugin</artifactId> <version>2.2.1</version> <configuration> <sourceDirectory>src/docs/asciidoc</sourceDirectory> <attributes> <imagesdir>./images</imagesdir> </attributes> </configuration> </plugin>
3.3 团队协作规范
我们制定的绘图规范:
-
颜色语义:
- 绿色:已实现的组件
- 蓝色:进行中的开发
- 灰色:废弃/待移除
-
版本控制:
- 架构图与代码同仓库存储
- 每次架构变更需要更新对应层级的图
-
评审流程:
mermaid复制graph TD A[开发人员修改Components图] --> B(发起Merge Request) B --> C{技术负责人评审} C -->|通过| D[合并到主干] C -->|拒绝| E[返回修改]
4. 常见误区与避坑指南
4.1 过度工程:把架构图变成艺术品
我曾见过一个团队用3D效果画架构图,各种阴影、渐变、立体效果一应俱全。结果:
- 每次修改都要找专业美工
- 图形元素干扰信息传达
- 文件体积巨大难以版本控制
正确做法:坚持KISS原则,使用简单的矩形、线条和基础色块。推荐配色方案:
- 角色:橘色
- 系统:蓝色
- 外部服务:灰色
4.2 抽象泄漏:层级混淆的典型症状
常见错误示例:
plaintext复制+-------------------+ +-------------------+
| 用户(角色) | | MySQL(技术组件) |
+-------------------+ +-------------------+
这直接把Context层和Containers层的元素混在一起了。修正后:
plaintext复制# Context层
+-------------+ +-----------------+
| 用户(角色) |<----->| 订单系统(黑盒) |
+-------------+ +-----------------+
# Containers层
+-----------------+
| 订单服务(Java) |
+-----------------+
|
v
+-----------------+
| MySQL数据库 |
+-----------------+
4.3 信息过载:试图在一张图中展示所有细节
一个真实的反例:某系统架构图包含了从用户角色到类方法的全部四个层级,结果打印出来需要12张A3纸拼接。这种图根本没法用于日常沟通。
分层展示技巧:
- 使用工具的书签/图层功能
- 为不同角色准备不同视图:
markdown复制
docs/ ├── architecture/ │ ├── 01-context.md # 给业务方 │ ├── 02-containers.md # 给运维 │ └── 03-components.md # 给开发 └── README.md
4.4 缺乏演进:架构图与系统实际脱节
我见过最极端的案例:系统已经完成微服务拆分半年了,架构图上还显示是单体应用。这比没有架构图更危险。
保持同步的实践:
- 将架构图检查加入CI流水线
yaml复制# .github/workflows/arch-check.yml jobs: arch-review: runs-on: ubuntu-latest steps: - uses: structurizr/cli@v1 with: args: validate -w workspace.dsl - 架构守护测试(示例):
java复制@Test public void should_not_allow_direct_db_access_from_controller() { JavaClasses classes = new ClassFileImporter() .importPackages("com.myapp"); ArchRule rule = noClasses() .that().resideInAPackage("..controller..") .should().accessClassesThat().resideInAPackage("..repository.."); rule.check(classes); }
5. C4模型与其他架构方法的配合
5.1 与UML的互补关系
C4模型不是要取代UML,而是与之配合:
- 用C4定义架构全景
- 用UML序列图补充关键流程
plantuml复制@startuml actor User participant "Order Service" as OS participant "Payment Service" as PS User -> OS: 提交订单 OS -> PS: 创建支付 PS --> OS: 支付URL OS --> User: 跳转支付 @enduml
5.2 在敏捷环境中的实践
Scrum团队如何应用C4:
- Sprint规划时更新Context图
- 技术故事任务分解参考Components图
- 架构看板示例:
markdown复制
| 层级 | 负责人 | 状态 | 最后更新 | |------------|--------|--------|----------| | Context | PO | 已确认 | 2023-10-01 | | Containers | 架构师 | 评审中 | 2023-10-15 | | Components | Tech Lead | 开发中 | 2023-10-20 |
5.3 与微服务架构的结合
在微服务设计中特别有用的模式:
- 每个服务独立的C4文档:
bash复制
services/ ├── order-service/ │ ├── docs/ │ │ ├── context.md │ │ └── containers.md │ └── src/ └── payment-service/ ├── docs/ │ ├── context.md │ └── containers.md └── src/ - 全局上下文图:展示服务间关系
- 契约测试:验证接口一致性
java复制@Pact(provider="OrderService", consumer="PaymentService") public RequestResponsePact createPact(PactDslWithProvider builder) { return builder .given("订单已创建") .uponReceiving("支付请求") .path("/payments") .method("POST") .willRespondWith() .status(201) .toPact(); }
6. 推荐工具链与实践心得
6.1 绘图工具横向对比
| 工具名称 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Structurizr | 原生支持C4,版本控制友好 | 付费版功能更完整 | 长期维护的大型项目 |
| PlantUML | 代码化设计,易于版本控制 | 学习曲线较陡 | 开发者主导的团队 |
| Draw.io | 免费易用,模板丰富 | 缺乏C4原生支持 | 快速原型设计 |
| Miro | 协作功能强大 | 难以维护严谨的架构图 | 远程团队头脑风暴 |
6.2 我的个人工作流
-
初始设计阶段:
- 用Miro白板与团队快速草图
- 导出到Draw.io细化
-
开发阶段:
- 用PlantUML编写架构即代码
- 集成到项目的docs-as-code体系
-
维护阶段:
- 使用Structurizr维护正式文档
- 通过CI自动生成架构差异报告
6.3 让架构图保持活力的技巧
- 将架构评审纳入DoD:每个用户故事完成的标准包括更新相关架构图
- 架构守护测试:像写单元测试一样编写架构规则
java复制@ArchTest static final ArchRule layers_should_respect_dependencies = layeredArchitecture() .layer("Controller").definedBy("..controller..") .layer("Service").definedBy("..service..") .layer("Repository").definedBy("..repository..") .whereLayer("Controller").mayNotBeAccessedByAnyLayer() .whereLayer("Service").mayOnlyBeAccessedByLayers("Controller") .whereLayer("Repository").mayOnlyBeAccessedByLayers("Service"); - 可视化架构债务:用颜色标注需要重构的部分
plantuml复制@startuml !include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Component.puml Component(legacy, "老订单模块", "技术债务", "red") Component(new, "新支付服务", "推荐方案", "green") @enduml
在最近一次系统升级中,我们通过持续维护的C4图,仅用2天就完成了影响范围评估,而过去类似工作通常需要1-2周。这让我深刻体会到:好的架构图不是装饰品,而是高效工程实践的加速器。
