C4 组件图(Component):完整指南与示例 - Archyl Blog

C4 组件图(C4 模型第 3 层)放大到单个容器内部,展示其中的组件。本指南讲解什么是组件图、它何时值得维护、一个完整的实战示例、常见错误,以及如何在不背负维护负担的前提下保持第 3 层的准确。

C4 组件图(Component):完整指南与示例

组件图(Component)是 C4 模型的第 3 层——而它也是名声最差的一层。第 1 层(系统上下文)容易画,且很少变化。第 2 层(容器)干净地对应你部署的东西。但第 3 层呢?它放大到单个容器的内部,这意味着每当一位开发者重构一个包,它就要变。大多数团队要么干脆跳过它,要么画一次就任其腐烂。

这很可惜,因为一张维护良好的组件图,是对一位刚接触某代码库的开发者而言最有用的单一产物。它回答了每个新人都会问的问题:"X 的逻辑究竟住在哪里?"

本指南涵盖什么是 C4 组件图、它与 UML 组件图有何不同、第 3 层何时值得为之付出维护成本(以及何时不值得)、一个完整的实战示例、让组件图变得毫无用处的错误,以及如何不靠纯手工就让它们保持准确。

如果你对 C4 模型本身还不熟悉,请先阅读我们的 C4 模型完整指南,再回到这里。

什么是 C4 组件图?

组件图打开一个容器——来自你容器图的一个可部署单元——并揭示其内部的组件。

在 C4 术语中,**组件(component)**是一组内聚的相关功能,藏在一个定义良好的接口之后。想想一个应用内部的主要构建块:

  • 一个 HTTP 控制器或处理器组
  • 一个拥有某一片业务逻辑的领域服务
  • 一个 repository 或数据访问层
  • 一个包装外部 API 的客户端
  • 一个中间件、拦截器或后台 worker

关键词是组(grouping)。组件是对代码的抽象,而不是单个类或文件。OrderService 组件可能横跨十几个文件,但从概念上说它是一件东西:容器中负责订单业务逻辑的那一部分。正如术语表中的定义所说,C4 组件是"一组具有单一职责、更高层级的代码分组"——而不是与某个编程语言类的一一对应。

组件图回答了什么

  • 这个容器内部是如何组织的?
  • 哪个组件拥有哪项职责?
  • 组件之间如何协作来处理一个请求?
  • 对数据库和外部系统的调用从哪里发起?

