ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

图表即代码:diagram-design让架构图像代码一样可维护、评审、复用

图表即代码:diagram-design让架构图像代码一样可维护、评审、复用

写这套图表脚本,我前后重构了三次,中间推翻过两版,才找到一套真正能扛住真实业务压力的结构。如果你也在用手拖拽画架构图、时序图,画完发现版本对不上、变更查不到、图形风格全靠个人手感,那这篇东西应该对你有用。

这不是一篇入门级软件广告,也不是什么新框架的发布会。diagram-design 是我对一个问题的回答:当图表变成交付物的一部分时,怎么让它们像代码一样被维护、评审、复用、演进。如果你正在负责一个中大型系统的设计文档,或者被"图跟代码不同步"折磨过,下面这些思路和代码可以直接抄走。

1. 从拖拽工具迁到脚本化:逼着自己先吃够了苦头

先说动机。以前团队画架构图用的是传统绘图软件,画一张图动辄半天。更要命的是,图跟实际代码差了好几轮迭代。后来我们换了思路,把所有交付级图表全部改成"以代码为源"的脚本化方案,diagram-design 就是在这样被迫换挡的过程中长出来的。

1.1 传统绘图模式下那些没人敢说的痛点

传统拖拽画图有几个天然瘸腿的地方:

  • 版本对比是奢望。两张图的细微差异只能用肉眼找。有一次我改了一个模块的位置,评审人愣是看了十分钟没发现。
  • 代码审查成了空话。架构图评审只能"看着聊",改起来无从下手,大家渐渐默认"图是可以不准的"。
  • 图形风格全靠个人手感。四个人画同一套系统,就有四种配色、四种线宽、四种图例风格。文档拼在一起活像四个人分别画完再硬贴的。
  • 图表复用基本为零。这个系统里画过的组件,换一个文档里再画一遍,没有任何人觉得应该去复用原来的那份。

如果只是画给自己看,这些问题都还能忍。可当图表要作为需求说明、设计评审、交接文档的正式组成部分时,它们就不再是"好看不好看"的问题,而是交付质量问题。

1.2 为什么说"图也是代码"是个反直觉但正确的起点

我在重构所有存量图表时,定的第一条原则就是:凡是交易在文档里的图,必须有源代码,必须可以自动渲染,必须在代码审查里能逐字 diff。

这个决定当时遭到了不少质疑。有同事认为画个图而已,引入代码化是"过度工程"。但真正上手后,大家发现"图即代码"带来的第一波红利不是效率,而是安全感:你不再担心图是过时的。代码改哪了,图跟着改,遗漏变更时审查阶段就能发现。这种感觉一旦尝到,就很难退回拖拽模式了。

说白了,diagram-design 不是什么天才设计,它是我吃得苦头够多之后,老老实实总结出的一套工程约束。

2. diagram-design 的设计骨架:把自己当成一个小型渲染引擎

我迭代到第三版时,项目的内部结构才稳定下来。整体上它分四层,每层职责单一,核心是不把任何业务语义写死在渲染函数里。

2.1 层次一:声明式图表 DSL,把图画成数据

图表本质上是"节点+关系+分组"三个概念的组合。diagram-design 的第一层是一套轻量级 DSL,让一张图变成一段结构化的数据描述。

以一张订单系统的核心调用链为例:

diagram order_flow { participant client as "客户端" participant gateway as "API 网关" participant order as "订单中心" participant stock as "库存服务" client -> gateway: "提交订单" gateway -> order: "创建订单" order -> stock: "预扣库存" stock --> order: "扣减结果" order --> gateway: "业务响应" gateway --> client: "返回结果" }

这段描述里没有任何布局信息。我在设计 DSL 的时候就决定布局是引擎的事,不是写图的人的事。因为一旦允许人为指定坐标,就意味着每张图可以凭手感生长,版本间 diff 会被大量无意义的坐标改动污染。

有人会觉得不指定坐标会失去控制。我的观点是:你失去的是"微调的自由",换来的是"一致性、可审查性和自动化"。对交付级文档来说,这笔交易是划算的。

2.2 层次二:布局引擎,自动排布节点与关系

第二层是布局引擎,负责读取 DSL 生成带坐标的图模型。diagram-design 支持三种布局策略,我基本按照图类型来选:

图类型默认布局策略适用场景
架构分层图分层/网格布局系统模块、服务依赖
时序链路图纵向分层布局调用链、请求时序
思维导图/分组图树形辐射布局需求拆解、领域建模

布局引擎的处理管线分为建模、分层、排布、去重叠四个阶段。前两者决定节点上下关系,后两者决定视觉密度。

实际编码时,我尽量不发明算法,而是把问题拆小后复用经典方案。比如去重叠算法直接采用了基于力导向的碰撞分离策略,代码不到一百行:

def separate_overlaps(nodes, padding=16): changed = True while changed: changed = False for i in range(len(nodes)): for j in range(i + 1, len(nodes)): if overlaps(nodes[i], nodes[j], padding): push_apart(nodes[i], nodes[j]) changed = True

这套引擎算不上漂亮,但足够稳定,处理几百个节点的单张图毫无压力,渲染时间都在毫秒级。

2.3 层次三:渲染适配层,一套图模型多种输出

diagram-design 最让我得意的一层是渲染适配层。它让同一个图模型可以渲染成 SVG、PNG 甚至 HTML 片段。

核心思路是渲染器只消费"坐标+种类+文本"的图模型,不关心 DSL 图层的事。我封装了一个简单的注册机制:

RENDERERS = {} def register_renderer(name): def deco(cls): RENDERERS[name] = cls return cls return deco @register_renderer("svg") class SvgRenderer: def render(self, model): ... @register_renderer("html") class HtmlRenderer: def render(self, model): ...

这个设计带来的直接好处是:将来想换前端框架、调整图标风格、输出不同格式,都不需要碰 DSL 和布局逻辑。后来我们真的为某个交付项目定制过一套深色主题渲染器,整个改动只发生在渲染层,没有侵入核心代码。

层次划分带来的维护性,在这场改造里体现得淋漓尽致。

2.4 层次四:组件库与样式规范,解决"千人千图"

第四层负责视觉规范统一。diagram-design 内置了一组基础组件:服务节点、外部系统、消息队列、数据库、人员角色等。每个组件的形状、配色、线型都由主题变量控制。

主题变量长这样:

theme: palette: primary: "#4f6df5" success: "#2ecc8f" warning: "#ff9f43" danger: "#ee5253" shape: service: "rounded-rect" external: "rect" queue: "cylinder" database: "cylinder" stroke: width: 2 style: "solid"

组件级复用带来一个隐性收益:团队讨论图表的单元从"这张图好不好看"变成"这个节点该用哪种组件表达"。前者是审美分歧,没有标准答案;后者是语义共识,可以被讨论和统一。语义一致后,整份文档的图表体系自然就有了系统性。

3. 三张真实场景的高频图,从零到交付的完整拆解

有了框架,接下来用三张真实项目中高频出现的图来演示完整闭环。这三张图代表了三种完全不同的难点。

3.1 系统架构图:不光画关系,还要表达"演进方向"

架构图最忌讳的是一张图塞下所有东西。diagram-design 的做法是用分层表达抽象级别,用虚线表达演进路径。

以会员中台为例,我先定义领域模块与依赖关系:

diagram member_platform { layer "接入层" { component gateway as "接入网关" component auth as "认证中心" } layer "业务层" { component profile as "会员档案" component voucher as "权益中心" component growth as "成长体系" } layer "存储层" { component mysql as "MySQL 集群" component redis as "Redis 集群" } gateway -> auth: "鉴权" gateway -> profile: "查询档案" profile -> mysql: "读写" growth -> redis: "缓存" voucher -> growth: "触发成长值" }

通过layer命令把节点分组,布局引擎自动把图层铺开。相比手工排版,这节省了至少七成的时间。

架构图有个手艺活:表达未来演进。我的做法是不把演进线跟现有依赖线混在一起,而是用独立的evolution区块描述,渲染成特定颜色的虚线。比如规划中的推荐服务,就用虚线连向会员标签中心,提醒所有人"这条路的连接器还没焊死"。

3.2 时序图:脚本方案完胜拖拽画图的一个场景

时序图是最适合脚本化的图类型,没有之一。手工画时序图要反复调整生命线高度、消息箭头位置,稍有改动画半小时。脚本化之后,改动成本几乎为零。

以一次退款请求为例:

diagram refund_flow { actor user as "用户" participant app as "App" participant trade as "交易中心" participant pay as "支付网关" participant ledger as "账务系统" user -> app: "申请退款" app -> trade: "退款申请" trade -> pay: "发起原路退回" pay --> trade: "受理成功" trade -> ledger: "登记冲正" ledger --> trade: "冲正完成" trade --> app: "退款进度" app --> user: "结果通知" }

时序图脚本化的红利比架构图还明显,因为时序图的信息密度高、改动力度频繁,往往一次接口调整整张图要重画。现在只需要增删几行文本。

这类图有一处细节容易翻车:消息返回用虚线,异步消息用半边箭头号。这些语义被直接写进 DSL 语法里,从源头保证规范,而不是靠画图人自觉。

3.3 部署拓扑图:环境差异怎么用同一份源图表达

第三类是部署拓扑图,难点不在画,而在一张源图怎么在不同环境复现。我把环境差异抽到了配置层,DSL 只描述部署单元及其连接关系。

示意如下:

diagram deploy_topo { env "staging" { node k8s as "K8s 集群" node redis as "Redis" node mq as "MQ 集群" service "app-server" -> redis service "app-server" -> mq } }

