C4 代码图(Code,第 4 层):何时需要它,何时不需要
代码图(Code)是 C4 模型中最被误解的一层。它是大多数团队跳过的一层,是 Simon Brown 本人都形容为可选的一层,然而它也是引发问题最多的一层:"我们真的需要把类画出来吗?""这不就是 UML 吗?""代码变了之后谁来维护这个?"
简短的回答:大多数时候,你不需要 C4 代码图。更长的回答更有意思,因为第 4 层确实能带来回报的那些情形,恰恰是团队在没有它时损失时间最多的情形——密集的算法、受监管的领域,以及进入一段已经没人完全搞懂的代码进行入职。
本指南涵盖 C4 代码图实际展示的内容、为什么手工画一张几乎总是个错误、C4 模型第 4 层值得拥有的具体场景,以及自动生成如何彻底翻转其成本收益的算计。
如果你对 C4 模型还不熟悉,请先阅读我们的 C4 模型完整指南——本文假设你已经了解这四个层级。
什么是 C4 代码图?
代码图是 C4 模型的第 4 层——最深的缩放层级。组件图(第 3 层)展示容器内部的主要构建块,而代码图放大到单个组件,展示它实现层面的细节:
- **类(Classes)**及其关系(继承、组合、依赖)
- **接口(Interfaces)**及实现它们的类型
- **函数(Functions)**与方法,包括关键签名
- 数据结构,如实体、值对象和 DTO
实践中,一张 C4 第 4 层的图通常被渲染为一张 UML 类图或一张实体-关系图。C4 模型并不为这一层规定表示法——它明确地委托给现有标准,因为画类这个问题几十年前就已经解决了。
有两条属性定义了代码层:
- 范围:一个组件。 代码图绝不横跨整个容器,更不用说整个系统。它恰好打开一个组件——
OrderRepository、PaymentService——并展示其内部。 - 粒度:真实的代码符号。 代码图上的每个方框都对应源码中一个实际的类、接口或函数。这里不再有任何抽象。这是图与代码应当是同一件东西的层级。
第二条属性正是第 4 层如此频繁被跳过的原因,我们稍后会看到。(如需快速参考,请见我们术语表中的代码图条目。)
Simon Brown 本人的建议:别画它
这里有一个令初次接触 C4 模型的人惊讶的部分:它的创造者建议不要手工创建代码图。Simon Brown 的指导意见是,第 4 层是可选的,而当你确实需要它时,它应当按需从源码生成——由你的 IDE、由文档工具,或由任何其他读取实际代码的东西——而不是手工绘制和维护。
这个理由很难反驳:
- 代码无时无刻不在变。 一张手绘的类图,其准确性大概只能维持到有人合并下一个 pull request 为止。每一次重命名、每一个抽出的方法、每一个新字段,都让它变得更不对一点。
- 信息已经存在。 与容器图不同——容器图捕捉的决策不存在于任何人的脑子里,也不在任何单个文件中——代码图重复的是源码本身已经精确陈述过的东西。
- 工具做得更好。 现代 IDE(IntelliJ、Visual Studio 等)能在几秒内从代码生成类图。它们总是准确的,因为它们是推导出来的,而非画出来的。
所以任何采用 C4 的团队的默认立场都应当是:对第 1-3 层建模,需要时生成第 4 层。 上下文图、容器图和组件图捕捉的是代码里没有的知识。而代码层本身就是代码。
C4 代码图究竟何时值得
"通常跳过它"不等于"总是跳过它"。确实存在一些情形,让一个代码级别的视图在你的文档中赢得一席之地。
1. 复杂算法与精巧设计
有些组件实现的逻辑,靠从头到尾读文件是真的很难跟下来的:一个带链式策略的定价引擎、一个治理订单生命周期的状态机、一个有深层访问者层级的解析器。当代码的结构本身就是洞见时——当弄清楚哪个类委托给哪个类就是整场仗时——一张代码图能把数小时的代码阅读压缩成一张图。
检验方法:如果一位资深工程师需要一块白板才能向另一位资深工程师解释清楚这个组件,那块白板上的草图,就是一张等待被捕捉下来的代码图。
2. 受监管和受审计的领域
在金融、医疗、航空以及其他受监管的行业里,审计员和评估员常常要求实现层面的文档:哪个类处理持卡人数据、在哪里施加加密、审计轨迹是怎么写的。在这里,一张第 4 层的图不是锦上添花——它是证据。这同样适用于安全评审和威胁建模,在那里,数据流经哪些具体的类至关重要。
3. 进入密集、长寿命的代码进行入职
积累了多年决策的遗留组件对新人而言极其残酷。把你代码库里那三四个最吓人的组件——那些部落知识高度集中的组件——画成代码图,能大幅缩短入职时间。新开发者在一头扎进 8000 行代码之前,先看到了组件的形状。
4. 记录公开契约与设计模式
如果一个组件暴露了一个供其他团队据以构建的 API 表面,或实现了一个其结构本身就是文档的模式(策略、观察者、六边形端口与适配器),那么一个代码级别的视图能让契约显式化。
何时你不需要它(也就是大多数时候)
诚实地面对相反的情形,因为它们覆盖了任何系统中的大多数组件:
- 代码本身可读。 一个命名良好、包结构清晰的组件不需要图。目录树就是那张图。
- 第 3 层已经回答了问题。 如果有人问"订单服务是怎么跟支付通信的?",那是一个组件图的问题。画类只会增加噪音,而非信号。
- 组件就是直白的 CRUD。 一个处理器、一个服务、一个 repository、一个 model。这种形态人人都见过一千遍了。在类级别记录它,等于什么都没记录。
- 没人要求。 文档应当回答人们真正有的问题。如果从来没有人需要一个组件的类级别视图,那创建一个就是把精力花在了没人用的东西上。
更宏观的 C4 方法所秉持的指导原则在这里完全适用:你创建的每一张图,都是你必须维护的一张图。在第 1-3 层,这份维护成本为你换来了别处不存在的知识。在第 4 层,它通常只为你换来一份略微更漂亮、略微更陈旧的源码副本。
自动生成如何改变这道算式
以上的一切都假设必须有人来画并维护这张图。正是这个假设,让 Simon Brown"跳过它或生成它"的建议成为正确选择——手工搞第 4 层的成本几乎从不足以正当化其收益。
自动化把这道算式里的成本一侧抹掉了。Archyl 的 AI discovery 分析你连接的代码仓库,直接从源码中提取代码级别的元素——类、接口和函数,并把它们挂接到你 C4 模型里正确的组件上。开发者不必在一个绘图工具里画出 PaymentService 及其协作者,模型是从代码库中实际存在的内容填充而成的,并且始终与它来自的代码仓库保持连通。
这把问题从"第 4 层值得维护吗?"变成了"第 4 层值得拥有吗?"——一个低得多的门槛。当代码级别的视图是生成并刷新的、而非手绘的时候:
- 它从不撒谎,因为它派生自源码,而非某人对源码的记忆。
- 它在上下文中可导航:你在同一个模型里从系统钻到容器、再到组件、再到代码,而不必在某个 wiki 里翻找一张 PNG。
- 那些"尽管有成本但仍然值得"的情形(审计、入职、复杂组件)变得轻而易举地值得,而那些边缘情形则变得免费。
你仍然要运用判断力,决定哪些组件值得在第 4 层投入关注——一个琐碎 CRUD 组件的生成视图依然是噪音。但失败模式从"陈旧的图误导团队"转变为"一个零成本、没人用的视图"。
一个实战示例:一个支付处理组件的内部
我们把它具体化。假设你的组件图(第 3 层)在后端 API 容器内展示了一个 PaymentProcessor 组件。在第 4 层放大进去,可能揭示出:
[PaymentService (interface)]
├── ProcessPayment(order, method) -> PaymentResult
└── RefundPayment(paymentID, amount) -> RefundResult
[StripePaymentService] ──implements──> [PaymentService]
[StripePaymentService] ──uses──> [StripeAdapter] : wraps the Stripe SDK
[StripePaymentService] ──uses──> [RetryPolicy] : exponential backoff, 3 attempts
[StripePaymentService] ──uses──> [PaymentRepository] : persists payment records
[StripeAdapter] ──maps──> [PaymentResult] : translates Stripe responses to domain types
[RetryPolicy] ──raises──> [PaymentFailedError] : after exhausting retries
[PaymentRepository] ──persists──> [Payment (entity)]
注意这个视图回答了第 3 层无法回答的东西:
- 测试的接缝在哪里?
PaymentService是一个接口;测试可以替换一个假实现,而无需触碰 Stripe。 - 第二家服务商会插在哪里? 一个实现同一接口的
PaypalPaymentService——结构让这个扩展点一目了然。 - 失败时会发生什么?
RetryPolicy类拥有退避行为;PaymentFailedError是上报路径。一位审计员问"一次扣款失败时会发生什么?",从图里就能得到答案。 - 什么会触碰数据库? 只有
PaymentRepository。持卡人数据流是可追踪的,这在一个纳入 PCI 范围的组件里关系重大。
这是一个第 4 层物有所值的组件:它对资金敏感、失败处理不那么显而易见、多个类以一种刻意的模式协作。把它和,比方说,一个只读写一条资料记录的 UserProfileHandler 对比一下——在类级别画那个,不会告诉你任何目录树没告诉你的东西。
C4 第 4 层的常见错误
手工维护类图
经典的失败。一位充满干劲的工程师为整个代码库画出了漂亮的类图;六个月后,它们描述的是一个不复存在的代码库,如今反而在主动误导。如果一张代码图不是从源码生成的——或者至少不是定期重新生成的——那就把它当作画好那天就已过期。
记录 getter、setter 和样板代码
一张列出每个访问器、每个构造函数重载、每个工具方法的代码图,犯的是和一张 1:1 比例尺地图同样的毛病。纳入承载设计意图的元素——接口、关键协作、定义契约的方法——并省略那些繁文缛节。生成的视图应当被过滤,而非倾倒。
在第 3 层够用的地方用第 4 层
如果你的"代码图"展示的是"控制器调服务、服务调 repository"这类东西,那你画的是一张方框形状像类的组件图。把它收回到第 3 层。把第 4 层留给那些内部结构才是看点的组件。
把整个容器画到类级别
一张横跨整个服务的 200 个类的图既违反 C4 层级体系,本身也无法阅读。一张代码图覆盖一个组件。如果你没法把它收得这么紧,那你第 3 层图里的组件边界多半需要重新想想。
把第 4 层当作"完整"文档的必备项
有些团队带着清单思维采用 C4:四个层级,所以要四套图。结果是浪费精力,并对整套实践心生抵触。C4 明确说明第 4 层是可选的。三个维护良好的层级,每一次都胜过四个正在腐烂的层级。
常见问题
C4 第 4 层就是一张 UML 类图吗?
大体上,是的——而且这是有意为之。C4 模型不为代码层定义自己的表示法;它建议复用现有的,而 UML 类图是最常见的选择(ER 图适用于以数据为中心的组件)。C4 增添的是范围与上下文:这张类图恰好描述一个组件,而那个组件坐落在一个由容器图和系统图构成的、可导航的层级体系之中。一张独立的 UML 类图悬浮无依;一张 C4 代码图则有一个地址。
我应该为每个组件都创建一张代码图吗?
不应该。大多数组件足够简单,源码就是最好的文档。把第 4 层留给那些在算法上复杂、对安全或合规敏感,或者是臭名昭著的入职障碍的组件。如果文档是自动生成的,多覆盖一些的成本会下降——但注意力仍然有限,所以要精选你呈现出来的东西。
我该如何让代码图保持更新?
靠不手工维护它。生成它:按需从你的 IDE 生成,在 CI 中从文档工具生成,或者从像 Archyl 这样的平台生成——它在 AI discovery 期间从你的代码仓库提取类、接口和函数,并让代码层始终挂接在你的组件模型上。任何依赖人类在重构后记得去更新一张类图的流程,都会失败。
C4 中组件和代码元素有什么区别?
组件(第 3 层)是一个具有单一职责的逻辑分组——"处理支付的那一部分"——它可能横跨好几个类和文件。代码元素(第 4 层)是源码中一个实际的符号:一个具体的类、接口或函数。组件是你选择的抽象;代码元素是编译器强制执行的事实。
结论
C4 代码图是你应当默认跳过、刻意才动用的一层。第 1-3 层捕捉的是别处不存在的架构知识;第 4 层映照的是代码已经说过的东西,所以它只有在结构本身就是难点时才赢得一席之地——复杂算法、受监管组件、密集的遗留代码——并且最好是生成而非绘制的。
如果你确实想要代码级别的视图,又不想背上维护税,把一个代码仓库连接到 Archyl,让 AI discovery 自动把类、接口和函数提取进你的 C4 模型。需要深度时你就有深度,而再也没人需要重画一张类图了。
想往上看一层?阅读 C4 组件图指南,用完整的 C4 模型指南温习一下,或浏览 C4 模型总览。准备好试试了吗?从免费的 Archyl 开始。