ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

程序框图绘制指南:符号规范、循环画法与环形复杂度

程序框图绘制指南:符号规范、循环画法与环形复杂度

上周帮一位同学看他写的选课辅助脚本,两百多行挤在一个函数里,四个条件判断套着两个循环,我盯了十几分钟才在脑子里把执行路径捋顺。他自己也说不清某个分支到底在什么情况下会走到,因为写的时候是边想边改的。这种事见得太多了,问题根本不在代码写得好不好,而在动手写之前有没有把逻辑落在纸上。程序框图,也就是大家常说的流程图,就是干这件事的——把一段程序的执行顺序、判断条件、循环出口用方框、菱形和箭头画出来,让脑子里的想法变成别人也能看懂的东西。它适合所有写代码的人:新手用它理清思路,避免写出绕不出来的嵌套;老手用它做设计评审和交接文档;哪怕不写代码的产品、测试同学,看懂流程图也能快速判断一段逻辑有没有漏洞。下面我拿一个完整的例子,把从需求到成图的整个过程拆一遍,顺带把符号规范、循环画法和踩过的坑都讲透。

1. 动手之前:先搞清楚这张图是给谁看的

很多人一上手就打开绘图工具拖方框,拖到一半发现结构不对,又全部推倒重来。我在实际项目里养成一个习惯:落笔前先问自己一句,这张图最终会出现在谁的面前。这个问题的答案直接决定你用多粗的笔、写多细的节点、要不要保留变量名。

1.1 三种用途对应三种截然不同的画法

给自己理思路用的草图,我从来不管美观。随手抓张纸,方框画得歪七扭八,箭头甚至会画到纸的边角再折回来。这种图的作用是逼自己把“如果……否则……”完整地想一遍,画完就可以扔。判断标准只有一个:盖上代码,你能不能照着这张图把逻辑重新讲一遍。讲得通就说明想清楚了,讲不通说明某个分支你自己也没搞明白。

给同事做设计评审或者代码交接用的图,规范就必须守住。节点里的文字要能独立读懂,不能出现i、tmp、flag这种只有作者自己明白的命名;判断框的两个出口必须明确标注“是/否”或者“真/假”;每一个循环都要让人一眼看出入口条件和退出条件。这类图我通常会保留真实变量名,因为它最终要跟代码对照着看,用score比用“分数”更省事。

给业务方、测试同学或者非技术背景的合作者看的图,要做一次“翻译”。把if (retryCount >= maxRetry)这种表达换成“重试次数已经用完了吗”,把技术性的中间变量全部藏起来,只留业务语义。这类图层级不能深,超过三层嵌套就拆成两张图,否则对方看到第三层就已经放弃阅读了。

用途精度要求命名方式是否需要存档常见问题
自己理思路低,能看懂就行随意外号否画完就忘,回头找不到
设计评审 / 交接高,需与代码一致真实变量名是命名随意导致无人能懂
业务 / 测试沟通中,逻辑完整业务语言是层级过深,读者放弃

1.2 为什么我坚持先画图再写代码

有个很直观的对比:需求变更的时候,改一张流程图平均五分钟,改一段已经写好的嵌套代码平均两小时,还得重新跑一遍测试。图是逻辑的草稿纸,草稿纸上改一笔几乎没有成本,代码里改一笔要考虑变量作用域、边界值、已有的测试用例会不会挂掉。

更关键的是,画图这个过程本身会暴露你“没想到的地方”。我印象最深的一次是做一个批量文件的处理脚本,思路很清楚:读目录、逐个处理、写结果。伪代码三行就写完了。可当我真的去画框图,画到“逐个处理”这个框的时候卡住了——如果中间某一个文件处理失败,是跳过继续,还是整体中断?如果全部失败,要不要回滚已经写出去的结果?这些问题在写伪代码的时候被“逐行处理”四个字完美地掩盖了,只有当你必须把它拆成一个个方框和判断菱形时,它们才会跳出来逼你回答。

所以我的建议很实在:任何超过三十行、带两个以上分支的函数,动键盘之前先在纸上或白板上把框图撸一遍。这个投入产出比高得离谱。

2. 符号规范:六个框加两条线,真的够用了