它刻意省略了什么

  • 单个的类、接口和函数(那是第 4 层,代码图
  • 其他容器的内部
  • 部署与基础设施细节

每张图一个容器,只画组件。如果你发现自己在画类,那就放大得太远了。

C4 组件图 vs. UML 组件图

这一点经常把人绊倒,因为 UML 也有一个叫"组件图"的东西——而它并不是同一回事。

UML 组件图把软件组件建模为具有提供接口和需求接口的单元(那著名的"棒棒糖与插座"表示法),常常强调物理打包、构件(artifact)和部署关系。它带有一套正式的表示法规则和一个精确的元模型。

C4 组件图则更宽松、更务实。它就是一个缩放层级体系中的第 3 层:组件用方框,关系用箭头,每个上面配一段简短的文字描述。没有什么特殊的表示法要学。价值来自这个层级体系——每个组件方框都住在一个特定的容器里,而那个容器又住在一个特定的系统里——而不是来自表示法本身。

实践中:如果有人在 C4 语境下要一张"组件图",他们想要的是一张某个容器内部结构的结构图,一位新开发者能在两分钟内读懂。如果他们想要棒棒糖接口和 <<artifact>> 构造型,那他们要的是 UML。

第 3 层何时值得维护?

这里有一个大多数指南都跳过的诚实答案:在整个 C4 模型中,第 3 层是投入产出比(更准确说是"投入与稳定性之比")最糟糕的一层

上下文图一年变几次。容器图在你增减一个服务时变化——也许每月一次。组件图则每当有人重构一个包、抽出一个服务或重命名一个模块时就要变。如果你手工维护它们,你就是在为永无止境的"园艺"工作签字画押,而一旦你停手,图就开始撒谎。

所以要谨慎地决定在哪里投入这份精力。

在以下情况创建组件图:

  • 容器确实复杂。 一个有 15 个包、多个层级和不那么显而易见的边界的后端 API 值得一张地图。一个只有三个端点的 CRUD 服务则不值得。
  • 结构体现了一个刻意的设计。 如果你用了六边形架构、CQRS 或某种清晰的分层方案,组件图会把意图中的结构显式化——并且当某个 pull request 违反它时,给你一个可以指着说话的东西。
  • 多个团队接触同一个容器。 共享代码需要一个共享的心智模型。
  • 入职是个瓶颈。 如果新开发者要花几周才能在一个容器里摸清门路,那么从第一位新人开始,一张组件图就值回票价了。

在以下情况跳过它:

  • 容器很小,它的目录结构不言自明。
  • 容器是第三方的(你不会去画 Redis 的内部)。
  • 没人愿意承诺让它保持更新。一张陈旧的组件图比没有更糟——它会信誓旦旦地把开发者送向已经不存在的代码。

这恰恰是为什么第 3 层是大多数团队要么跳过、要么自动化的一层。从实际代码结构中生成组件——并据此对它们进行校验——消除了那项扼杀手绘图的维护税。下面会详谈。

一个实战示例:一个 API 容器的内部

我们把它具体化。拿我们 C4 模型指南里的那个电商平台,放大到单个容器:订单 API(Order API, Go)。在第 2 层,它是一个方框。在第 3 层,它打开成一组组件。

组件

组件 职责
Auth Middleware 校验进入请求上的 JWT token,将用户身份注入请求上下文
Order Controller 创建、读取、取消订单的 REST 端点;请求校验与响应序列化
Admin Controller 后台订单管理的 REST 端点(退款、手动改状态)
Order Service 核心业务逻辑:订单生命周期、定价规则、库存检查、支付编排
Order Repository 数据访问层;通过 SQL 持久化和查询订单
Payment Client 包装 Stripe API;处理授权、扣款和退款
Email Adapter 通过 SendGrid 发送事务性邮件(订单确认、物流更新)
Event Publisher 向 Kafka 发布领域事件(OrderPlaced、OrderCancelled)

关系

[Auth Middleware] --> [Order Controller] : Passes authenticated requests
[Auth Middleware] --> [Admin Controller] : Passes authenticated requests (admin role)
[Order Controller] --> [Order Service] : Delegates business operations
[Admin Controller] --> [Order Service] : Delegates back-office operations
[Order Service] --> [Order Repository] : Reads/writes orders
[Order Service] --> [Payment Client] : Authorizes and captures payments
[Order Service] --> [Email Adapter] : Triggers transactional emails
[Order Service] --> [Event Publisher] : Emits domain events
[Order Repository] --> [Order Database (PostgreSQL)] : SQL
[Payment Client] --> [Payment Gateway (Stripe)] : HTTPS/REST
[Email Adapter] --> [Email Service (SendGrid)] : HTTPS/REST
[Event Publisher] --> [Message Queue (Kafka)] : Publishes events

注意数据库、Stripe、SendGrid 和 Kafka 出现在图的边缘。它们不是这个容器的组件——它们是相邻的容器和外部系统——但展示出向外的调用从哪里发起,恰恰是让这张图有用的关键。

这张图告诉新开发者什么

三十秒内,一位加入这个团队的开发者就学到了:

  1. 请求路径:中间件 → 控制器 → 服务 → repository。业务逻辑恰好住在一个地方,而那个地方不是控制器。
  2. 边界:所有外部调用都经过专门的适配器(Payment ClientEmail Adapter)。如果你需要跟 Stripe 通信,你去扩展这个客户端;你不会在控制器里导入 SDK。
  3. 副作用:订单会在 Kafka 上产生事件,并通过 SendGrid 发出邮件。如果某封邮件没发出去,你就知道该去查哪两个组件。
  4. 该在哪里加代码:一个新的"礼品卡"功能显然需要在控制器、服务,以及可能一个新客户端里做改动——而图展示了应当遵循的模式。

最后这一点被低估了。组件图不只是描述结构——它还在规定结构。它告诉贡献者,"与这个代码库保持一致"是什么样子。

组件图的常见错误

把一个类映射成一个组件

最常见的错误。如果你的容器有 80 个类,而你的组件图有 80 个方框,那你只是画了一张多此一举的类图。组件是分组OrderController(一个组件)可能涵盖五个处理器类。目标是每个容器 5-15 个组件。如果你超过了 20,要么你的抽象粒度太细——要么你的容器干的事太多了。

把每个容器都画到第 3 层

对称性很诱人:"我们有 12 个容器,所以需要 12 张组件图。"抵制它。那些图中的大多数永远不会被人读,也永远不会被更新。只为那两三个复杂度真正令人头疼的容器编写文档,其余的让目录结构自己说话。

任由图腐烂

第 3 层比任何其他层级漂移得都快。一月画的组件图,到六月描述的多半是一个已经不存在的容器。每一次重构、每一个抽出的包、每一个重命名的模块,都在拉大这个差距。而一张信誓旦旦却画错的图比没有图更糟:它会把开发者送去寻找两个季度前就被删掉的组件。如果你无法自动化这项维护工作,至少把"更新组件图"放进那个容器的 pull request 检查清单里。

画了组件却不写职责

一个标着 OrderManager、却没有描述的方框就是噪音。每个组件都应该带一句话的职责陈述("校验 JWT token 并注入用户身份")。如果你写不出这句话,那这个组件的边界八成划错了。

展示实现细节而非结构

泛型、设计模式、辅助工具类——这些都不属于第 3 层。如果一个组件有意思的部分在于它是怎么实现的,那是代码图的活儿(或者更常见地,是代码本身的活儿)。

Archyl 如何让组件图保持准确

上面的一切都指向同一个结论:组件图很有价值,但手工维护它们是一场注定要输的游戏。这恰恰是 Archyl 被打造来解决的问题。

AI Discovery 从代码中提取组件

你不必手工画组件,而是连接你的代码仓库并运行 AI discovery。Archyl 分析你的代码结构——包、模块、层级、命名约定——并提议一份 C4 模型,包括每个容器内部的组件以及它们之间的关系。一个有 handlers/service/repository/ 包的 Go 服务,回来时正好就是上面示例里那种组件图。你审阅这些建议,接受或调整它们,几分钟内就有了一份第 3 层模型。

漂移检测标记出偏离

这是让第 3 层活下去的部分。因为 Archyl 把组件链接到你代码仓库中实际的文件和目录,它能持续检查记录在案的组件是否仍然存在于代码中。当有人删掉一个包、重命名一个模块或重构一个层级时,漂移分数会下降,受影响的组件会被标记出来。你的组件图不再是一个快照,而成为一份受监控的契约。

架构即代码(Architecture as Code)

如果你更偏好显式定义而非自动发现,Archyl 支持用存放在你代码仓库中的 YAML 来定义你的 C4 模型——系统、容器、组件和关系。你的组件图被纳入版本管理、在 pull request 中评审,并自动渲染。再结合漂移检测,这就给了你一份具有代码般维护特性的第 3 层文档。

常见问题

C4 组件图和 UML 组件图有什么区别?

UML 组件图是一种正式表示法,带有提供/需求接口、构件和构造型,专注于把组件建模为打包的单元。C4 组件图则是 C4 缩放层级体系中的第 3 层:一张关于某个特定容器内部组件的、务实的方框加箭头视图。C4 没有正式的表示法要求——它的价值来自一致的层级体系(系统 → 容器 → 组件 → 代码),而不是符号本身。

组件和类是一回事吗?

不是。C4 组件是藏在定义良好的接口之后的一组内聚代码——它可能由一个类、十几个类、一个包或一个模块实现。如果你的组件图是一个类对应一个方框,那你向内多放大了一个层级。

每个容器都需要一张组件图吗?

不需要,而试图把每个容器都记录到第 3 层,是放弃 C4 最快的方式之一。只为那些复杂、跨团队共享或对入职至关重要的容器创建组件图。其余的,容器图加上一个可读的目录结构就足够了。

一张组件图应该展示多少个组件?

大致 5 到 15 个。少于 5 个,这张图大概没告诉你任何容器图没告诉你的东西。多于 20 个,要么你的组件边界粒度太细,要么容器本身就该拆分。


想要始终保持准确的组件图吗?免费试用 Archyl,几分钟内从你的代码库生成一份 C4 模型——组件也包含在内。或者继续阅读:什么是 C4 模型?完整指南 | C4 容器图指南 | C4 代码图指南 | 术语表中的组件图