拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

程序流图实战:从代码反推、ISO 5807画法到测试评审

程序流图实战:从代码反推、ISO 5807画法到测试评审

接手过一段两千多行的对账逻辑的人大概都有体会:变量名短得像密码,注释停在三个版本之前,唯一还能信的只有代码本身。我第一次遇到这种活儿,用的办法是拿一张 A3 纸把主流程画成程序流图,画到第三天,那个藏在双重循环里的状态机才露出真面目。程序流图不是什么高深的产物,它就是用图形把"代码先执行哪一步、什么条件下走哪条路"讲清楚的一种表达方式,控制流是它唯一的主角,数据怎么存、存在哪张表,反倒不是它关心的重点。

这篇内容想解决的是两件事:既能看懂别人给的流图,也能把自己手里的代码画成别人看得懂的流图。从符号规范、三种基本结构的画法、从代码反推图的完整流程,一直讲到工具选型和高频错误。刚入行的同学可以照着第四节的步骤一步步做,有几年经验的老手更建议直接跳到第六节的错法清单和第七节的落地习惯,那里面的坑我基本都踩过一遍。

1. 程序流图给谁看:受众决定了这张图画到什么颗粒度

很多人画流图的第一反应是打开工具就画,画完发现既不像给领导看的架构图,也不像给自己看的草稿,最后谁都不爱看。问题出在动笔之前没有确认读者。同一段代码,给不同的人看,图的颗粒度和侧重点完全不一样,这一步定错了,后面画得再工整也是白费。

1.1 三类读者,三种颗粒度

给自己梳理逻辑。这时候图是草稿,颗粒度可以细到每个赋值语句,框里直接写代码里的变量名,甚至可以省略图例和标题。我习惯用大白纸或者白板,画错了直接划掉,重点是把脑子的思路倒出来。这种图基本上不用留档,梳理完就该进垃圾桶,硬要放进文档库反而是噪音。

给接手代码的人看。这时候颗粒度要收到"一个框一件事",框里的文字用业务动词开头,比如"校验订单状态""写入支付流水",不要出现tmpflagret这类只有原作者才懂的命名。判断框的条件要写完整表达式,不能只写"判断一下"。这类图的合格标准是:对方不看代码,顺着图能复述出主干逻辑。

给测试和评审看。这时候图需要额外承担两个职责:一是每条分支路径都能被指出来,成为测试用例的来源;二是异常路径要和正常路径分开画,让评审的人一眼看出"哪条错误没人处理"。颗粒度可以比上一类略粗,但对分支的完整性和准确性要求最高,漏一条边界就是漏一个 bug。

读者颗粒度图形风格是否需要留档
自己细到语句手绘草稿不留
接手者一框一事规范符号+图例留,随代码提交
测试与评审分支完整主流程居中、异常靠右留,版本受控
产品与业务只到业务动作少用技术术语留,可单独版本

1.2 别把程序流图和数据流图、时序图混为一谈

这是新手最容易犯的错。有人拿了张数据流图过来,说"你按这个画流图",结果画出来的东西既不像数据流图也不像程序流图。区分它们的办法很简单,看这张图回答的是什么问题:

图种关注点回答的问题常用场景
程序流图控制流按什么顺序执行、走哪条分支代码梳理、测试设计
数据流图数据加工与流向数据从哪来、经过哪些加工、到哪去需求分析、系统设计
时序图对象间消息顺序谁在什么时刻调用谁接口交互、链路排查
状态图状态迁移什么事件驱动状态从 A 到 B订单状态机、设备控制
N-S 图结构化嵌套分支与循环怎么嵌套教学、结构化设计
活动图业务流程与并发业务怎么流转、哪里并行业务流程评审

我在实际项目里的做法是:入口处放一张活动图讲业务怎么走,具体到某个函数用程序流图讲代码怎么跑,接口调用顺序单独用时序图。三种图各司其职,不要试图用一张图画完所有事情,画到最后一定是一团乱麻。