初学画框图的人容易走两个极端。一种是觉得符号越多越专业,把各种特殊图形全用上,结果图上一半的符号连自己都要查手册;另一种是完全不管规范,判断和赋值全用矩形,看图的人得靠猜。规范这东西的意义不在于好看,而在于消除歧义——同一个图形在全世界的解读应该是一致的。

2.1 六种基本图形与它们的语义边界

真正高频使用的图形就那么几个,我把它们和最容易混淆的地方整理在一起。

图形名称表示什么最常见的误用
圆角矩形 / 椭圆起止框流程的开始与结束一张图里出现多个入口
矩形处理框赋值、计算、函数调用把判断条件塞进矩形里
菱形判断框条件分支只画一个出口,或出口不标注
平行四边形输入输出框读数据、打印、返回与处理框混用,不区分内外
双边矩形子程序框调用另一个已定义的流程直接展开,导致主图爆炸
小圆圈连接符跨页或跨区域的断点续接只画一半,找不到对应点

这里有个特别值得说的细节:很多人分不清“处理框”和“输入输出框”。判断标准其实很干脆——这个动作有没有和外部的数据源发生交换。读文件、收用户输入、打印结果、向接口发请求,这些都属于输入输出,用平行四边形;纯粹在内存里算,比如sum = sum + i、字符串拼接、格式化,那才是处理框。严格区分的好处是,当你要把这段逻辑改成从数据库读而不是从文件读时,一眼就能在图里定位到要改的那几个框。

2.2 连线和出口标注的三条硬规矩

第一条,判断框必须有两个或两个以上的出口,而且每一个出口都要标注条件。我见过太多只画一个出口的图,读者根本不知道“不满足”的时候往哪走。标注用“是/否”最省事,条件复杂的时候直接写具体的判断结果,比如“余额充足 / 余额不足”,比写“是/否”更不容易看错。

第二条,主干方向自上而下、自左向右,箭头在主干上可以省略,非主干的回连和横连必须画箭头。这条规矩的目的是让读者的眼睛有固定的扫描路径。一旦你开始在下半部分画向上指的箭头,就说明这里是一个循环回边,必须画清楚,不能省。

第三条,一条连线不能同时承载两种含义。有些图为了省地方,让一条箭头从判断框出来之后又分叉,这在规范里是不允许的——分叉必须经过一个新的判断框,否则逻辑上就说不清是哪条路径被选中了。

2.3 图太大怎么办:分层与连接符

一张图超过十五个框,可读性就开始崩塌。我处理的办法是“主图 + 子图”两层结构。主图里只保留主干流程和高层的判断,遇到需要展开的复杂段落,画一个双边矩形,写上子流程的名字,另起一张纸画它的内部。这样主图永远能在一屏内看完,需要抠细节的时候再翻到子图。

跨页或者跨区域的时候用连接符,规则是成对出现、编号对应。画一个小圆圈写A,另一处再画一个圆圈写A,读者就知道流程从这跳到那。这里有个小坑:连接符只解决“位置不够”的问题,不解决“逻辑混乱”的问题。如果一张图里出现七八个连接符来回跳,那说明结构本身该拆了,靠连接符硬凑只会让图更难读。

3. 完整例子:成绩等级判定,从需求到成图

理论说再多不如完整走一遍。我选一个足够简单但分支够多的例子——根据分数输出等级,同时处理非法输入。这个例子麻雀虽小,判断、分支、合并、异常处理全都有。

3.1 先把边界钉死,再谈怎么画

需求描述通常是模糊的:“输入分数,输出对应等级”。这句话里有三个必须提前确认的点:分数的合法范围是什么,边界值归哪一档,非法输入怎么处理。

我的习惯是先列一张边界表,把所有可能的取值区间穷举出来。这一步花不了五分钟,但能省掉后面反复改图的时间。

输入范围输出说明
小于 0 或大于 100数据非法越界,直接拒绝
90 到 100A含 90
80 到 89B含 80
60 到 79C含 60
0 到 59D含 0

注意“含”与“不含”必须写清楚。如果需求里没说,我就按最常见的左闭右开或者整数分档处理,并且在图的注释里写明,避免以后有人拿 89.5 来问你为什么算 B。