这样设计后,生产环境只需要切换env参数,同一份 DSL 就能基于不同环境配置渲染出不同的拓扑图。既不破坏单份源图的唯一性,也避免了为每个环境维护三份几乎一样的文件。

这个模式后来也被我应用到了配置审计场景——让部署图与 CMDB 数据源联动,节点状态直接从配置读取,图从"静态资产"变成了"可校验的实时视图"。

4. 团队落地与协作流程:从"画图工具"到"评审基建"

diagram-design 真正进入团队协作层面后,挑战不再是技术问题,而是习惯和流程。这阶段我积累了不少教训,每一条都来自实际摩擦。

4.1 存量图表迁移的三个阶段

团队里存量的图表不是一张两张,迁移时必须分阶段,不能搞一刀切。

第一阶段只迁移"活文档"——还在持续迭代的设计图、接口调用链图。死文档直接归档,不再迁移。这一步能立刻让活跃交付物进入版本管控。

第二阶段补"空白地带"——系统梳理后发现有些核心链路一直没有图。用脚本化补齐后,很多隐藏的重复建设被直接暴露出来。

第三阶段才是翻新存量旧图——只挑仍被高频引用的,小图随手重画,大图拆分模块。整个过程不追求一口气清零,但每个迭代周期都会把存量图数量往下压,直到稳定进入"新图全脚本化,旧图不再增加"的稳态。

4.2 图表的代码审查怎么落地

图表代码审查的机制,跟代码审查一样:让图表的可读性成为可讨论的显性问题。

我在团队里定了三条基本线:

  • 图里不出现跨三层以上的依赖。出现时,意味着模块划分已经有问题,图只是负责把问题暴露出来。
  • 一张图只讲一件事。既画流程又画部署还画组织架构的"四不像图",必须拆。
  • 节点命名必须来自真实代码模块名。不允许为了图好看而用书面别名,否则图与代码的对应关系就断裂了。

这些线在执行时不需要复杂规则,审查时照着这三条逐图过一遍即可。久而久之,大家画图的初始质量会迅速提高,因为评审反馈足够快。

4.3 自动化校验,把"图对不对"从人工判断变成机器判断

我对 diagram-design 比较满意的一个增强功能是配置了自动校验脚本。它在 CI 里对每个图做三件事:

  1. 死链检查:图中每个节点在系统模块清单里必须存在,避免文档图指向不存在的模块。
  2. 孤立节点检查:不允许出现既没有被依赖也不依赖别人的节点,这个节点如果存在于真实系统,那就是需要找负责人的"幽灵模块";如果不存在,那就是图里的多余要素。
  3. 层级深度检查:任一链路跨层超过预设阈值时,构建直接失败,并给出告警。

自动校验的价值在于把图的质量门槛从"评审人经验"转移成了"可执行的客观标准"。每次变更提交,构建没过就知道哪里有问题,不需要等到评审会再花人力去发现。

4.4 最容易被忽视的:让"不想学"的人先获得甜头

接入过程里,工程惯性是最难克服的部分。一直画传统图的同事,短期不会因为"版本可 trace"这种长期价值而兴奋。一定要给他们短期的、看得见的甜头。

我的做法是建立了一个"常用图模板库",把团队最高频的几十种图全部预制成模板。想画某种服务依赖图时,直接复制模板,改几个节点名,渲染出来的图就比手画的漂亮且统一。这叫"让人先用起来,再理解规范"。

有个之前很抗拒脚本化的同事,后来自己偷偷用模板补回来了攒了半年的所有图。他没说什么,但下一个迭代的需求图全是用 diagram-design 画的。我觉得这就是最好的正向反馈。

5. 写在最后:一点关于选择的个人看法

diagram-design 这个名字起初只是个临时目录名,后来不知不觉成了团队文档规范体系里不可分割的一部分。我现在最深的体会是,好的图表工具不是帮你画得更快,而是帮你把思考过程显性化、把协作摩擦前置化。图的源码化带来了争议、审查、修改成本,但正是这些"麻烦",逼着整个团队每次画图时都想清楚:这个图到底想表达什么,是不是该画成图。

如果你也有几十上百张存量图表需要维护,或者团队文档里的架构图总被质疑"这是新的还是旧的",我建议不要急着找一个全功能的画图软件,而是认真考虑让图表进入你的代码仓库。刚开始的几天会觉得别扭,跑通一个迭代之后,你就再也不想回去手动拖拽了。

最后分享一个小技巧:即便你不在团队协作场景,也可以把个人项目里最常用的三种图(模块依赖图、时序图、部署拓扑图)先脚本化。坚持一个季度后再看,你会发现自己对系统结构的直觉变得比从前准了很多,因为你在"写图"而不是"画图"的每一天,都在强迫自己精确地理解系统。就是这种笨功夫,最终会让你的长期效率远超那些只靠手感拖框图的人。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进