提示:如果你画出来的图里大部分箭头在传数据而不是传控制,那你要的其实是数据流图,换一种画法会顺很多。

1.3 流图和代码注释的分工

我见过有人在每个处理框下面加三行注释,把它写成了一篇短文。流图的长处是"看见结构",注释的长处是"解释意图"。图里只写"做了什么",为什么这么做留在注释里。比如一个框写"重试三次",至于"为什么是三次而不是五次",写进代码注释或者文档,别塞进框里,否则框会越画越大,图会越来越读不下去。

2. 符号与规范:ISO 5807 里真正需要记牢的部分

程序流图的符号体系来自 ISO 5807(国内对应 GB 1526),标准里定义的图形有几十个,但日常画代码梳理,真正高频用到的就那么几个。我的建议是先把这五六个图形用熟,剩下的按需查表,别一开始就背一整套符号库,那样只会让动笔的阻力变大。

2.1 五个必须用对的图形

图形名称语义常对应的代码
圆角矩形或椭圆起止框流程的起点与终点函数入口、return
矩形处理框一段可执行的处理赋值、计算、调用
菱形判断框单入口、多出口的条件if、while、三元表达式
平行四边形输入输出框数据进入或离开系统读文件、调接口、打印
圆圈连接符跨页或跨区的接续跳转标签

另外几个按需使用的:六边形的准备框,用来表示循环初始化这类准备工作;上下带双线的矩形叫循环边界框,用来圈出循环体的范围,画多层嵌套循环时特别有用;右侧带波浪线的矩形是文档框,表示输出到文件或报表;虚线矩形是注释框,只做说明,不参与流程。

这里有个特别容易翻车的点:判断框里只能写条件,不能写动作。我见过框里写着"处理订单"的菱形,那就不叫判断框了,读者根本不知道从哪条线出去。判断框的内容必须是能判断真假的表达式,比如"库存 >= 数量",出口线上再标"是"和"否"。

2.2 连线的三条硬规矩

第一条,箭头方向就是执行方向,别用无箭头的线。有些人为了图面干净省掉箭头,靠"从上往下"的默认约定,一旦出现回边和向上跳转,立刻歧义。

第二条,一个出口只能有一条线。判断框的两个出口要分别画两条独立的线,不能画一条线出去再分叉,分叉在流程图里没有语义。

第三条,跨区或跨页用连接符。一张图如果横向铺到一米宽,读的人要来回摇头,那就该在右侧断开,用一个圆圈标"A",下一页再用圆圈标"A"接上。连接符的编号要有顺序,不要出现两个"A"。

2.3 判断框出口标注的写法

出口线上的标注我建议写具体条件而不是单纯写"是/否",尤其是嵌套判断的时候。比如三层嵌套的校验,全是"是/否"看起来就像迷宫,写成"金额超限""库存不足""账户冻结"立刻清楚。如果条件太长,可以在线上写简短标签,旁边的注释框里补完整表达式。

注意:出口线标注不要只写"Y/N",跨团队评审时这两个字母的歧义率出乎意料地高,写"是/否"或者直接写条件更稳妥。

3. 三种基本结构的画法拆解与代码对照

结构化程序设计的三种基本结构是顺序、选择、循环,任何一段代码拆到最后都是这三样的组合。把这三种画法练到不用想,剩下的就是耐心问题。这一节我用常见的 Python 和 Java 片段做对照,把容易画错的地方逐个拆开。

3.1 顺序结构:处理框合并的取舍

顺序结构最简单,一行代码一个框,但没人会这么画。真正要拿捏的是合并到什么程度。我的原则是:合并的标准是"读者是否需要知道中间细节"。

total = 0 for item in items: total += item.price * item.count discount = total * 0.1 if total > 1000 else 0 final = total - discount