3.2 伪代码是个不可或缺的中间态

从需求直接跳到画图,中间容易断片。我的做法是先写一段极简伪代码,把顺序和判断关系定下来,再把它“翻译”成图形。伪代码不需要严谨,能表达清楚分支走向就行。

score = 读取输入() if score < 0 或 score > 100: 输出("数据非法") elif score >= 90: 输出("A") elif score >= 80: 输出("B") elif score >= 60: 输出("C") else: 输出("D")

这段伪代码有个容易被忽略的细节:判断是从高到低依次进行的,后面的条件实际上隐含了“前面的条件都不满足”。比如走到score >= 80这一步时,已经确定分数不超过 89,所以不需要再写score >= 80 且 score < 90。这个隐含关系在代码里靠elif保证,但在框图里必须体现出来——每个菱形的“否”出口连向下一个菱形,“是”出口直接连到输出框。画图的时候如果不注意这点,很容易把条件写成并列的四个菱形,读者就得自己去做交集运算,这就是典型的“图画了但没画明白”。

3.3 落笔:主干、分支、异常的先后顺序

我画图的固定顺序是:先画主干,再补分支,最后补异常。主干就是最顺的那条路——输入、判断、输出、结束。先把这条线拉直,图就有了骨架。

下面是我用纯文本画的示意图,实际动手时用纸笔或者绘图工具都行,结构是一样的:

┌──────────┐ │ 开始 │ └────┬─────┘ ↓ ┌───────────────┐ │ 输入 score │ └───────┬───────┘ ↓ ╱─────────╲ ╱ score<0 ╲ ╱ 或 >100 ? ╲──── 是 ──→ ┌────────────┐ ╲ ╱ │ 输出"非法" │ ╲───────────╱ └──────┬─────┘ │ 否 │ ↓ │ ╱─────────╲ │ ╱ score>=90 ╲─── 是 ──→ ┌──────┐ │ ╲ ╱ │ 输出A│ │ ╲─────────╱ └──┬───┘ │ │ 否 │ │ ↓ │ │ ╱─────────╲ │ │ ╱ score>=80 ╲─── 是 ──→ ┌──────┐ │ ╲ ╱ │ 输出B│ │ ╲─────────╱ └──┬───┘ │ │ 否 │ │ ↓ │ │ ╱─────────╲ │ │ ╱ score>=60 ╲─── 是 ──→ ┌──────┐ │ ╲ ╱ │ 输出C│ │ ╲─────────╱ └──┬───┘ │ │ 否 │ │ ↓ │ │ ┌─────────┐ │ │ │ 输出 D │ │ │ └────┬────┘ │ │ │ │ │ └────────┬──────────┴─────┘ ↓ ┌──────────┐ │ 结束 │ └──────────┘

画到这里有两个地方要注意。第一,所有输出框最终必须汇聚到一个结束框,不能出现“输出 A 之后流程就断了”的情况,那意味着这段逻辑没有归宿,读者会怀疑是不是漏画了。第二,异常分支要先于正常分支判断,这一点在图上体现为越界判断放在最上面。顺序反了的话,负数分数会一路掉到“输出 D”,这就成了逻辑错误,而不仅仅是画法问题。

3.4 用环形复杂度给图做一次“体检”

图画完了,怎么判断它复杂不复杂、测试要覆盖多少条路径?这里有个很实用的量化指标——环形复杂度。它的算法有两种,结果一样:一是数边的数量减去节点数量再加二,二是判定节点的数量加一。

拿上面这张图来数。节点一共 12 个:开始、输入、四个判断、五个输出(非法、A、B、C、D)、结束。边一共 15 条。代入公式得到 15 − 12 + 2 = 5。用判定节点数验证:四个判断加一,同样是 5。两个结果吻合,说明图画得是自洽的。

这个 5 意味着什么?意味着这段逻辑至少有 5 条互相独立的执行路径,测试的时候至少要设计 5 个用例才能把它们全覆盖到:一个越界值、一个 95、一个 85、一个 70、一个 30。如果你画的图算出来是 12,那就要掂量一下是不是拆得不够细,是不是该把某一段抽成子流程了。我个人经验是,单个函数的框图环形复杂度超过 10,就该考虑重构,因为对应的代码大概率已经难以维护。

