4、系统架构设计文档:架构图绘制规范(C4模型)、模块接口定义(IDD)、系统约束与假设

好,咱们今天聊聊系统架构设计文档。说实话,这章是整门课里我最想跟你细聊的部分之一。为什么?因为我在项目里见过太多「画图一时爽,维护火葬场」的案例了。架构图画得天花乱坠,结果三个月后连画图的人自己都看不懂——嗯,我自己就干过这种事。

所以今天咱们把话说透:架构图怎么画才不坑人?接口怎么定义才不打架?约束和假设怎么写才不背锅?

4.1 架构图绘制规范:C4模型实战

先说说C4模型。这玩意儿不是什么高深理论,说白了就是一套「从远到近、从粗到细」的画图套路。我习惯把它理解成「地图缩放」——你看高德地图,先看全国,再看城市,再看街道,最后看具体建筑。C4模型也是这个道理。

C4模型的四个层次:

  • Context(系统上下文图):系统在哪儿?跟谁打交道?
  • Container(容器图):系统由哪些「大块头」组成?比如Web服务器、数据库、消息队列。
  • Component(组件图):每个容器里有哪些「小零件」?比如登录模块、支付模块。
  • Code(代码图):具体到类、接口、函数——这个我一般只在关键模块画。

我在项目里遇到过最典型的翻车案例:有人直接画了个「系统架构图」,里面既有服务器又有函数调用,还有数据库表结构。你想想看,这图给项目经理看,他看不懂;给开发看,又嫌太粗。所以C4模型的核心价值就是——不同的人看不同的图

4.1.1 系统上下文图(Context)

这张图最简单,也最容易被人忽略。我建议你把它放在文档的第一页。它回答三个问题:

  • 我们的系统叫什么?
  • 谁在用这个系统?(用户、其他系统)
  • 系统跟外部怎么交互?(API、文件、消息)

画法要点:中间一个大方块代表你的系统,周围画小人或方块代表外部角色,箭头表示交互方向。别画太复杂,一张A4纸能放下就行。

我的小技巧:用不同颜色区分「人」和「系统」。人用蓝色,系统用灰色。这样一眼就能看出哪些交互需要做UI,哪些是后台对接。

4.1.2 容器图(Container)

这张图是技术选型的核心。每个「容器」就是一个可独立部署的单元。比如:

  • 前端App(React Native)
  • 后端API服务(Spring Boot)
  • 数据库(MySQL)
  • 缓存(Redis)
  • 消息队列(RabbitMQ)

画图时注意:每个容器都要标注技术栈。我曾经接手过一个项目,图上只写了「数据库」三个字,结果部署时才发现用的是MongoDB还是PostgreSQL都不知道——你说坑不坑?

4.1.3 组件图(Component)

到了这一层,咱们开始聊「模块」了。比如你的后端API容器里,可能有:

  • 用户管理组件
  • 订单处理组件
  • 支付网关组件
  • 通知推送组件

画组件图时,我习惯用分层布局:上层是接口层(Controller),中间是业务层(Service),下层是数据层(Repository)。这样谁依赖谁,一目了然。

注意:组件图不要画成「面条图」。如果两个组件之间有超过3条连线,说明你的模块划分有问题。我见过最夸张的一个项目,组件图里全是交叉线,最后重构时发现80%的代码都在互相调用——说白了就是没有解耦。

4.1.4 代码图(Code)

这一层我一般只在关键模块画。比如支付模块、权限校验模块。画的是类图、接口图、时序图。别把整个系统的类都画上去——没人看,也没人维护。

4.2 模块接口定义(IDD)

接口定义文档(Interface Definition Document),简称IDD。这东西写得好,团队协作效率翻倍;写得烂,联调时天天吵架。

我个人的习惯是:每个接口都要包含以下6个要素

要素 说明 示例
接口名称 唯一标识,建议用动词+名词 createOrder
输入参数 参数名、类型、是否必填、取值范围 userId (String, 必填)
输出结果 成功/失败时的返回结构 { code: 0, data: {...} }
错误码 每个错误码的含义 1001: 参数无效
调用方式 同步/异步、REST/gRPC/消息 POST /api/v1/orders
性能要求 响应时间、吞吐量 P99 < 200ms

这里我要特别强调一下错误码。很多团队写IDD时只写「成功」的情况,失败的情况一笔带过。结果联调时,调用方不知道「参数错误」该返回什么码,「服务超时」又该返回什么码。我建议你从一开始就把错误码定义清楚,最好做成一个枚举表。

接口定义示例(伪代码):

// 接口:创建订单
// 调用方式:POST /api/v1/orders
// 请求体:
{
  "userId": "string (必填)",
  "productId": "string (必填)",
  "quantity": "int (必填, 1-100)",
  "couponCode": "string (可选)"
}
// 成功响应:
{
  "code": 0,
  "message": "success",
  "data": {
    "orderId": "string",
    "totalAmount": "float",
    "status": "string"
  }
}
// 错误码:
// 1001 - 参数无效(具体字段在message中说明)
// 1002 - 库存不足
// 1003 - 用户余额不足
// 2001 - 系统内部错误

4.3 系统约束与假设

这部分很多人觉得「没啥好写的」,其实恰恰相反——约束和假设是甩锅神器,也是保护自己的护身符

什么叫约束?就是「我们必须在这些条件下干活」。比如:

  • 硬件约束:CPU主频1GHz,内存512MB,Flash 8MB
  • 网络约束:带宽10Mbps,延迟<50ms
  • 时间约束:系统必须在100ms内响应
  • 安全约束:必须支持TLS 1.2以上

什么叫假设?就是「我们默认这些条件成立」。比如:

  • 假设网络连接稳定,丢包率<0.1%
  • 假设第三方服务可用性>99.9%
  • 假设用户输入符合规范(不做极端恶意输入测试)
  • 假设系统运行在Linux x86_64环境下

我曾经踩过的坑:有个项目,我们假设「数据库连接池默认20个连接就够了」。结果上线后用户量暴增,连接池被打满,系统直接挂了。后来复盘时发现,这个假设根本没写在文档里。从那以后,我要求团队把所有假设都白纸黑字写下来,哪怕看起来再理所当然。

写约束和假设时,我建议你遵循「SMART原则」:

  • Specific(具体):别写「性能要好」,要写「响应时间<200ms」
  • Measurable(可测量):别写「要安全」,要写「通过OWASP Top 10安全测试」
  • Achievable(可实现):别写「永不宕机」,要写「可用性99.9%」
  • Relevant(相关):只写跟系统设计直接相关的
  • Time-bound(有时限):别写「未来扩展」,要写「2025年Q2前支持10万并发」

最后说一句:文档是活的,不是死的。架构图、接口定义、约束假设,这些都要随着项目进展持续更新。我见过太多项目,文档写完之后就再也没人碰过——那还不如不写。

好,这一章就聊到这儿。下一章咱们讲详细设计文档,到时候我会分享一些「画时序图不翻车」的实战经验。