这三行如果画成一个框"计算订单总价与折扣",代码审阅时没问题,但如果你要排查"折扣算错"的问题,就得拆成三个框:累加明细、判断是否满减、扣减折扣。同一段代码,两张图,用途不同,这就是前面说的受众决定颗粒度。

我的经验是,处理框里的文字超过十二个汉字,就该考虑拆开。超过这个长度,读者要么看不清,要么理解为另一种意思。另外,连续的赋值如果中间没有分支,可以合并;一旦中间夹了判断,必须断开,因为控制流在那里分岔了。

3.2 选择结构:if-else、switch 和提前 return 的画法差异

if-else是最好画的:判断框一个入口两个出口,真分支往下,假分支往右或者往下走另一条路,最后两条路汇聚到一个点继续。关键在于汇聚点要不要画。如果两个分支之后还有共同逻辑,必须画汇聚;如果两个分支各自 return 结束,就不用硬凑汇聚点,各自接一个结束框更清楚。

else if链有个常见的画错方式:有人画成一排并列的菱形,然后从第一个菱形往下连到第二个,看起来像顺序执行。正确的画法是串联——第一个判断为假才走第二个判断,箭头必须体现这个先后关系。当判断超过四个,横着排会拉得很宽,这时候竖着排更合适,或者干脆改成表格描述。

switch稍微特殊。如果用菱形串联画,四五个 case 会画得很啰嗦。我的做法是:case 少于五个用判断框串联;超过五个就用一个"多路选择"标识,按 ISO 规范可以用六边形,然后在注释框里列出所有 case 与对应处理,图面上只画主干和 default 分支。

提前 return这种写法值得单独说。很多人照着if (!valid) return;画图时会把 return 当成正常分支画在下面,结果主流程被挤到右边,越画越歪。更好的做法是:把校验失败的路径统一往右(或者往上)走,接一个结束框,主流程在中间一路向下。这样图的主干一目了然,读者扫一眼就知道正常路径在哪。

3.3 循环结构:while、for、do-while 的回边差异

循环画错的重灾区在这里。三种循环的回边方向完全不一样,混了就彻底读错。

while的判断框在循环体之前。条件为真向下进入循环体,循环体结束画一条回边回到判断框上方,条件为假时从判断框右侧出循环。回边一定要从循环体的末尾回到判断框,不能回到体中间的某个框。

for循环多一个增量环节。判断框之前是初始化(可以用准备框),判断为真进入体,体结束之后先经过一个"变量自增"处理框,再回边到判断框。我见过太多图省略了自增框,读者看完不知道循环变量怎么变,也不知道什么时候能退出。这个框不画,图的正确性就打了折扣。

do-while的判断框在体之后。循环体先执行一遍,然后判断,条件为真回边到体入口,条件为假向下出循环。它的特点是最少执行一次,图上要把这个特点体现出来,回边指向体的第一个框而不是判断框。

break和continue的处理也值得单独说。continue应该画一条箭头回到循环的增量框(for)或者判断框(while),并在线旁标注continue;break则从循环体内部直接拉一条箭头跳到循环的出口汇聚点,标注break。如果循环里有多个break,出口处会汇聚好几条线,这时候用连接符收一下,图面会干净很多。

while True这种写法,判断框直接写死为真,靠内部break退出。图上要在循环出口旁边加一个注释框,说明"退出依赖内部 break",否则读者会以为这是个死循环。

3.4 异常与错误分支怎么落地

带try-catch的代码是流图里最容易画乱的部分。我的做法是把异常路径统一放在右侧一列,主流程保持在中间。正常逻辑从上到下,异常从哪个框可能抛出,就从那个框拉一条虚线箭头向右,汇入异常处理列,最后统一指向结束或者向上抛出。

try { Order order = repo.load(id); if (!order.isPayable()) { throw new BizException("订单不可支付"); } payService.pay(order); } catch (BizException e) { log.warn(...); return Result.fail(e.getMessage()); } catch (Exception e) { log.error(...); return Result.fail("系统繁忙"); }

