软件分析与设计笔记 10:精炼用例模型:include、extend 与泛化

三种精炼用例关系的判断标准:include 什么时候用、它和 extend 的区别到底在哪、参与者泛化与用例泛化各解决什么问题,以及精炼到什么程度就该停手。

学习#需求分析#用例建模#UML#学习笔记

本文是 《软件分析与设计笔记》 系列的第 10 篇,共 28 篇

这是我在学习软件分析与设计时整理的系列笔记。概念部分是我按自己的理解写的,示例与图示都是自己编、自己画的。理解有偏差的地方欢迎指出。

贯穿案例:片区家电维修派单系统。08 画了用例图、09 抽了实体,这一篇把 08 那张平铺的图精炼一遍。

0 用例画完之后,重复出现了

第一版用例图通常是这样的:每个用例各画各的,画完一看,同一件事在好几个用例里各写了一遍。

贯穿案例里就有三处:

  • 「校验工单状态」 —— 「派单」「申请配件」「回填工单」里各写了一遍;
  • 「撤回申请」 —— 只在提交后 5 分钟内可以,是「申请配件」的一个分支;
  • 「客服」和「片区经理」 —— 画了两套用例,但其中一大半是同一个。

用例模型的精炼(refining the use case model)要解决的就是这类重复,以及把可选流程从主流程里拆出来。有三种关系可以用。

精炼用例模型的三种方式:include 把公共步骤抽成被复用的用例,extend 描述可选或例外流程,泛化把共同部分抽象成父级,包括参与者泛化与用例泛化;每个关系都标出箭头方向与判断依据

1 include:把公共步骤抽出来

include 关系(include relationship) 表示:这个用例每次执行时都会用到另一个用例。

text
派单      --include--> 校验工单状态
申请配件  --include--> 校验工单状态
回填工单  --include--> 校验工单状态

1.1 它会每次都执行吗

判断标准就一条:这个步骤会不会每次都被执行?

「校验工单状态」在这三个用例里都是必经的第一步 —— 工单不处于「进行中」,后面什么都不能做。所以适合 include。而被 include 的那个用例(图上叫 校验工单状态)通常不是一个完整的目标,它更像是被复用的一个片段 —— 这是 include 的一个特征:被 include 的用例一般不独立出现。

1.2 箭头方向是唯一的坑

include 的箭头由基用例指向被包含用例:

text
派单 ──«include»──> 校验工单状态

记法:「我需要它」—— 箭头朝被需要的一方。这个方向画反,语义就变成「校验工单状态依赖派单」,完全说不通。

还有一条讲义明确说的:线用实线还是虚线都可以。所以不要在线的样式上纠结,方向才是分。

1.3 什么不该做成 include

讲义里有一句直接的刹车条件:不要给「只涉及一个屏幕」的东西做 include。

贯穿案例里的反例:有人会把「显示工单详情」做成 include。但「显示详情」只是一个界面动作,它每次都会被做,却不是一个可复用的业务步骤。把它抽出来只会让图变复杂,不会让任何东西变清楚。

2 extend:可选与例外

extend 关系(extend relationship) 表示:在特定条件下,主用例会额外做一件事。

text
申请配件  <--extend--  撤回申请(5 分钟内)
申请配件  <--extend--  转人工审核(金额超 500)

判断标准:这个步骤只在某些条件下发生,而主用例本身并不知道它存在。

最后半句是关键。主用例(base use case)不需要知道有「撤回」这回事 —— 「申请配件」该做的就是申请。撤回是叠加上去的可选项。如果主用例必须知道对方的存在才能跑通,那就不是 extend,而是 include 或者主流程的一部分。

2.1 扩展点挂在哪里

这里有个衍生出来的「扩展点(extension point)」概念:扩展用例挂在主用例的哪个位置触发。

本例里「撤回申请」的扩展点是「提交成功之后、仓管出库之前」—— 因为一旦仓管已经出库,就不是撤回而是退货了。扩展点写清楚,实现的边界就清楚了。

2.2 箭头方向与 include 相反

这是全篇最集中的扣分点,一定要记成条件反射:

text
撤回申请 ──«extend»──> 申请配件

