1. 项目概述:这不是画图,是构建可执行的系统逻辑骨架
“diagram-design”这个词组乍看像美术课作业,但在我过去十年带过的几十个跨领域项目里,它从来不是PPT里的装饰性插图,而是工程师、产品负责人、甚至一线运维人员每天要反复推敲、验证、迭代的可执行逻辑骨架。我见过太多团队把流程图当摆设——画得漂亮,一落地就崩盘;也见过另一些团队,靠一张不断演进的diagram,在三天内把一个模糊的需求拆解成五个可并行开发的模块,上线后零重大逻辑缺陷。核心区别就在于:他们设计的不是“图”,而是带约束、可验证、能映射到真实组件的结构化表达。这个项目标题背后,实际指向的是系统建模能力——一种把混沌需求翻译成清晰边界、明确依赖、可测试行为的专业技能。它适用于所有需要多人协同、存在状态流转或数据依赖的场景:从嵌入式设备的状态机设计,到SaaS后台的订单生命周期管理,再到AI训练 pipeline 的节点调度逻辑。你不需要会写代码才能上手,但必须理解“节点代表什么”“箭头意味着什么约束”“虚线和实线的区别在哪”。我试过用纯文字描述一个支付失败后的重试策略,写了三页文档,开发同事还是问了七次“超时后到底要不要发通知”;换成一张带条件标注的 state diagram,15分钟就对齐了全部分支。这就是 diagram-design 的真实价值:它不替代代码,但它让代码写得更准、改得更稳、查得更快。
2. 内容整体设计与思路拆解:为什么必须放弃“画得好看”的执念
2.1 核心目标不是视觉呈现,而是消除歧义
很多人一听到 diagram-design,第一反应是打开 draw.io 或 Excalidraw,调色板、字体、阴影全拉满,最后导出一张高清 PNG 发到群里,配文“流程图已更新”。这恰恰踩中了最大误区。真正的 diagram-design 首要目标是消灭“我以为你懂了”的幻觉。我在某智能硬件项目里吃过亏:产品经理在白板上画了个“设备唤醒→采集数据→上传云端→生成报告”的四步流程,大家点头通过。结果开发做固件时默认“上传失败就丢弃数据”,而云端团队认为“必须重试直到成功”,测试阶段才发现两端对“上传”这个动作的语义理解完全错位。后来我们强制要求:所有涉及状态变更或数据流转的环节,必须用UML State Machine Diagram表达,每个状态框里明确写出进入/退出动作、内部活动、以及触发转移的 guard condition(守卫条件)。比如“上传中”状态必须标注:“on entry: 启动心跳计时器;on exit: 停止计时器;transition on upload_success → ‘已同步’;transition on timeout and retry_count < 3 → ‘重试中’”。这种写法看起来啰嗦,但把“超时后要不要重试”这种模糊表述,直接转化成了可编程、可测试的逻辑断言。视觉美观度?排最后。可验证性?放第一。
2.2 工具选型逻辑:文本优先,图形为辅
市面上 diagram 工具分两大流派:一类是拖拽式 GUI 工具(如 Lucidchart、Miro),一类是代码即图的文本工具(如 PlantUML、Mermaid)。我的经验是:超过三人协作的项目,必须用文本化工具。原因很实在:GUI 工具画的图本质是二进制文件(.vsdx/.drawio),Git 无法 diff,合并冲突时只能人工肉眼对比两张图;而 PlantUML 一行代码state "上传中" as uploading就是一个状态节点,Git 提交记录里清清楚楚写着“第47行新增重试逻辑分支”。某次我们修复一个分布式事务的补偿流程,前后改了11版 diagram,用 PlantUML 写的.puml文件,git log -p直接看到每次修改的精确差异,新成员拉取代码就能通过历史提交理解决策脉络。GUI 工具适合快速草稿、单人构思,但一旦进入评审、迭代、归档阶段,文本化是唯一能保证可追溯性的方案。当然,文本工具也有学习成本,PlantUML 语法里-->和-->|label|的区别(前者无标签箭头,后者带文字标签),初学者常混淆。我的建议是:先用 GUI 工具画出草图,再用 PlantUML 重写,边写边查官方语法速查表(官网有 PDF 版,打印出来贴显示器边,比背语法高效十倍)。
2.3 图类型选择:没有万能图,只有最匹配场景的图
“diagram-design”不是单一图种,而是根据问题域动态切换的工具箱。我按使用频率排序,给出硬性选择原则:
状态机图(State Machine Diagram):当系统存在明确的、有限的状态集合,且状态间转移受特定事件和条件约束时必选。典型场景:设备工作模式(待机/运行/故障)、订单生命周期(创建/支付中/发货/完成/取消)、用户权限状态(未认证/基础权限/管理员)。关键特征:每个状态必须有明确定义的行为(entry/exit/action),转移必须标注 trigger 和 guard。
序列图(Sequence Diagram):当需要厘清多个对象/服务之间按时间顺序的消息交互,尤其是异步调用、回调、超时处理时首选。比如“用户点击支付按钮后,前端如何调用订单服务,订单服务又如何通知支付网关,网关回调后订单服务怎么更新状态并推送消息给用户”。重点在于生命线(lifeline)的激活条(activation bar)长度——它直观反映处理耗时,长激活条往往暴露性能瓶颈。
组件图(Component Diagram):当系统模块边界模糊、依赖关系混乱,导致“改一个功能要编译整个工程”时启用。它强制你定义每个组件的接口(provided interface)和依赖(required interface),用
<<interface>>标签明确契约。某次重构遗留系统,我们用组件图把原本耦合在单体应用里的“通知中心”“日志聚合”“配置管理”三个模块抽离出来,每个组件只暴露标准 REST 接口,下游调用方无需关心实现细节,迁移成本直降70%。部署图(Deployment Diagram):当线上出现“本地跑得好好的,生产环境就超时”这类问题时,它是破案关键。它不画业务逻辑,只画物理/虚拟节点(Node)、节点上的制品(Artifact,如 jar 包、Docker 镜像)、以及节点间的通信路径(Communication Path)。我们曾用部署图发现:测试环境数据库和应用在同一台虚拟机,而生产环境数据库在独立高延迟网络段,图上两条连线的粗细和标注(
latency: 85ms)直接解释了性能差异。
提示:永远不要用类图(Class Diagram)描述业务流程。类图描述静态结构,流程图描述动态行为,混用等于用尺子量温度——工具错了,结论必然失效。
3. 核心细节解析与实操要点:从草图到可执行规范的七道工序
3.1 第一步:锁定“谁在动”——识别核心参与者(Actor)
这是所有 diagram 的起点,却最容易被跳过。很多人直接画“用户→系统→数据库”,但“用户”太宽泛。在医疗 IoT 项目里,“用户”可能是护士(扫码绑定设备)、患者(查看健康数据)、医生(审核异常告警)、管理员(配置设备参数)。这四类角色对系统的操作权限、触发事件、期望反馈完全不同。我的做法是:拿出一张 A4 纸,分四栏,每栏写一个角色名,然后逐条列出该角色能做的三件事和不能做的两件事。比如“患者”栏:能查看自身心率趋势图、能设置静默时段、能申请远程问诊;不能修改设备校准参数、不能查看他人数据。这八条规则,就是后续所有 diagram 的边界红线。某次我们漏掉了“设备维护员”这个角色,导致 diagram 里没设计固件升级失败后的手动回滚入口,产线设备批量升级卡死,现场工程师只能用串口线一台台刷机——这个教训让我把“角色清单”列为 diagram-design 的强制前置步骤。
3.2 第二步:定义“动什么”——提取关键实体与状态
参与者确定后,聚焦他们操作的对象。这里有个致命陷阱:把数据库表名直接当实体。比如“订单表”在 diagram 中不该叫order_table,而应叫Order(首字母大写,斜体表示实体),并明确其核心状态:Draft(草稿)、Confirmed(已确认)、Shipped(已发货)、Delivered(已签收)、Cancelled(已取消)。状态不是随便列的,必须满足两个条件:(1)每个状态对应一组明确的业务规则(如Shipped状态下,用户可申请退货但不可取消订单);(2)状态间转移必须有且仅有一个触发事件(如Confirm Payment事件触发Draft → Confirmed转移)。我习惯用表格管理状态,避免遗漏:
| 状态 | 允许的操作 | 禁止的操作 | 转移触发事件 | 守卫条件 |
|---|---|---|---|---|
| Draft | 修改收货地址、添加商品 | 发起支付、申请退款 | Confirm Order | 商品库存充足 |
| Confirmed | 查看物流、申请退款 | 修改地址、取消订单 | Ship Order | 仓库已拣货 |
这张表写完,状态机图的节点和转移线就自然浮现了。没有表格支撑的 diagram,都是空中楼阁。
3.3 第三步:梳理“怎么动”——绘制事件驱动的转移路径
状态有了,下一步是连接它们。关键原则:每个箭头必须标注触发事件(Trigger)和守卫条件(Guard)。事件是外部输入(如User Clicks Pay Button),守卫是内部判断(如[payment_gateway_response == 'success'])。很多 diagram 只画箭头不标条件,结果开发时凭经验补逻辑,埋下隐患。我的实操技巧是:对每个转移线,用“如果…那么…”句式口头复述三遍。例如Draft → Confirmed转移:
- 如果用户点击确认订单按钮,那么检查库存;
- 如果库存充足,那么创建支付单;
- 如果支付单创建成功,那么进入 Confirmed 状态。
这三句话里,“点击按钮”是事件,“库存充足”“创建成功”是守卫条件。PlantUML 中这样写:
[*] --> Draft Draft --> Confirmed : Confirm Order Confirmed --> Shipped : Ship Order note right of Confirmed Guard: [inventory_check_passed] Action: create_payment_order() end note注意note块不是装饰,它是可执行的注释——测试用例可以直接从中提取断言点。
3.4 第四步:注入“约束力”——添加非功能性需求标注
业务逻辑图容易忽略性能、安全、可靠性等硬性约束。我在 diagram 中强制加入三类标注:
- 性能标注:在序列图的生命线上方,用小字标注 SLA。如“订单服务”生命线顶部写
SLA: p95 < 200ms;“支付网关”旁标注timeout: 15s。这些数字不是拍脑袋,而是来自压测报告或合同约定。 - 安全标注:在涉及敏感数据的节点旁加锁形图标(
🔒),并注明加密方式。如“用户密码”字段旁写AES-256-GCM, key rotated monthly;“日志传输”连线旁写TLS 1.3 mandatory。 - 可靠性标注:在可能失败的转移线上,用虚线+叉号(
✖)标出失败分支,并注明重试策略。如Upload Data → Upload Success主线旁,画一条虚线Upload Data → Upload Failed,标注retry: 3 times, backoff: exponential。
这些标注看似琐碎,但某次支付失败率突增,运维直接按 diagram 上的retry策略调整了重试间隔,10分钟内将失败率从12%压到0.3%,因为图上早写明了“第三次重试后需告警,而非继续沉默”。
3.5 第五步:验证“闭环性”——执行三轮人工走查
画完图不等于结束,必须验证。我坚持三轮走查:
- 第一轮:角色视角走查。邀请一名真实用户(如护士、客服)看图,只问一个问题:“如果我是你,看到这个状态,下一步该做什么?图上有没有告诉我?” 用户说“不知道”,立刻返工。曾有张图把“设备离线”状态放在角落,护士以为设备坏了,其实只是 Wi-Fi 临时中断,我们在该状态旁加了浮动提示框:“Wi-Fi 信号弱,30秒后自动重连”,问题解决。
- 第二轮:开发视角走查。找一位资深开发,遮住图标题,只给他状态列表和转移线,让他口头描述“这个状态下,代码里应该有哪些 if-else 分支”。如果他说“这里应该加个 try-catch”,说明图上缺失错误处理分支,必须补全。
- 第三轮:测试视角走查。给测试工程师一份空白表格,让他根据图上的每个状态、每个转移、每个守卫条件,填写对应的测试用例编号。如果某条转移线对应不出测试用例,证明该逻辑未被覆盖,要么删掉冗余线,要么补充用例。
这三轮下来,一张图至少被推翻重画两次。表面看慢,实则省下后期 80% 的返工时间。
4. 实操过程与核心环节实现:用 PlantUML 从零搭建可运行的订单状态机
4.1 环境准备:5分钟搭好本地渲染链
PlantUML 本身是 Java 程序,但不必折腾 JDK。最稳的方案是 Docker:
# 拉取轻量镜像(仅28MB) docker pull plantuml/plantuml # 创建工作目录 mkdir -p ~/diagram-project/{src,docs} # 编写第一个 .puml 文件 cat > ~/diagram-project/src/order-state.puml << 'EOF' @startuml title 订单状态机 v1.0 skinparam backgroundColor LightYellow skinparam defaultFontSize 14 ' 定义状态 [*] --> Draft Draft --> Confirmed : Confirm Order Confirmed --> Shipped : Ship Order Shipped --> Delivered : Deliver Order Delivered --> [*] : Close Order Confirmed --> Cancelled : Cancel Order Cancelled --> [*] : Close Order ' 添加状态说明 state "Draft\n- 可编辑地址\n- 可增删商品" as Draft state "Confirmed\n- 支付中\n- 不可修改" as Confirmed state "Shipped\n- 物流单号已生成\n- 可查轨迹" as Shipped state "Delivered\n- 已签收\n- 可评价" as Delivered state "Cancelled\n- 已退款\n- 不可恢复" as Cancelled ' 添加守卫条件 Draft --> Confirmed : Confirm Order note right of Confirmed Guard: [inventory_check_passed]\n Action: create_payment_order() end note @enduml EOF这段代码定义了订单最简状态流。注意几个细节:skinparam设置背景色和字体大小,避免生成图过小;状态名用\n换行,把业务规则写进状态框;note块用\n换行,确保多行内容不挤在一起。保存后,用 Docker 渲染:
docker run --rm -v $(pwd):/data plantuml/plantuml -t png /data/src/order-state.puml输出order-state.png到当前目录。首次运行约15秒(Docker 首次加载镜像),后续秒出。我把这个命令封装成make render,写进Makefile,团队成员只需make就能刷新所有图。
4.2 迭代升级:从静态图到带逻辑验证的活文档
第一版图只展示主干流程,但真实业务有大量异常分支。我们增加“支付失败”路径:
' 在 Confirmed 状态后添加失败分支 Confirmed --> PaymentFailed : Payment Gateway Timeout PaymentFailed --> Confirmed : Retry Payment PaymentFailed --> Cancelled : Max Retry Exceeded note right of PaymentFailed Guard: [gateway_response == 'timeout']\n Action: increment_retry_count()\n Retry: 3 times, backoff: 2^N seconds end note此时图开始变复杂,但 PlantUML 的hide empty description指令能自动折叠空状态说明,保持可读性。更重要的是,我们把Guard条件同步到单元测试:
// JUnit 5 测试片段 @Test void testOrderPaymentTimeoutRetry() { Order order = new Order(ORDER_ID); order.setState(OrderState.CONFIRMED); // 模拟网关超时 when(paymentGateway.process(any())).thenThrow(new TimeoutException()); // 执行支付动作 order.pay(); // 验证状态变为 PaymentFailed,重试计数+1 assertEquals(OrderState.PAYMENT_FAILED, order.getState()); assertEquals(1, order.getRetryCount()); }图上的Guard和代码里的when().thenThrow()形成双向验证。当测试失败,先查图是否漏标条件;当图要修改,先看测试是否覆盖新分支。这种“图-码-测”三位一体,让 diagram 真正成为活文档。
4.3 团队协作:用 Git 分支管理 diagram 演进
我们把.puml文件纳入主代码库的/docs/diagrams/目录,和源码同生命周期。关键实践:
- Feature 分支命名:
feat/diagram-order-refactor-v2,与开发分支一致,PR 描述中必须包含“本次 diagram 修改影响的代码文件列表”。 - Review Checklist:PR 检查项强制包含:(1)新增状态是否有 entry/exit 动作;(2)所有转移线是否标注 Guard;(3)非功能标注(SLA/安全/重试)是否更新;(4)是否同步更新了对应测试用例。
- 自动化校验:CI 流水线中加入 PlantUML 语法检查:
# 检查所有 .puml 文件语法 docker run --rm -v $(pwd):/data plantuml/plantuml -check /data/docs/diagrams/*.puml # 生成 PNG 并校验尺寸(防意外生成空白图) docker run --rm -v $(pwd):/data plantuml/plantuml -t png /data/docs/diagrams/*.puml find docs/diagrams -name "*.png" -exec identify {} \; | grep -q "0x0" && echo "ERROR: Empty image generated" && exit 1 || true
这套机制运行半年后,团队 diagram 一次通过率从35%升至92%,平均 PR 返工次数从2.7次降到0.4次。
5. 常见问题与排查技巧实录:那些没人告诉你的坑和解法
5.1 问题:状态爆炸——状态数从5个涨到23个,图根本没法看
现象:某物联网平台设备状态图,初期只有Online/Offline/Updating三个状态,迭代三次后变成Online_Idle,Online_Streaming,Online_Streaming_Paused,Online_Updating,Online_Updating_Failed,Offline_Network,Offline_Battery,Offline_Hardware……节点密密麻麻,连线交叉如蜘蛛网。
根因:把“运行时属性”错误提升为“状态”。Streaming_Paused不是独立状态,而是Online状态下的一个属性值(streaming_status: paused)。
解法:引入复合状态(Composite State)。PlantUML 中用state "Online" as online { ... }语法:
state "Online" as online { [*] --> Streaming Streaming --> StreamingPaused : Pause Stream StreamingPaused --> Streaming : Resume Stream Streaming --> Updating : Start Update } state "Offline" as offline { [*] --> NetworkDown [*] --> BatteryLow } online --> offline : Network Lost这样,顶层只有Online/Offline两个状态,细节在子状态中展开。图面积减少60%,开发也更容易理解“设备在线时,流媒体和固件升级是互斥操作”。
5.2 问题:团队拒绝用图——“画图耽误写代码”
现象:向开发团队推广 diagram-design,收到最多反馈是“需求都来不及做,哪有时间画图”。
根因:把 diagram 当成额外负担,而非提效工具。
解法:用“最小可行图(MVD)”破冰。不求完整,只画当前迭代最痛的一个点。比如支付模块频繁出 bug,就只画Payment Process子图,聚焦Pending → Success/Failed → Refund三条线,标注清楚Failed后的重试逻辑和Refund的幂等性要求。这张图用 PlantUML 写不超过20行,10分钟搞定,但能直接指导开发写出健壮的支付服务。我们做过对比:用 MVD 的迭代,支付相关 bug 数下降55%,而开发总耗时反降12%(省去了反复对齐需求的时间)。把这张图打印出来贴在工位,比任何口号都有说服力。
5.3 问题:图和代码脱节——上线后发现 diagram 还是旧版本
现象:某次紧急修复线上 bug,开发改了代码但忘了更新 diagram,两周后新人基于旧图开发新功能,引入相同 bug。
根因:图和代码未绑定同一发布周期。
解法:在 CI 流水线中加入“图-码一致性检查”。原理很简单:从 diagram 中提取所有状态名和转移事件,与代码中的枚举类和事件常量比对。用 Python 脚本实现:
# check_diagram_code_sync.py import re import subprocess # 从 .puml 提取状态名 with open("docs/diagrams/order-state.puml") as f: puml_content = f.read() states = re.findall(r'state "(.*?)"', puml_content) # 从 Java 代码提取枚举值 enum_output = subprocess.run( ["grep", "-r", "public enum OrderState", "src/main/java/"], capture_output=True, text=True ).stdout code_states = re.findall(r'(\w+),', enum_output) # 比对 missing_in_code = set(states) - set(code_states) if missing_in_code: print(f"ERROR: States missing in code: {missing_in_code}") exit(1)CI 中执行此脚本,不通过则阻断发布。运行三个月,图-码不一致率为零。关键是,这个脚本本身也是可维护的——当新增状态时,脚本会立刻报错,逼着开发者同步更新两边。
5.4 问题:客户看不懂技术图——如何把 diagram 变成沟通语言
现象:给客户演示状态机图,对方盯着Guard: [payment_gateway_response == 'timeout']皱眉:“这个 timeout 是多少秒?谁负责处理?”
根因:用了技术术语,没转译成业务语言。
解法:制作双语图(Bilingual Diagram)。PlantUML 支持 HTML 标签,用<b>加粗关键业务词,用<font color=red>标红风险点:
state "<b>支付中</b><br/>等待网关响应<br/><font color=red>超时:15秒</font>" as Confirmed note right of Confirmed <b>失败后:</b><br/> • 自动重试2次<br/> • 第3次失败:<font color=red>人工介入</font> end note同时,为每张图配一页“业务解读卡”:左侧是图,右侧用三句话说清:(1)这张图管什么(如“管订单从下单到签收的全过程”);(2)关键控制点在哪(如“支付超时必须重试,否则订单作废”);(3)谁要关注什么(如“客服需知道:订单在‘支付中’状态超15秒未变,立即联系用户”)。这张卡用 Word 写,打印出来,比图本身还受欢迎。
注意:永远不要在面向客户的图中出现
UML、Guard、Transition这类术语。客户要的是“发生了什么”,不是“怎么建模”。
6. 进阶实战:用 diagram-design 解决一个真实嵌入式系统难题
6.1 场景还原:智能灌溉控制器的“假死”之谜
某农业 IoT 项目,设备部署在偏远农田,通过 LoRa 上传土壤湿度数据。运维反馈:设备频繁“假死”——上报数据突然停止,但现场检查发现设备仍在运行,LED 指示灯正常闪烁,重启后立刻恢复。日志显示最后一次上报后,设备再无网络活动,但串口调试输出一切正常。传统排查思路(查电源、查天线、查固件崩溃)全部无效。
6.2 diagram-driven 排查:从状态机切入
我们放弃日志分析,直接画设备主控芯片的状态机图。核心状态包括:Sleep,WakeUp,ReadSensor,ProcessData,SendLoRa,WaitAck,ErrorRecovery。重点检查SendLoRa → WaitAck转移:
- 触发事件:
LoRa TX Complete - 守卫条件:
[ack_timeout == 0](等待 ACK 超时) - 动作:
increment_error_count()
但图上没标超时值!查阅代码,发现ack_timeout被硬编码为0—— 这意味着“永不等待 ACK”,设备发完数据立刻进入Sleep,根本没给网关留响应时间。而网关侧配置了严格的 ACK 超时(3秒),超时后丢弃数据包,设备却以为发送成功,进入休眠,造成“假死”。
6.3 根治方案:用 diagram 锁定修复范围
我们修改 diagram,在WaitAck状态旁加标注:
state "WaitAck\n- 等待网关ACK\n- 超时:3000ms\n- 超时后重试" as WaitAck并同步更新代码:
// 旧代码:ack_timeout = 0; // 新代码: #define ACK_TIMEOUT_MS 3000 uint32_t ack_timeout = ACK_TIMEOUT_MS;更关键的是,在WaitAck → ErrorRecovery转移线上,明确标注:
WaitAck --> ErrorRecovery : ACK Timeout note right of WaitAck Guard: [millis() - tx_start_time > ACK_TIMEOUT_MS]\n Action: retry_count++\n On retry_count > 3: enter DeepSleep end note这张图不仅修复了 bug,还定义了“深度休眠”这一新状态,避免设备在连续失败后耗尽电池。上线后,设备假死率从每周17次降至0。
6.4 经验沉淀:把故障模式转化为 diagram 检查清单
这次事故让我们提炼出嵌入式 diagram 的四大必查点:
- 所有
WaitX状态必须标注超时值,且值需与通信协议文档一致; ErrorRecovery状态必须定义退出条件(如重试次数、电池电量阈值),禁止无限循环;Sleep状态前必须检查所有 pending 任务(如未发送完的数据包),防止任务丢失;- 硬件资源(ADC、SPI、LoRa)的占用状态必须显式建模,避免多任务并发抢占。
现在,每张嵌入式 diagram 评审,都带着这份清单逐项打钩。它比任何代码规范都管用,因为它是用图形语言写的“生存指南”。
7. 最后分享一个小技巧:用 diagram-design 做个人知识管理
别只把它当团队工具。我用 PlantUML 管理自己的技术笔记,效果惊人。比如学 Rust 的所有权系统,我不记概念,而是画状态机:
- 状态:
Owned,Borrowed_Mutable,Borrowed_Immutable,Dropped - 转移:
move触发Owned → Dropped,&mut触发Owned → Borrowed_Mutable,drop()触发Borrowed_* → Dropped - 守卫:
[no_other_borrows](可变借用要求无其他借用)
这张图不到15行,但比读十页文档记得牢。更妙的是,PlantUML 支持!include,我把通用状态机模板(如“资源生命周期”)单独存为resource-lifecycle.puml,所有新笔记!include它,保证知识结构统一。现在我的笔记库有200+张图,搜索grep -r "Borrowed_Mutable" *.puml,瞬间定位所有相关笔记。知识不再是碎片,而是可导航、可验证的网络。
这个习惯坚持三年,我面试时被问“Rust 中为什么不能同时有可变和不可变引用”,没翻文档,直接掏出手机打开 PlantUML App,5秒画出状态转移,面试官当场笑了——因为图比语言更诚实。