4. 循环最容易画错:三类翻车现场

判断分支画错了顶多逻辑不对,循环画错了整张图的可读性会直接归零。我自己和身边同事踩过的坑,基本都能归到下面三类。

4.1 判断在入口还是在出口,差别是“可能一次都不执行”

有一种循环是先判断再执行,条件不满足就一次都不进;另一种是先执行再判断,至少会跑一次。这两者在图上长得完全不一样,但很多人在画的时候没意识到要区分。

前者的画法是:判断菱形放在循环体的上方,“是”出口向下进入循环体,循环体末尾用一条回边连回菱形上方。后者的画法是:循环体先画,末尾接判断菱形,“继续”的回边指回循环体开头,“退出”连向后续流程。如果你去看别人的图,发现判断菱形在下面,那说明这段逻辑至少会执行一次——如果你本来想要的是“可能一次都不执行”,那图就画错了,代码八成也写错了。

这个区别在真实场景里很要命。比如“逐条读取待处理任务”,如果队列是空的,正确的行为应该是一次都不进循环直接结束。这时候判断必须在入口。反过来,“先执行一次再决定是否重试”这种场景,判断就得放在出口。

4.2 循环变量的更新必须画出来,不能省

这是新手最常犯的错:循环体里只画了“做事”的框,忘了画“计数器加一”或者“指针后移”的框。图的表面上看是有回边的,逻辑上似乎能绕回来,但少了这个更新步骤,回边就变成了一个永远满足条件的死循环。

我见过一张图,累加求和的循环里画了“sum 加上当前项”,回边直接连回判断框,唯独漏了“下标加一”。看图的人第一反应是这段代码会死循环,第二反应是作者可能压根没意识到自己漏了。这种图比不画图还糟糕,因为它传递的是错误信息。

所以我的检查习惯是:每画完一个循环,先在脑子里空跑两轮,确认第二轮进入时的状态和第一轮不一样。如果两轮状态完全一样,那这个循环就是死的。

4.3 嵌套循环和提前退出,别画成意大利面

两层循环叠在一起,如果每一层都画完整的判断和回边,图形会迅速变成一团乱麻。我的处理办法是分两张图:外层循环一张,内层抽成子流程,主图里只留一个双边矩形,标上“处理单行数据”。这样读者先理解整体节奏,需要细节的时候再翻子图。

提前退出是另一个麻烦点。在循环体中间用break跳出,在代码里只占一行,但在图上意味着一条从深层直接连到循环外部的长箭头,这条箭头往往要横穿大半个图,非常难看。规范的做法是用标志位:在循环条件里加一个“未找到”的判断,找到之后把标志位置为真,循环自然结束,后续流程再根据标志位决定走哪条路。这样图上不会出现跨越层级的箭头,逻辑也更容易被测试覆盖。

5. 工具怎么选:手绘、拖拽、文本生成

工具这件事没有标准答案,关键在于你处在流程的哪个阶段。我把常用的三类工具按使用场景排了个序,你可以对照自己的情况挑。

5.1 三类工具的适用场景对比

类型上手成本修改成本版本管理适合场景
纸笔 / 白板极低高,改一处可能要重画无讨论阶段、快速验证思路
拖拽式绘图中,需熟悉对齐和连线中,挪动方框会牵动连线弱,靠文件另存对外交付、汇报材料
文本描述生成中,需要记语法极低,改一行字即可强,可与代码同库存放长期维护的设计文档

拖拽式绘图工具最大的痛点是“挪一个框,五条线全歪”。如果你的图需要反复迭代,这个成本会累积得很快。文本描述生成的思路正好相反——你用几行文字定义节点和连线,工具负责排版,改动的时候只改文字,布局自动重排。它的短板是初期要花时间熟悉语法,而且复杂布局的精细控制不如手动拖拽。

5.2 我日常的组合拳

实际工作中我是这么搭配的:讨论阶段直接在纸上或白板上画,怎么快怎么来,画完拍照留存;确定方案之后,用文本描述的方式把结构重新写一遍,扔进代码仓库里跟源码放在一起;需要给非技术同事看的时候,再从同一份文本导出成图片,或者手动整理一版更美观的。

