软件分析与设计笔记 22:分析建模:边界、实体与控制对象

不经过分析直接写代码会踩什么坑、三种分析对象各管一段、怎么用顺序图完成一次用例实现,以及从顺序图导出类的属性、操作与关系的方法。

学习#分析建模#面向对象#UML#学习笔记

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

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

贯穿案例:片区家电维修派单系统。21 篇讲了 UML 的符号,这一篇用它做一次真正的分析建模 —— 产物就是 27 篇考试里那道顺序图题。

0 跳过分析直接写代码会怎样

需求给了、数据库也建好了,直接开始写代码 —— 这是很多项目的实际做法。它通常在三到六个月后付出代价:

  • 界面上的一段逻辑,被复制到三个页面里(因为写的时候没想清楚该放哪)。本例:客服端和工程师端都要「校验工单是否进行中」,结果这段判断写了两遍;
  • 业务规则散落在控制器、数据访问和界面代码里。本例:「库存不足就转待件挂起」这条规则如果写在页面里,仓管端就没法复用;
  • 想做一个「批量导入」,发现没有地方可以复用已有的处理逻辑。本例:想给仓管做「批量出库」,发现出库逻辑埋在单个出库的页面事件里。

分析建模(analysis modelling) 就是在需求和代码之间插一层:先想清楚每个用例由哪些对象协作完成。

1 它在流程里的位置

阶段回答的问题产物
需求建模(requirements modelling)系统要做什么用例模型、活动图
分析建模(analysis modelling)由哪些对象协作完成分析模型(类图 + 顺序图)
设计建模(design modelling)怎么落到平台与数据上设计模型(含表、访问层)

分析建模有个重要特征:它刻意不关心实现平台。不讨论用哪个框架、哪张表、什么字段类型 —— 只讨论职责怎么分。先把「该谁做」定下来,再决定「怎么做」。

这一点在本例里有个很具体的表现:分析模型里写「:PartsController 调用 :StockItem.getStock()」,不写这个调用是走数据库还是走缓存,也不写 getStock() 返回的是对象还是 DTO。这些等设计阶段再说。

分析建模里的三种对象与用例实现:边界对象负责系统与外部交互,实体对象承载业务数据与规则,控制对象协调用例流程;下方用一个顺序图示意如何从对象之间的消息导出类与操作,并标出三条关系限制

2 三种分析对象

分析对象(analysis object)通常按职责分成三种构造型(stereotype):

2.1 边界对象

① 边界对象(boundary object) —— 系统与外部交互的地方:界面、外部接口。它负责接收输入与展示结果,不负责业务规则。

它的一个重要特征是改动最频繁:界面几乎每次迭代都在变。所以业务规则一定不能写在边界对象里,否则每次改界面都要重新验证业务逻辑。

本例里有两个边界对象,而且是两种不同的边界:

  • :PartsForm —— 人的界面(工程师填申请的那个表单);
  • :SupplierAPI —— 系统的接口(去查供应商库存的那一端)。

两者都叫 boundary,但性质完全不同:前者会因为「按钮放左边还是右边」而改,后者会因为「供应商改了接口版本」而改。区分它们的意义在于:改动的触发源不同,所以不能混在一个类里。

2.2 实体对象

② 实体对象(entity object) —— 业务上需要长期记住的东西,也就是笔记 09 里的业务实体。它承载数据与规则,通常能对应到数据表。

本例的实体是 :WorkOrder(工单)、:StockItem(库存项)、:PartUsage(配件消耗)、:IssueNote(出库单)。

这里有个关键点值得强调:业务规则应该落在实体上,而不是控制对象上。「工单在什么状态下允许申请配件」这条规则属于 :WorkOrder,因为它管的是工单自己的状态;如果写进控制器,那么别的用例(比如批量出库)想复用这条规则就复用不了。

2.3 控制对象

③ 控制对象(control object) —— 协调一个用例的执行顺序:接收请求,判断该交给谁处理(这个动作就是委派,delegation),再汇总结果。它本身不该堆积业务规则。

一般给每个用例配一个控制对象,命名上直接对应::RequestPartsController 实现「申请配件」这个用例。这个名字本身就能追溯 —— 看到类名就知道它服务哪个用例。

控制对象是三者里最容易失控的 —— 因为它「什么都管」,很容易变成什么都往里塞的上帝类(god class)。一个判断方法:如果这个类的方法里出现了大量「先查 A、再判断 B、然后更新 C」,那这些逻辑大多属于实体对象。

本例里一个具体的越界:如果 :RequestPartsController 里写了「如果数量大于 20 就报错」,这条规则就放错了 —— 它属于 :PartUsage。控制器只该做「收到申请 → 让 :PartUsage 自己校验 → 根据结果决定下一步」。

3 用顺序图做一次用例实现

3.1 做法

把「每个用例由哪些对象协作完成」画出来,这个过程叫用例实现(use case realization)。