扩展用例指向基用例。记法与 include 对称:「我附加到它」—— 箭头朝被附加的一方。

两个补充细节:

  • 条件写在箭头旁,不加方括号。(方括号是活动图 guard 的写法 —— 这个区别每年都有人混。)
  • 讲义说 extend 的条件也可以不写。但写了更好,因为它说明了这条扩展什么时候触发。

2.3 基用例必须自己站得住

extend 有一个容易忽略的前提:被扩展的那个用例,即使扩展不执行,也必须是一个完整、有意义的目标。

「申请配件」即使永远不撤回,它也是完整的。「撤回申请」单独拿出来呢?它没有意义 —— 你没申请过,撤什么?

这就引出一条判据:如果一个用例单独看没有意义,它很可能是某个基用例的扩展,而不是独立用例。这一条能帮你判断方向。

3 include 和 extend 的区别,一句话

includeextend
执行时机每次都会执行只在特定条件下执行
依赖方向主用例依赖被包含用例扩展用例依赖主用例
箭头方向基用例 → 被包含用例扩展用例 → 基用例
主用例是否知道知道,是它的必经步骤不知道,是可叠加的行为
能否独立存在被包含用例一般不独立扩展用例不能独立
典型用途复用公共步骤可选功能、例外流程

记不住的时候,问一句:「这一步能不做吗?」 不能不做 → include;可能不做 → extend。

4 泛化:把共同的部分提到父级

泛化(generalization) 是把多个元素共同的部分抽象成一个父级。用例模型里有两个地方能用到:

4.1 参与者泛化

① 参与者泛化(generalization of actors)。 本例里就是「客服」和「片区经理」:两人共用一整套用例(看待派单、看排期、改约),区别只在经理多几个「看报表」的用例。

做法是把共同的放到父级「门店员工」,子级只画差异。

text
       门店员工 (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 怎么选

选哪种取决于哪一类问题更容易推翻已有的设计。如果例外流程经常改主线,那就该先做分支 —— 因为先做的主线很可能白做。

本例恰好属于后者:「缺料」这个例外几乎改写了整个主流程(工单要转「待件挂起」、客户要改约、仓管要催货)。如果先做主线再发现这一点,主线要返工。所以本例该先做分支。

本节自测

  1. 判断用 include 还是 extend,只需要问哪一个问题?
  2. 「派单」需要知道「撤回申请」的存在吗?这个问题的答案说明了什么?
  3. include 和 extend 的箭头方向分别指向哪一方?各用什么记法记住?
  4. 参与者泛化和用例泛化分别在什么情况下有用?本例中哪个建议用、哪个不建议?
  5. 精炼到什么程度就该停下?讲义给了哪三条具体的刹车条件?
  6. 什么情况下应该先细化例外流程,而不是主线?本例属于哪种?

参考答案

  1. 「这一步能不做吗?」 不能不做 → include;可能不做 → extend(§3)。
  2. 不需要 —— 「派单」该做的就是派单,撤回是叠加在「申请配件」上的可选项。这说明了主用例不必知道扩展用例的存在;反之若主用例必须知道对方才能跑通,那就不是 extend 而是 include(§2、§3)。
  3. include:基用例 → 被包含用例,记法「我需要它」;extend:扩展用例 → 基用例,记法「我附加到它」(§1.2、§2.2)。
  4. 参与者泛化用于两人共用一整套用例、只是权限不同的情况 —— 本例「客服」与「片区经理」建议用;用例泛化用于流程本身不同、共同部分很多的情况 —— 本例三种家电报修只是表单字段不同,不建议用,一个用例加一个「家电类型」字段就够(§4.1、§4.2)。
  5. 停的判据是 「业务方看图的难度上升了就不值得」。三条具体刹车:不要为只涉及一个屏幕的行为建 include/extend;include 嵌套不超过一层;extend 嵌套不超过一层(§5)。
  6. 当例外流程容易推翻主线设计时该先做分支。本例属于这种 —— 「缺料」这个例外几乎改写了主流程(工单转待件挂起、客户改约、仓管催货),先做主线会返工(§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过度抽象抽象层次过多,反而降低可读性

© 2026 HuanYu Blog

记录技术,也记录思考