软件分析与设计笔记 08:功能需求(一):用例模型与活动图
从用户故事到用例的翻译过程:什么才算「好」的参与者,系统边界怎么划,用例描述必须写清哪四件事,活动图怎么把分支、并行与泳道画清楚,以及这两个图在 draw.io 里各用哪个形状、最容易画错哪一笔。
本文是 《软件分析与设计笔记》 系列的第 8 篇,共 28 篇
这是我在学习软件分析与设计时整理的系列笔记。概念部分是我按自己的理解写的,示例与图示都是自己编、自己画的。理解有偏差的地方欢迎指出。
本系列的贯穿案例:片区家电维修派单系统 —— 客户报修 → 客服派单 → 工程师接单上门 → 回填工单 → 仓管备料 → 片区经理看板。角色:客户、客服、工程师、仓管、片区经理,以及外部的「配件供应商库存系统」。这一篇从这里开始,后面 09、10、12、14、18、21、22、27 用的都是它。
0 故事写完,开发还是问你一堆问题
十条用户故事摆在开发面前,第一个问题通常是:
「工程师到了现场,发现配件没有货,这单怎么办?」
故事里没写。因为用户故事天生只描述从使用者视角的一句话,它不负责回答「系统内部怎么处理各种情况」。
功能性需求(functional requirement)要把这一层补上,需要换一种描述方式:用例(use case)。
1 从用户故事到用例
1.1 两种视角的差别
两者的视角不一样:
| 用户故事 | 用例 | |
|---|---|---|
| 视角 | 使用者的诉求 | 系统与外部的一次完整交互 |
| 粒度 | 一句话 | 一段可展开的流程 |
| 关注点 | 为什么做 | 系统该做什么、按什么顺序做 |
1.2 三种翻译情况
翻译时有三种常见情况:
- 一对一 —— 一条故事正好对应一个用例,例如「客户报修」;
- 多对一 —— 几条故事合成一个用例,例如「查询配件库存 / 查看可约时段 / 确认上门时间」最终都是「安排上门」这个用例的步骤;
- 一对多 —— 一条大故事拆成几个用例,例如「派单」会拆成「创建工单」「分配工程师」「通知客户」三个。
判断标准是:一个用例应该有一个明确的目标达成点。「工单建好了」是一个目标,「客户知道几点上门」是另一个。
2 什么才算「好」的参与者
2.1 三个判断要点
把所有参与者与用例画在一起,就是用例图(use case diagram);图加上每个用例的文字描述,构成完整的用例模型(use case model)。其中的基本单元是参与者(actor)—— 与系统交互的外部角色。三个判断要点:
- 是唯一的角色,不能是泛称。「用户」不算一个 actor,「客户」「客服」「仓管」才是。因为不同的角色关心的东西不一样;
- 可以是人,也可以是外部系统。 在贯穿案例里,「配件供应商库存系统」就是一个 actor —— 系统要去查它的库存;
- 判断依据是目标,不是身份。 问一句:这个角色在系统里有没有自己要达成的目标?没有的就不是 actor,只是流程里的一个步骤。
第 3 条能过滤掉很多噪音。比如「打印机」不是 actor(它没有目标),但「片区经理」是 —— 他要看的看板和工程师要看的不一样。
上面这张图把这两个图种要用的符号一次列全,下面每讲到一处,都可以回来对照它在 draw.io 里搜什么。
画这一笔:draw.io 左侧搜索框输入
actor,把火柴人拖进画布,双击图中默认的Actor字样改成Customer。 想快速复制同类角色:选中火柴人按Ctrl+D(复制),再双击改名。
2.2 一个角色还是一类角色
贯穿案例里有个真实的判断:「普通客户」和「买了延保的客户」要不要建成两个 actor?
判断方法是问一句:他们做的事一样吗?
- 一样,只是待遇不同(延保客户免费换件)→ 合并成一个 actor,差异写成业务规则,放到活动图的分支里;
- 不一样,能用的功能都不同(延保客户能自助申请换件)→ 拆成两个 actor。
这个判断会直接改变用例的个数,所以要早做。
2.3 两个 actor 连同一个用例:可以,但有前提
一个用例连两个火柴人,是完全合法的画法 —— 它表示这两个角色触发并执行这个用例的方式完全一致。
注意最后半句。讲义的原话是:
both will trigger and execute the use case in the same way
它不是「两个角色都会用到这个功能」,而是「两人点进去之后看到同一个界面、走同一套步骤」。这两件事差很远。
本例里正好有一对可以对照:
| 一对角色 | 用法一致吗 | 该怎么画 |
|---|---|---|
| 客服 与 片区经理 | 「看待派单」「看排期」「改约」三个人事完全一样,经理只是多了「看报表」 | 一条用例连两条线(或做参与者泛化) |
| 客服 与 工程师 | 客服是「派单」,工程师是「接单」—— 动作方向都不同 | 拆成两个用例(Assign Engineer / Accept Assignment) |
第二行是很多人会画错的地方:两个用例都跟「派单」有关,看起来像一个东西的两面,但用法完全不同 —— 所以必须分开画。
判断方法一句话:
这两个角色点进去之后,看到的是同一个界面、走的是同一套步骤吗? 是 → 一个用例连两根线;不是 → 拆成两个用例,哪怕名字听起来像。
讲义里还有一条方向相反、同样重要的提醒:「好的 actor = 唯一角色」。两组人扮演同一个角色时可以合并;但如果他们将来会访问不同的用例,就该保持分开。也就是说 —— 能不能共用一个用例,由「用法是否一致」决定,不由「是不是同一批人」决定。
2.4 共用的三种画法,什么时候用哪种
| 画法 | 什么场景用 | 图上的样子 |
|---|---|---|
| 一条用例 + 两条关联线 | 首选。两人只在这一个用例上用法一致 | 两个火柴人各拉一条实线到同一个椭圆 |
| 参与者泛化 | 两人共用的是一整批用例,只是权限不同 | 父角色(抽象)挂公共用例,子角色只挂差异用例,空心三角子→父 |
| 拆成两个用例 | 两人用法不同 | 两个椭圆,各连各的 actor |
第一种和第二种怎么选,看共用的是一个还是一批:只共用一个,直接连两根线最省事;共用一整批,泛化更清楚(泛化会在笔记 10 里展开)。
画这一笔:draw.io 里直接从第二个 actor 拖一条实线到这个已有椭圆即可,不需要新建图形。 注意不要画箭头 —— 关联线表示 actor 主动触发用例,带箭头只在「系统主动推送」(比如定时提醒)时才用。
3 系统边界:哪些交互进系统
3.1 边界怎么划
用例图画在一个系统边界(system boundary) 里。边界怎么划,是这一步最需要讨论的地方。
看贯穿案例里的一个常见分歧:「给客户发上门短信」这件事,在系统边界内还是外?
判断方法:这个动作需不需要系统保存状态、做判断?
- 「决定该给谁发、什么时候发、发什么内容」需要保存工单状态、判断工程师有没有出发 —— 在边界内,是一个用例;
- 「通过哪个短信服务商把短信发出去」是纯粹的通道 —— 在边界外,它是一个外部系统 actor。
边界划错会带来两种后果:划得太宽,需求里塞满了技术选型;划得太窄,该由系统负责的判断被漏掉了。
画这一笔:draw.io 搜
boundary(UML 分类下),拖入画布拉成一个大矩形,所有用例都放进去。 actor 必须留在框外 —— 框里只放椭圆。这一条阅卷人扫一眼就能判断。
3.2 边界画完之后
边界确定后,把落在边界内的用例一个个列出来。贯穿案例的第一版清单大致是:
客户 :报修、查询上门进度、评价服务
客服 :创建工单、分配工程师、改派
工程师 :接单、回填工单、申请备料
仓管 :出库、归还余料
片区经理 :查看派单看板、查看工程师工作量
供应商系统:查询配件库存(系统调用)
这份清单还不是图,只是画图前的材料。下一步才是把它画出来。
4 自动化到什么程度:搬流程还是改流程
边界划完之后,还有一个更根本的问题:这一段流程要不要原样保留?
- digital automation(数字化自动化) —— 把现有手工作业 1:1 搬进系统。风险小,但会把现有流程里的低效也一起固化下来;
- digital transformation(数字化转型) —— 借这个机会重新设计流程本身,做的不是「原来的事换个工具」,而是「换一种做法」。
判断工具就是一句提问:「这个动作存在的理由是什么」。贯穿案例里,现在客服要「打电话问工程师今天有没有空」,问一句为什么 —— 答案往往是「因为不知道排期」。真正的目的(知道谁有空)完全可以通过系统排期实现,那这一步本身就不需要保留。
这个决定要早做,因为它直接改变用例的个数。
5 用例描述必须写清的四件事
只画图不够,每个用例都要配一段文字描述(use case description),至少包含:
| 要素 | 内容 |
|---|---|
| 参与者(actor) | 谁发起这次交互 |
| 前置条件(precondition) | 开始之前必须已经成立的状态 |
| 主流程(basic flow) | 一切顺利时的步骤序列 |
| 例外流程(alternative flow) | 出错、取消、超时、数据缺失时怎么办 |
前置条件常常被忽略。贯穿案例里,「回填工单」的前置条件是「工程师已到现场且工单状态为进行中」—— 没有这一句,讨论就不知道从这里开始。与之对应的后置条件(postcondition)则写明结束后应当成立的状态,例如「工单状态变为待结算,配件消耗已记录」。
例外流程才是真正的价值所在。主流程只占一个用例的 30%,剩下 70% 的工作量都藏在例外里。 比如「申请备料」的例外是「仓库也没有这个配件」,这时整单要转成「待件挂起」,客户要收到改约通知 —— 这条路径如果没在用例里写出来,开发根本不会做。
6 活动图:把流程画到看得见分支
6.1 三类结构
用例描述写清楚了,还需要一种图形化的表达方式,让业务方也能看懂 —— 这就是活动图(activity diagram)。
一张活动图里最该出现的是这三类结构:
- 判断节点(decision node)与汇合节点(merge node) —— 根据条件走不同分支,再合回一条路径。贯穿案例里最典型的是「是否在保修期内」,两条路径的处理完全不同;
- 并行(fork / join) —— 两件事同时进行。比如「通知客户」和「占用工程师时段」可以并行,不必等对方;
- 泳道(swimlane) —— 用竖直分区把不同角色负责的步骤分开。
画这一笔:draw.io 里这三个形状分别是 —— 判断用
decision(菱形),并行用fork(粗黑横线,搜horizontal bar),泳道搜lane(竖长条)或直接手画矩形 + 分隔线。 判断的分支条件要写在方括号里([yes][no]),draw.io 里是双击连线中间的位置加文字,不是拖一个文本框。
6.2 泳道为什么最实用
泳道是最实用的一类。它一眼就能看出「这一步是谁在做」—— 如果一条流程在活动图上反复跨越泳道,通常说明这个流程本身有协调成本。
贯穿案例里有个很好的对比:「工程师申请备料」这件事,如果画出来是在工程师泳道和仓管泳道之间来回跨四次(申请 → 查库存 → 缺货 → 改约 → 通知),那这条流程的协调成本就很高。看到这种图,业务方自己就会说「能不能简化成一次申请」。
画这一笔:泳道在 draw.io 里没有自动吸附。最省事的做法是手画:拖 N 个等宽矩形横排,顶部写角色名,然后所有活动框都放进对应的矩形里。 缺点是一移动就乱。所以先把活动位置排好,最后再画泳道框。
6.3 图之外还要一张表
除了图形,实际交付时往往还会配一张活动流程表(activity flow table):每一行是一个步骤,列出执行者、动作、条件、输出。图形负责沟通,表格负责精确。
一个例子(截自「申请备料」这一段):
| 活动 | 执行者 | 条件 | 说明 |
|---|---|---|---|
| 提交备料申请 | 工程师 | 工单状态 = 进行中 | 填写配件编号与数量 |
| 查询库存 | 系统 | — | 调供应商库存接口 |
| 库存充足 | 仓管 | 库存 ≥ 申请量 | 生成出库单 |
| 库存不足 | 系统 | 库存 < 申请量 | 工单转「待件挂起」,通知客户改约 |
6.4 把实体挂到活动图上
考试里活动图那道题通常会加一句「include entities」。这不是让你多画几个框,而是要求这张图同时表达流程和数据两件事 —— 只画了动作和分支,等于只答了一半。
做法是给每个动作用到的业务实体系一条线。方向取决于读还是写,这是这一节唯一的难点:
| 这一步在做什么 | 箭头方向 | 例子 |
|---|---|---|
| 读数据(查、取、显示) | 实体 → 动作 | 「查询工单状态」← WorkOrder |
| 写数据(新增、更新、删除) | 动作 → 实体 | 「回填工单」→ WorkOrder |
一个记法:数据流向动作就是读,动作流向数据就是写。看起来像废话,但画的时候极容易顺手都画成同一个方向。
实体本身怎么挑,还是那三条标准(笔记 09):业务概念而不是动作、需要长期记住、有多个实例。在贯穿案例里,WorkOrder、PartUsage、StockItem 都是实体;「提交申请」不是。
画这一笔:实体用普通矩形(或搜
class取一个空的三格框),直接放在对应泳道的右侧。连线时按上表定方向,标read/write在线上更清楚。 不必给每个动作都挂实体 —— 挂那些真正读写数据的动作就够了,否则图会糊成一团。
6.5 自查这张活动图
画完先过一遍这五条,能挡住大部分低级失分:
- 只有一个开始状态,且每个分支最终都能走到结束状态?
- 每个判断的出口都标了方括号条件?
- 泳道数量与参与角色数量对得上?
- 需要实体的动作都挂了实体,读写方向没画反?
- 没有画「是否输入有误」「操作成功」这类校验与确认框?(这一阶段应忽略异常)
7 为什么值得画活动图
三个具体收益:
- 逼出例外流程。 画到分支处,必须回答「另一种情况怎么办」;
- 发现缺失的步骤。 从「客户点了提交」到「系统提示成功」之间,通常还漏着好几步;
- 可以直接拿去和用户核对。 一张图比一段文字更容易被指出错误 —— 「这里不是这样的」,这是需求验证最有效的时刻之一。
8 用例按什么打包
8.1 三种打包依据
用例多了以后需要分组,也就是划分用例包(use case package)。常见的依据有三种:
- 按业务主题(package by domain) —— 按业务范围归类,例如「报修相关」「备料相关」;
- 按角色 —— 每个角色一包,便于按岗位做需求确认;
- 按流程阶段 —— 对应业务的阶段划分,例如「上门前」「上门中」「上门后」。
贯穿案例按第一种分成三包比较自然:
报修与派单 :报修、创建工单、分配工程师、改派
上门服务 :接单、回填工单、申请备料
结算与看板 :评价、结算、派单看板、工作量统计
打包的意义不只是整理。它同时也是在回答「谁该来确认这一包需求」:按角色打包的,就该由对应角色确认。
本节自测
- 用户故事和用例的视角差别是什么?
- 判断一个角色是不是 actor,看的是身份还是目标?
- 贯穿案例里「给客户发上门短信」这件事,哪部分在系统边界内,哪部分在外?
- 为什么说用例的价值主要在例外流程里?举一个本例中的例外。
- 活动图里的泳道解决了什么问题?流程反复跨越泳道说明什么?
- 在 draw.io 里画泳道,为什么建议「最后再画泳道框」?
- 「普通客户」和「延保客户」,什么情况下合并成一个 actor,什么情况下拆成两个?
- 用例包按什么依据划,会直接影响哪件事?
参考答案
- 视角差别:用户故事是「用一句话说清谁要什么」,用例是「系统与外部的一次完整交互」。前者回答为什么做,后者回答系统做什么、按什么顺序做(§1.1)。
- 看目标。 判断标准是「这个角色在系统里有没有自己要达成的目标」。所以他是不是人、什么职位都不重要 —— 打印机没目标,不是 actor;供应商库存系统有目标(被查询),是 actor(§2.1)。
- 分两段看:决定发给谁、何时发、发什么内容 → 在边界内(要保存工单状态、做判断);通过哪个短信服务商发出去 → 在边界外(纯通道,是外部系统 actor)(§3.1)。
- 因为主流程只占 30%,例外里藏着大部分真实工作量。本例的典型例外:「申请备料时仓库也没有这个配件」→ 工单转「待件挂起」→ 通知客户改约 → 重新排期。这条路径不写出来,开发就不会做(§5)。
- 泳道解决「这一步是谁在做」,让人一眼看出责任归属。反复跨越泳道说明这个流程的协调成本高 —— 本例「申请备料」跨四次就是个信号,业务方看到会想简化(§6.2)。
- 因为 draw.io 的泳道框不会自动吸附活动框,一移动就乱。先排好活动位置、再画泳道框,可以少返工(§6.2 的画这一笔)。
- 合并:两人做的事一样、只是待遇不同(延保客户免费换件)→ 差异写成业务规则,放进活动图分支;拆分:能用的功能都不同(延保客户能自助申请换件)→ 各自要有独立用例(§2.2)。
- 按什么依据打包,直接决定「谁该来确认这一包需求」。按角色打包就由对应角色确认,按阶段打包就由该阶段的负责人确认(§8.1)。
术语表
| 术语 | 中文 | 一句话解释 |
|---|---|---|
| functional requirement | 功能需求 | 系统要「做什么」 |
| use case | 用例 | 一个参与者通过系统达成某个目标的完整交互 |
| use case model | 用例模型 | 参与者与用例的集合,描述系统的功能边界 |
| use case diagram | 用例图 | 画出参与者、用例与系统边界的图 |
| actor | 参与者 | 与系统交互的外部角色,可以是人或外部系统 |
| system boundary | 系统边界 | 区分「系统负责」与「外部负责」的那条线 |
| use case description | 用例描述 | 用文字写清一个用例的流程与条件 |
| basic flow | 主流程 | 一切顺利时的步骤序列 |
| alternative flow | 例外流程 | 出错、取消、超时等情况下的处理 |
| precondition | 前置条件 | 用例开始前必须成立的状态 |
| postcondition | 后置条件 | 用例结束后应当成立的状态 |
| activity diagram | 活动图 | 描述流程步骤、分支与并行的图 |
| activity flow table | 活动流程表 | 用表格逐行记录步骤、执行者与条件 |
| decision node | 判断节点 | 按条件分叉的节点 |
| merge node | 汇合节点 | 把多个分支重新合到一起的节点 |
| fork / join | 分叉 / 汇合 | 表示并行开始的节点 / 等待并行结束的节点 |
| swimlane | 泳道 | 用分区表示每个步骤由谁负责 |
| use case package | 用例包 | 按主题、角色或阶段对用例分组 |
| digital automation | 数字化自动化 | 把现有手工流程原样搬进系统 |
| digital transformation | 数字化转型 | 借系统建设重新设计流程本身 |