软件分析与设计笔记 10:精炼用例模型:include、extend 与泛化
三种精炼用例关系的判断标准:include 什么时候用、它和 extend 的区别到底在哪、参与者泛化与用例泛化各解决什么问题,以及精炼到什么程度就该停手。
本文是 《软件分析与设计笔记》 系列的第 10 篇,共 28 篇
这是我在学习软件分析与设计时整理的系列笔记。概念部分是我按自己的理解写的,示例与图示都是自己编、自己画的。理解有偏差的地方欢迎指出。
贯穿案例:片区家电维修派单系统。08 画了用例图、09 抽了实体,这一篇把 08 那张平铺的图精炼一遍。
0 用例画完之后,重复出现了
第一版用例图通常是这样的:每个用例各画各的,画完一看,同一件事在好几个用例里各写了一遍。
贯穿案例里就有三处:
- 「校验工单状态」 —— 「派单」「申请配件」「回填工单」里各写了一遍;
- 「撤回申请」 —— 只在提交后 5 分钟内可以,是「申请配件」的一个分支;
- 「客服」和「片区经理」 —— 画了两套用例,但其中一大半是同一个。
用例模型的精炼(refining the use case model)要解决的就是这类重复,以及把可选流程从主流程里拆出来。有三种关系可以用。
1 include:把公共步骤抽出来
include 关系(include relationship) 表示:这个用例每次执行时都会用到另一个用例。
派单 --include--> 校验工单状态
申请配件 --include--> 校验工单状态
回填工单 --include--> 校验工单状态
1.1 它会每次都执行吗
判断标准就一条:这个步骤会不会每次都被执行?
「校验工单状态」在这三个用例里都是必经的第一步 —— 工单不处于「进行中」,后面什么都不能做。所以适合 include。而被 include 的那个用例(图上叫 校验工单状态)通常不是一个完整的目标,它更像是被复用的一个片段 —— 这是 include 的一个特征:被 include 的用例一般不独立出现。
1.2 箭头方向是唯一的坑
include 的箭头由基用例指向被包含用例:
派单 ──«include»──> 校验工单状态
记法:「我需要它」—— 箭头朝被需要的一方。这个方向画反,语义就变成「校验工单状态依赖派单」,完全说不通。
还有一条讲义明确说的:线用实线还是虚线都可以。所以不要在线的样式上纠结,方向才是分。
1.3 什么不该做成 include
讲义里有一句直接的刹车条件:不要给「只涉及一个屏幕」的东西做 include。
贯穿案例里的反例:有人会把「显示工单详情」做成 include。但「显示详情」只是一个界面动作,它每次都会被做,却不是一个可复用的业务步骤。把它抽出来只会让图变复杂,不会让任何东西变清楚。
2 extend:可选与例外
extend 关系(extend relationship) 表示:在特定条件下,主用例会额外做一件事。
申请配件 <--extend-- 撤回申请(5 分钟内)
申请配件 <--extend-- 转人工审核(金额超 500)
判断标准:这个步骤只在某些条件下发生,而主用例本身并不知道它存在。
最后半句是关键。主用例(base use case)不需要知道有「撤回」这回事 —— 「申请配件」该做的就是申请。撤回是叠加上去的可选项。如果主用例必须知道对方的存在才能跑通,那就不是 extend,而是 include 或者主流程的一部分。
2.1 扩展点挂在哪里
这里有个衍生出来的「扩展点(extension point)」概念:扩展用例挂在主用例的哪个位置触发。
本例里「撤回申请」的扩展点是「提交成功之后、仓管出库之前」—— 因为一旦仓管已经出库,就不是撤回而是退货了。扩展点写清楚,实现的边界就清楚了。
2.2 箭头方向与 include 相反
这是全篇最集中的扣分点,一定要记成条件反射:
撤回申请 ──«extend»──> 申请配件
扩展用例指向基用例。记法与 include 对称:「我附加到它」—— 箭头朝被附加的一方。
两个补充细节:
- 条件写在箭头旁,不加方括号。(方括号是活动图 guard 的写法 —— 这个区别每年都有人混。)
- 讲义说 extend 的条件也可以不写。但写了更好,因为它说明了这条扩展什么时候触发。
2.3 基用例必须自己站得住
extend 有一个容易忽略的前提:被扩展的那个用例,即使扩展不执行,也必须是一个完整、有意义的目标。
「申请配件」即使永远不撤回,它也是完整的。「撤回申请」单独拿出来呢?它没有意义 —— 你没申请过,撤什么?
这就引出一条判据:如果一个用例单独看没有意义,它很可能是某个基用例的扩展,而不是独立用例。这一条能帮你判断方向。
3 include 和 extend 的区别,一句话
| include | extend | |
|---|---|---|
| 执行时机 | 每次都会执行 | 只在特定条件下执行 |
| 依赖方向 | 主用例依赖被包含用例 | 扩展用例依赖主用例 |
| 箭头方向 | 基用例 → 被包含用例 | 扩展用例 → 基用例 |
| 主用例是否知道 | 知道,是它的必经步骤 | 不知道,是可叠加的行为 |
| 能否独立存在 | 被包含用例一般不独立 | 扩展用例不能独立 |
| 典型用途 | 复用公共步骤 | 可选功能、例外流程 |
记不住的时候,问一句:「这一步能不做吗?」 不能不做 → include;可能不做 → extend。
4 泛化:把共同的部分提到父级
泛化(generalization) 是把多个元素共同的部分抽象成一个父级。用例模型里有两个地方能用到:
4.1 参与者泛化
① 参与者泛化(generalization of actors)。 本例里就是「客服」和「片区经理」:两人共用一整套用例(看待派单、看排期、改约),区别只在经理多几个「看报表」的用例。
做法是把共同的放到父级「门店员工」,子级只画差异。
门店员工 (abstract)
△ △
│ │
客服 片区经理
判断标准是 08 篇那条:他们做的事一样吗?一样(只是权限不同)→ 泛化或合并;不一样 → 保持两个独立角色。
4.2 用例泛化
② 用例泛化(generalization of use cases)。 本例里:「空调报修」「冰箱报修」「洗衣机报修」的流程主体一样(描述故障 → 选时段 → 提交),只有故障类型对应的表单字段不同。
把「提交报修」作为父用例(抽象用例,abstract use case,本身不直接执行),三个子用例继承并覆盖差异部分。
不过本例里我不建议这么做 —— 三种家电的差异只是表单字段,用一个用例 + 一个「家电类型」字段就够了。用例泛化只在流程本身不同时才值得用。
4.3 箭头方向
泛化的箭头是空心三角,由子指向父。同一张图上如果既有 include/extend 又有泛化,最容易混的就是这三种箭头。
记法:空心三角是「继承」,永远指向父;虚线开放箭头是「引用」,指向被引用方。
4.4 收益与代价
泛化的价值在于改动集中:共同流程变一次,所有子级都跟着变。代价是抽象层次变多,读图的人要多跳一层。所以只有当「差异确实很小、共同部分确实很多」时才值得用。
5 精炼到什么程度就该停
精炼有一个明显的收益递减点。把每个用例都拆成 include、再给每个角色都建一个父级,最后得到一张谁也看不懂的图 —— 这就是过度抽象(over-abstraction)。
讲义里给了几条具体的刹车条件:
- 不要为「只涉及一个屏幕」的行为建 include/extend。判据是:两个界面看起来很像,可能只是一个在显示模式、一个在编辑模式 —— 那不是共同流程,是两种不同的任务;
- include 的嵌套不要超过一层。被包含的用例不要再 include 别的东西,否则读图要跳三层;
- extend 的嵌套也不要超过一层。
一条总原则:如果精炼之后,业务方看图的难度上升了,那这次精炼就是不值得的。用例模型的首要读者是业务方,不是建模的人。
另外,用例变多之后的分组(use case package)是另一层组织手段,笔记 08 已经讲过。
6 先细化哪一个:两种替代方案
最后剩一个实际问题:用例很多,先细化哪个?两种常见策略:
6.1 策略一:先做主线
策略一:先做主线。 先把核心流程(派单、申请配件)细化到能开发,再处理分支。适合要尽早看到可用版本的场景。
6.2 策略二:先做分支
策略二:先做分支。 先把例外流程(缺料、改约、退回)理清。适合业务规则复杂、例外很多、返工成本高的场景。
6.3 怎么选
选哪种取决于哪一类问题更容易推翻已有的设计。如果例外流程经常改主线,那就该先做分支 —— 因为先做的主线很可能白做。
本例恰好属于后者:「缺料」这个例外几乎改写了整个主流程(工单要转「待件挂起」、客户要改约、仓管要催货)。如果先做主线再发现这一点,主线要返工。所以本例该先做分支。
本节自测
- 判断用 include 还是 extend,只需要问哪一个问题?
- 「派单」需要知道「撤回申请」的存在吗?这个问题的答案说明了什么?
- include 和 extend 的箭头方向分别指向哪一方?各用什么记法记住?
- 参与者泛化和用例泛化分别在什么情况下有用?本例中哪个建议用、哪个不建议?
- 精炼到什么程度就该停下?讲义给了哪三条具体的刹车条件?
- 什么情况下应该先细化例外流程,而不是主线?本例属于哪种?
参考答案
- 「这一步能不做吗?」 不能不做 → include;可能不做 → extend(§3)。
- 不需要 —— 「派单」该做的就是派单,撤回是叠加在「申请配件」上的可选项。这说明了主用例不必知道扩展用例的存在;反之若主用例必须知道对方才能跑通,那就不是 extend 而是 include(§2、§3)。
- include:基用例 → 被包含用例,记法「我需要它」;extend:扩展用例 → 基用例,记法「我附加到它」(§1.2、§2.2)。
- 参与者泛化用于两人共用一整套用例、只是权限不同的情况 —— 本例「客服」与「片区经理」建议用;用例泛化用于流程本身不同、共同部分很多的情况 —— 本例三种家电报修只是表单字段不同,不建议用,一个用例加一个「家电类型」字段就够(§4.1、§4.2)。
- 停的判据是 「业务方看图的难度上升了就不值得」。三条具体刹车:不要为只涉及一个屏幕的行为建 include/extend;include 嵌套不超过一层;extend 嵌套不超过一层(§5)。
- 当例外流程容易推翻主线设计时该先做分支。本例属于这种 —— 「缺料」这个例外几乎改写了主流程(工单转待件挂起、客户改约、仓管催货),先做主线会返工(§6.3)。
术语表
| 术语 | 中文 | 一句话解释 |
|---|---|---|
| include relationship | 包含关系 | 主用例每次执行都会用到的公共步骤 |
| extend relationship | 扩展关系 | 只在特定条件下叠加到主用例上的行为 |
| base use case | 基础用例 | 被扩展的那个主用例 |
| extension point | 扩展点 | 扩展用例挂载到主用例上的触发位置 |
| generalization of actors | 参与者泛化 | 把多个角色的共同部分抽象成父角色 |
| generalization of use cases | 用例泛化 | 把多个用例的共同流程抽象成父用例 |
| abstract use case | 抽象用例 | 只用于被继承、本身不直接执行的用例 |
| refining the use case model | 精炼用例模型 | 用 include、extend、泛化整理用例结构 |
| use case package | 用例包 | 按主题、角色或阶段对用例分组 |
| over-abstraction | 过度抽象 | 抽象层次过多,反而降低可读性 |