这段代码画成流图会得到:主干是"加载订单 → 判断可支付 → 发起支付",右侧有两条异常线分别接两个 catch,两个 catch 各自的结束框可以合并成一个"返回失败"。图上用虚线表示异常流,实线表示正常流,这是很多团队的约定,写进图例里,别人一看就懂。

提示:如果一个函数里异常分支比正常分支还多,说明这段代码该拆了,不要试图用一张更复杂的图去掩盖设计问题。

4. 从现成代码反推流图:一套能落地的梳理流程

前面讲的是"怎么画得像",这一节讲"怎么画得出来"。面对一段陌生的代码,直接硬画通常画到一半就崩了,因为脑子里同时在处理"读代码"和"排布局"两件事。我习惯分三步走:先定边界,再映射,最后合并。

4.1 先定边界与出入口

动笔前先回答三个问题:这个函数的入口在哪?出口有几个?跨函数调用要不要展开?

入口很简单,一个起止框写函数签名,比如"settle(订单号, 支付方式)"。出口要数清楚,有五个return就画五个结束框(或者在右侧合并成一列)。跨函数调用默认不展开,画成一个处理框,框角标一个小三角或者加编号,注明"见子图 3"。除非那个被调函数正是排查问题的关键,否则展开会让图迅速膨胀到不可读。

还有一个容易忽略的边界:入参校验算不算图内。我的建议是画进去,但合并成一个框"校验入参",因为校验失败的路径也是流程的一部分,跳过它,图的路径就不完整,测试同学会来找你。

4.2 缩进映射法:把嵌套代码逐层翻译

这是我最常用的方法,三步就能把代码结构搬到图上。

第一步,把代码按缩进抄成树。拿一段真实的代码举例:

def settle(order, user): if not user.is_active: return Fail("账号已冻结") if order.amount <= 0: return Fail("金额非法") if user.balance < order.amount: if user.credit_limit > 0: use_credit = True else: return Fail("余额不足") else: use_credit = False result = pay(order, user, use_credit) if not result.ok: retry(order, user) return Success(result.txn_id)

抄成树就是:第一层是四个并列的校验判断,第二层是第三个判断里的信用额度分支,最后是支付与重试。

第二步,每个if/while开一个菱形。注意树的同一层是并列的,图上也应该并列,不要画成串联的瀑布。上面这段代码里前三个判断是并列校验,画成三个连续的菱形,任何一个走"否"都往右出到结束框,只有全"是"才继续往下。这一步画对,图的骨架就对了。

第三步,连续的赋值和调用合并成矩形。上面代码里use_credit = True / False各自合并成一个框,pay和retry各一个框,return各一个结束框。合并的时候记住一个框只干一件事,pay和retry不合并,因为它们之间夹着判断。

按这三步走下来,最典型的坑是把并列判断画成嵌套判断。图上看是甲走完才轮到乙,实际上两者互不影响,这个错误会让测试同学以为存在"甲为真时乙被跳过"的路径,白白多设计用例。画完以后对一遍代码的缩进层级,能立刻发现。

4.3 复杂度刹车:什么时候必须拆图

流图不是越全越好。我给自己定的几条线:

指标建议上限超了怎么办
单图处理框数量15 到 20 个按阶段拆成多张子图
判断框嵌套深度3 层抽出独立函数单独出图
图面交叉线尽量为 0用连接符断开,或调整分支位置
单页尺寸A4 装得下缩小颗粒度,合并同类处理

超出这些线的时候,拆图的方向有几种:按阶段拆(校验阶段、计算阶段、落库阶段各一张)、按正常与异常拆(主图只有正常路径,异常单独一张)、按子函数拆(主图只保留调用关系)。我一般首选按阶段拆,因为阶段之间的衔接点少,每张图的入口出口都清楚,读者也容易对照代码定位。

5. 工具选型:从白板手绘到文本化建模