文本描述的好处在于,它能进版本管理。改了什么、谁改的、什么时候改的,一清二楚。而拖拽出来的图片文件,本质上是二进制,两个人同时改就冲突,历史版本也没法对比。我现在的习惯是,凡是会长期存在的设计文档,一律用文本描述维护,只在最终交付时导出一份图片。

下面是一段文本描述的写法示例,各家工具的语法细节不同,但思想一致:先声明节点,再声明连线,连线上的标注写成标签。

节点定义: start = 开始 input = 输入数据 check = 数据合法吗 err = 输出错误提示 handle = 执行处理 end = 结束 连线定义: start -> input input -> check check -> err [标签: 否] check -> handle [标签: 是] err -> end handle -> end

这份描述只有十几行,但已经把结构说清楚了,而且改动起来毫无压力——加一个分支就是在末尾补两行,不用去拖方框。

5.3 出图前的五项自检

不管用什么工具,图出完我都要过一遍清单,这五条能拦住八成的低级错误:

  • 起止框是否唯一。一张图只能有一个入口,结束框可以有多条线汇入,但入口多了读者就不知道该从哪看起。
  • 每个判断框的两个出口是否都标了条件。空着的出口等于没画。
  • 是否存在走不到结束的路径。逐条路径扫一遍,每条都得有归宿。
  • 是否有没有任何连线的孤立节点。通常是画到一半改了结构留下的残余。
  • 图里的变量名和代码里的变量名是否一致。不一致的话,这张图在评审时基本废掉一半。

6. 常见问题速查与排查思路

最后把我这些年被问得最多、以及自己踩过的坑整理成一份速查表。遇到问题的时候按症状对号入座,通常能快速定位。

6.1 症状对照表

症状大概率原因处理办法
图看着很满但说不清逻辑判断条件写成了并列,没有体现先后顺序把并列的判断改成一串级联的菱形
别人问“这种情况走哪”答不上来某个分支的出口没有标注补全每个出口的条件文字
图越画越大,一屏放不下没有做子流程拆分把超过八个框的段落抽成子图
循环看起来永远出不来漏画了循环变量更新或退出条件在循环体末尾补上更新框
回边箭头横穿整张图用了跨层跳出的写法改成标志位控制,让循环自然结束
代码和图对不上图是早期版本,代码改过没同步把图放进仓库,和代码一起改
同一张图里存在两种画法风格多人协作拼凑统一对齐方向和标注习惯后再合图

6.2 几个不太写在手册里的小技巧

第一,画图之前先数分支。把所有的判断条件列成一张清单,数一数有几个,心里就有底了。分支超过六个的流程,我基本会先考虑能不能用查表代替一长串判断——把“如果 A 输出 1,如果 B 输出 2”这种结构改成一张映射表加一次查询,图画出来会清爽一个量级。

第二,判断框里的条件尽量写成肯定句。写“是否合法”比写“是否不合法”好读,因为读者不需要在心里做一次取反。这个细节看起来无关紧要,但在一张有十几个判断的图里,累积起来的认知负担相当可观。

第三,凡是涉及数值区间,一律在图上标注端点归属。写“60 到 79”是不够的,要写“60 ≤ x < 80”,或者至少在注释里说明。我吃过这个亏,图和代码对边界理解不一致,测试提了个边界值的 bug 才发现是图画得含糊,代码照着含糊的图写,自然也含糊。

第四,给图配上版本说明。哪怕只是一行“v2,新增越界校验”,也能让半年后的自己少花十分钟回忆当初为什么这么改。

第五,别在图上写代码。节点里写score >= 90 && score <= 100这种表达式,图就退化成代码的另一种排版形式了,失去了“用图形降低理解门槛”的意义。节点里写人能读懂的判断,复杂表达式放在注释或者旁边的说明里。

我个人的体会是,画程序框图这件事的价值不在图本身,而在画的那个过程。图是给别人看的,思考是给自己留的。很多人觉得画图费时间,实际上是省下了后面排查和交接的时间,只是这笔账要过一阵子才能算出来。等到哪天你需要接手一段三个月前自己写的、只有图没有注释的逻辑,就能明白当初那五分钟花得有多值。

RELATED READING

延伸阅读

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