做法是:取一个用例的主流程(basic flow),逐个步骤问「这一步由谁执行」,把参与者与对象的交互画成顺序图。

3.2 本例完整走一遍:申请配件

08 篇那个「申请配件」的主流程有 10 步。逐行问「谁执行」,得到这样一张对照:

flow 步骤谁发起发给谁消息
1. 工程师提交申请:Engineer:PartsFormsubmit(workOrderId, partNo, qty)
2. 校验工单状态:PartsForm:RequestPartsControllerrequestParts(...)
3. 查询当前库存:RequestPartsController:StockItemgetStock(partNo)
4. 调用供应商接口:RequestPartsController:SupplierAPIqueryAvailable(partNo)
5. 比对判断:RequestPartsController自己自身方法
6. 生成出库单:RequestPartsController:IssueNotecreateIssueNote(...)
7. 写入工单:RequestPartsController:WorkOrderaddPartUsage(...)
8. 通知仓管:RequestPartsController:Storekeepernotify()
9. 显示成功:RequestPartsController:PartsForm返回结果

这张表就是顺序图的草稿。注意第 4 步的接收者是 :SupplierAPI(boundary),不是实体 —— 判断依据在 21 篇讲过:「你需要写代码去和它对接,那它是系统外部接口」。

把这张表画成图,就是本例的用例实现:

申请配件的分析模型顺序图:五条生命线从左到右依次是工程师、申请界面、配件控制器、库存项与供应商接口,构造型分别是 actor、boundary、control、entity、boundary。消息按 1 到 9 编号,分别是提交申请、转控制器、查本地库存并虚线返回、调供应商接口并虚线返回、控制器自身比对、创建出库单、写入工单的配件消耗、通知仓管、返回成功消息给界面。底部标出三个观察点:控制器的委派比例健康、外部系统是边界对象、通知仓管这一步可疑

3.3 顺序图暴露了什么

画完这张图,有两件事立刻变得清楚:

① 控制器确实在协调,但没有堆积规则。它做了 4 次委派(去库存、去供应商、去出库单、去工单),自己只做了「比对」这一个判断。这个比例是健康的。

② 第 8 步「通知仓管」是个可疑点。仓库通知是「申请配件」这个用例的一部分吗?还是应该独立成一个用例?

讨论的结论是:应该是独立的。因为通知的渠道和时机(短信 / App / 邮件)与配件申请无关,而且仓管可能想改通知方式而不影响申请流程。所以分析模型里保留 notify(),但设计时它可以变成一个独立的消息服务。

这就是分析建模的价值:它让你在写代码之前就发现「这里可能该切开」。

4 从顺序图导出类

4.1 导出规则

顺序图画清楚之后,类图基本就出来了 —— 导出规则是机械的:

顺序图里的东西对应到类图
每个参与对象一个类
对象收到的消息该类的一个操作
需要记住的数据类的属性
谁调用谁类之间的关系

按这张表,从上面那张顺序图导出的类图是这样的:

类构造型操作(来自收到的消息)
PartsFormboundarysubmit()、displayResult()
RequestPartsControllercontrolrequestParts()
StockItementitygetStock()
SupplierAPIboundaryqueryAvailable()
IssueNoteentitycreateIssueNote()
WorkOrderentityaddPartUsage()

注意这三个实体的操作全都是「名词 + 动词」的形式,而且都跟自己的数据有关 —— 这正是 21 篇强调的「行为跟着数据走」。

4.2 为什么先画顺序图

这也是为什么推荐先画顺序图、再画类图:如果反过来,先列一堆类再想它们怎么协作,很容易列出一批「看起来合理但用不上」的类。

本例里可以验证这一点:如果先凭空列类,很容易列出「NotificationService」「InventoryManager」「ValidationHelper」这类看起来专业的类 —— 但它们在这条流程里一个都用不上(通知只有一行 notify(),校验只有 qty > 20 一个判断)。

用了才建,不用不建。这是分析建模最实用的一条纪律。

5 关系上的限制

5.1 不是所有关系都能随便用

从顺序图导出关系时,有一点要留意:不是所有关系都可以随便用。

  • 依赖 —— 出现在方法参数里,最轻;
  • 关联 —— 需要长期保留引用时才用;
  • 聚合 / 组合 —— 要有明确的整体—部分语义。

一个常见错误是「为了让图好看,把所有关系都画成组合」。判断依据还是那句:整体没了,这个部分还活得下去吗?

本例里 :WorkOrder 与 :PartUsage 是真组合(配件消耗随工单消亡),而 :PartUsage 与 :StockItem 只是引用(库存项独立存在,配件消耗只是记了个 partNo)。这两个如果都画成组合,实现的人会以为删工单要删库存项 —— 那是灾难。

5.2 分析模型的三条硬限制

讲义明确列出的三条,属于「画了就扣分」的类型:

关系能不能用本例的说明
boundary 直接关联 entity❌ 不推荐:PartsForm 不能直连 :StockItem,中间必须隔着 :RequestPartsController
actor 与 entity / control 直接关联❌ 不推荐:Engineer 只接触 :PartsForm,不能直接连 :WorkOrder
不同构造型之间做继承 / 聚合 / 组合❌ 无意义「控制器继承实体」这种关系没有业务含义

第一条的理由值得说清:如果界面能直接拿数据,那「业务规则该放哪」这件事就彻底失守了。因为一旦 :PartsForm 能直接读 :StockItem,它就会顺手在那里写判断,然后这段判断再也不会被别处复用。

5.3 关系方向要反映调用方向

还有一条实践建议:关系的方向要能反映调用方向。如果 A 调 B、B 又调 A,那张图上会出现环 —— 环通常不是错,但值得停下来看看是不是职责切分有问题。

本例的分析模型是无环的::Engineer → :PartsForm → :RequestPartsController → 各实体。这个单向性是健康的 —— 它说明职责分得清楚,没有互相依赖。

6 类图怎么文档化(documenting the class diagram)

分析模型最终要交出去,类图的说明通常有三种组织方式:

  1. 每张图配一段文字 —— 逐个类说明职责。适合类少的场景;
  2. 统一一张职责表 —— 类名 + 职责 + 关键操作 + 协作对象。适合类多的场景,也便于对照检查职责有没有重叠;
  3. 在图上直接标注 —— 把职责写在类旁边的注释框里。适合评审时快速对齐。

本例用第二种最合适,因为类不多不少(六个):

类一句话职责协作对象
PartsForm接收工程师的申请输入,显示结果RequestPartsController
RequestPartsController协调一次配件申请的执行顺序其余五个
StockItem记住某个配件的当前库存与安全库存RequestPartsController
SupplierAPI对接供应商,返回实时可售量RequestPartsController
IssueNote记录一次出库(谁、何时、多少)RequestPartsController
WorkOrder记录工单状态与它用掉的配件RequestPartsController

三种方式可以混用。关键是每个类的职责必须有一句话能说清楚 —— 说不清,通常意味着这个类做了两件不相关的事。

上表里如果有哪一行写不出来,就该回去检查那个类了。

本节自测

  1. 分析建模和设计建模的关注点差别是什么?
  2. 业务规则为什么不该写在边界对象里?
  3. 怎么判断一个控制对象是不是已经变成上帝类?
  4. 从顺序图导出类图时,「对象收到的消息」对应到类图的什么?
  5. 为什么推荐先画顺序图再画类图,而不是反过来?
  6. 本例里 :PartsForm 和 :SupplierAPI 都是 boundary,为什么要分成两个类?
  7. 本例分析模型为什么是「无环」的?有环说明什么?

参考答案

  1. 分析建模只讨论「职责怎么分」,不关心实现平台(不讨论框架、表、字段类型);设计建模才把这些落到数据库、访问层与框架上(§1)。
  2. 因为边界对象改动最频繁(界面每次迭代都在变)。业务规则写在那里,每次改界面都要重新验证业务逻辑,而且别的用例无法复用(§2.1)。
  3. 看它的方法里是不是有大量 「先查 A、再判断 B、然后更新 C」 —— 这类逻辑大多属于实体对象。本例反例:控制器里写「数量大于 20 就报错」,这条规则应属于 :PartUsage(§2.3)。
  4. 对应到该类的一个操作(operation)。本例 :WorkOrder 收到 addPartUsage() 消息,所以它就有 addPartUsage() 这个操作(§4.1)。
  5. 因为反过来先列类,很容易列出一批 「看起来合理但用不上」 的类。本例若凭空列,会列出 NotificationService、ValidationHelper 等,但流程里它们各只有一个方法(§4.2)。
  6. 因为它们的改动触发源不同::PartsForm 因「按钮放哪」而改,:SupplierAPI 因「供应商接口改版」而改。混在一个类里会让两种变化互相牵连(§2.1)。
  7. 因为调用方向是单向的::Engineer → :PartsForm → :RequestPartsController → 各实体。有环说明职责切分有问题(A 与 B 互相依赖)(§5.3)。

术语表

术语中文一句话解释
analysis modelling分析建模确定每个用例由哪些对象协作完成
analysis object分析对象分析模型里承担一种职责的对象
stereotype构造型给模型元素附加语义的分类标记
boundary object边界对象系统与外部交互的对象,如界面与外部接口
entity object实体对象承载业务数据与规则的对象
control object控制对象协调用例执行顺序的对象
use case realization用例实现用对象协作把用例的流程具体化
basic flow主流程一切顺利时的步骤序列
delegation委派把请求转给该处理它的对象
god class上帝类职责过多、什么都管的类
documenting the class diagram类图文档化用文字、表格或注释说明每个类的职责

© 2026 HuanYu Blog

记录技术,也记录思考