工具这件事上没必要迷信。见过用 Visio 画出蜘蛛网的,也见过用记事本画得清清楚楚的。选工具的判据就两条:改动成本和能否随代码一起版本管理。

5.1 手绘与白板:讨论阶段的最优解

需求讨论、方案对齐阶段,白板手绘的速度没有对手。想改就擦,想挪就画个箭头,五分钟能改三版。我参与过的每一个棘手的流程问题,最后都是站着在白板前理清的,而不是坐着改 Visio。

它的短板也很明显:留不下来,散会就没了。我的做法是讨论完用手机拍一张,把照片贴进会议记录当临时附件,如果这个流程确实值得沉淀,再花二十分钟用工具重画一遍。不要试图在讨论阶段就用专业工具,那会让讨论节奏被工具拖慢。

5.2 图形化工具:draw.io 与 Visio 的取舍

需要正式交付的图,我用得最多的是 draw.io(现在叫 diagrams.net)。它的好处是免费、格式开放(.drawio本质上是 XML)、自带对齐辅助线和网格吸附、有现成的流程图符号库,还能直接嵌入仓库目录。画完导出 SVG 或 PNG,矢量格式放大不糊,评审时贴进文档很清爽。

工具适合场景版本管理主要短板
draw.io日常主力,仓库内维护好,XML 文本大图操作略卡
Visio正式交付文档差,二进制格式跨平台不便
白板手绘讨论、梳理不适用无法沉淀
文本化建模需要 diff 和评审极好,纯文本布局由引擎决定

Visio 的符号库更规范,模板更全,适合对格式有硬性要求的交付场景。但它的文件是二进制格式,放进 Git 只能看出版本变了,看不出改了什么,协同时两个人同时改基本必冲突。如果团队已经买了并且习惯用,那也行,但我不建议把 Visio 当作唯一的沉淀载体。

5.3 文本化方案:让图和代码一起进仓库

这几年我越来越倾向把关键的流程图用文本描述语言来维护,理由是它能 diff、能评审、能跟着代码一起提交。比如下面这种写法,在代码仓库里是一个普通的文本文件,改了哪一行 Git 看得清清楚楚:

@startuml start :校验入参与账号状态; if (余额是否足够?) then (是) :扣减余额; else (否) :读取信用额度; if (额度是否足够?) then (是) :使用信用支付; else (否) :返回余额不足; stop endif endif :写入交易流水; :返回成功; stop @enduml

它的缺点是布局由引擎决定,想微调某个框的位置很费劲,遇到引擎排得不理想的时候,唯一的办法是调整节点顺序或者拆图。所以我的实际组合是:需要精细排版的对外交付图用 draw.io,需要长期维护、频繁改动的内部图用文本化方案,两套并存,各管一段。

注意:不管用哪种工具,源文件都要进仓库,导出的图片放在同目录的images子目录里,命名带上模块和版本号,否则半年后没人分得清哪张图对应哪版代码。

6. 六类高频画错方式与现场修正思路

前面几节是正向的写法,这一节反过来讲错法。下面这六种问题,基本覆盖了我审图时打回修改的九成情况。

6.1 线条交叉成蜘蛛网

回边和异常分支一多,线就开始互相穿插,图上出现一片网格,看着就头疼。修正思路是按区域规划:主流程走中间一列,异常路径走右侧一列,循环回边统一走左侧,跨区域的用连接符断开。如果交叉还是消不掉,说明这个函数本身的分支太多,先拆图再说。

6.2 一个框塞了三件事

框里写着"查询用户并校验状态、计算价格、写入订单",这种框一旦出错,没人知道是哪一步出的问题。修正办法是把动词数一遍,一个框一个动词。判断标准很简单:如果你不能用一句话概括这个框在干什么,就该拆。

6.3 漏掉边界条件

边界条件漏了,图看着完整,测试照样漏。我习惯对着代码把这几类边界逐个过一遍:空集合、单元素、最大值、null、越界索引、除零、超时、重试次数耗尽、并发冲突、重复提交。每发现一个,就在对应的判断框上加一个出口。这一步花十分钟,能省掉后面几轮返工。

6.4 循环出口方向标反

while和do-while的回边画反是最隐蔽的错误,因为图整体看起来是通的,只有光照着图跑一遍才会发现。我的自检办法是:拿一个能让循环执行零次的输入,顺着图走一遍,看看是否真的会跳过循环体;再拿一个执行一次的输入走一遍。两次都对了,循环结构基本就没画错。

6.5 判断框出口不标条件

一个菱形两条线出去,都不写字,读者只能猜哪条是"是"。规范做法是每条出口线都标条件,嵌套判断时尤其要标具体表达式。

错误画法问题修正
出口不标任何文字无法判断分支走向标"是/否"或具体条件
只标"Y/N"跨团队歧义写"是/否"或条件表达式
一条线出去再分叉语义不成立拆成两条独立出口线
判断框里写动作混淆判断与处理动作放矩形,菱形只放条件

6.6 缺少标题、图例、版本信息

这三样最不起眼,但缺了以后图就没法长期用。标题写清模块与功能,图例说明符号和线型的约定,版本信息包含版本号、日期、作者、对应代码文件与函数名。我的模板是右下角放一个小方块,五行字搞定,一劳永逸。

7. 让流图活下来的习惯:评审、更新与测试对齐

画得再好的图,如果三个月后就没人维护,价值会迅速归零。我在这几年里养成的几个习惯,说不上高明,但确实让图活得比项目周期更长。

7.1 图和代码放在同一次提交里

这是最重要的一条。改逻辑必须改图,图和代码走同一个 PR,评审的人一并看。为了做到这一点,图的源文件必须放在仓库里、必须是文本或者可 diff 的格式,这一点在前面选工具的时候就要考虑到。做不到这一条,图迟早会过期。

我见过一种折中做法:图放在文档平台上,PR 描述里带链接。这个方案的问题在于,改代码的人经常忘记点那个链接,几轮迭代下来文档平台上的图就变成了历史文物。放在同一个仓库里,虽然会多占几十 KB,但维护成本最低。

7.2 用流图反推测试用例

这是流图最被低估的价值。每一条从入口到出口的完整路径,都对应一个测试用例。判断框数量为 n 的时候,理论路径数是 2 的 n 次方,实际当然不会全测,但可以按分支覆盖来取:每个判断框的两个出口至少各走一次,这样得到的用例集覆盖度已经不错。

我的做法是在图上把用到的路径用不同颜色描一遍,描完以后统计每个判断框的两个出口是否都被走过。如果一个出口始终没被描到,要么补用例,要么在图上标注"该分支理论上不可达"并且找出原因。这一步经常能挖出隐藏的 bug,比如某个else分支其实永远进不去,说明上游已经保证了条件。

7.3 建立淘汰机制

图不是越多越好。我的规则是:超过半年没被任何人打开过的图,要么更新,要么删掉。留着过期图比没有图更危险,因为后面接手的人会默认它是对的,照着错的图去理解代码,排查方向从一开始就偏了。删除不可惜,代码和 Git 历史还在,需要的时候重画一张也就个把小时的事。

画了这么多年的程序流图,我个人最大的体会是:动笔之前先数一下代码里的判断和循环数量,超过二十个就别硬画一张图。先把函数拆开,再每个函数画一张,最后用一张总的调用关系图串起来。这样做的好处是每张图都在可控规模内,出错概率低,别人读起来也轻松。另一个小技巧是画完之后别急着交付,隔一天再回来看一遍,很多当时觉得理所当然的分支,第二天再看会发现漏了条件。图是给自己和同事省时间的工具,不是给文档凑数的装饰,想清楚这一点,画法自然就简单了。

返回列表