Skip to content
On this page

第7章 建模步骤C-1 识别类和属性

← 返回《软件方法》

8.2 建模步骤 C-1 识别类和属性

8.2.1 面向对象

面向对象就是一些假设。 如果不认可“面向对象”的假设,也可以分析系统的核心域知识, 只不过用的方法不叫“面向对象方法”,叫“面向过程”、“面向组件”、 “面向肥皂”、“面向武德”都行,看你的假设是什么了。

面向对象的思考方式比目前的其他思考方式要好一点,原因不是计 算机喜欢面向对象或者面向对象更接近于计算机的底层,而是面向对象 的思考方式更能帮助人脑去剖析复杂问题。如果计算机有感情,估计它 应该更"喜欢"人类用机器语言直接给它发指令,因为这样自己就不用受 累搞什么编译、链接。

面向对象更能帮助剖析复杂问题,不意味着面向对象的思考方式比 其他的思考方式更容易掌握,而且随着你掌握了更强有力的思考工具, 更复杂的问题就会扑面而来。这些问题早已存在,只不过之前你没有能 力来发现和对付它们——“古人很少死于癌症”。

正如第 1 章在批评伪创新时所提到的,三角函数更能解决复杂问 题,不意味着它比全等三角形、相似三角形更容易掌握。

伪创新经常把“简洁”和“容易学”混淆,动不动嚷嚷“大道至简”, 其实背后意思是“不动脑子也能学会的才是好方法”。 像下面这些足够“简洁”而且可以解决之前困扰人们的一大片难题,

但并不“容易学”。

线性方程组有解<->系数矩阵与增广矩阵有相同的秩

方程存在根式解<->方程的群存在因子全为素数的子群系

当使用面向对象的方法来分析系统时,我们引入的第一个假设是:

系统由"对象"这样一种东西构成,对象封装了数据和行为。

严格来说,我们建模的是类(也许叫“基于类的方法”更合适), 即对象的“模板”,而不是对象。对象是运行时(或模拟运行时)才产 生的。

我们通过抽象思维把具有共同特征的对象集合归纳为"类",对象看 作类的实例。归类是人类认知的一种基本技能,其哲学讨论可以追溯到 柏拉图的理型论(Theory of Forms)。

我们引入的第二个假设是:

对象在一个"对象空间"中运行,在这个空间中发生的所有事情消 耗的时间为零。

您可以认为这个"对象空间"存在于大脑中,也可以把"对象空间"想 象成一台存储空间无限大,通信和运算速度无限快且分布在全宇宙的超 级计算机。在这个假设下,不用考虑什么硬盘、内存、Cache、加载, 只需要聚焦于思考核心域知识。

当然,“时间为零”、“无限快”是不可能的。我们的宇宙,目前因 果关系的最快速度是光速。

当前现实中的计算机和网络,要发生一段对人类有意义的因果关系, 例如,从提交关键词到返回查询结果,时间估计会以秒来计,不过,作 为人类的涉众可能对此已经表示满意了。

如果当前的计算机和网络资源能够满足人们对性能的要求,那么设 计模型(代码、存储……)和分析模型之间映射会非常直接。

反之,如果出现不可调和的性能问题,设计模型可能会有所调整, 例如,添加一些冗余,但这样的调整和具体的核心域知识无关,可以把 它们归纳成一些套路,出现相应问题时按照套路调整即可,不需要在分 析时考虑这些问题。 “不考虑性能”这一点,可以用来判断你思考的问题是分析问题还 是设计问题。

我们可以针对分析模型里的元素,一个一个问,“如果没有它,会 怎么样”,如果回答是“会有性能问题”,那么,可以从分析模型中把它 删掉。

例如,类图中有一个冗余的类,问“如果没有它,会怎么样”,答 “查询可能会慢”——可以删掉。状态机图里有一个状态“Transient”, 问“如果没有它,会怎么样”,答“会漏掉某些数据没有持久化”—— 可以删掉。

但如果回答是“没有它,系统就没法履行自己的责任了,因为要做 的系统就是一个持久化框架”,那就不一样了。

8.2.2 三种分析类

8.2.2.1 Ivar Jacoson 的假设

我们再引入一个假设:

系统中存在三种分析类:边界类(Boundary Class)、控制类(Control Class)和实体类(Entity Class)。

这个假设借用了 Ivar Jacoson 在“Object-Oriented Software Engineering: A Use Case Driven Approach”(Jacobson 1992)中 的思想。其他面向对象方法学可能并不认同这个假设,也就是说,面向 对象的分析不一定需要这个假设。

在 UML 模 型 中 , 我 们 可 以 用 Ivar Jacoson 建 议 的 构 造 型 (Stereotype)来表示三种分析类.

一些 UML 工具(如 Enterprise Architect、Visual Paradigm)已 经内置了这些分析类构造型。如果使用的建模工具没有内置这些构造型, 可以自己添加如“边界”等文字构造型;或者不用构造型区分, 通过给类起名"某某接口","某某控制",也有助于了解该类在系统中扮 演的角色。这一点,和第 3 章讲到业务工人、业务实体时的做法是一 样的。

在设计工作流,三种分析类可以映射到任何实现架构,包括但不限 于 MVC、MVP、MVVM、六边形、洋葱型……甚至映射到不做任何分 割的“架构”=。

构造型责任和需求的关系命名
边界类输入、输出以及简单的过滤每个有接口的外系统映射一个边界类。外系统名称+接口
控制类控制用例流,为实体类分配责任。每个用例映射一个控制类。用例名称+控制
实体类系统的核心,封装核心域逻辑和数据。用例和实体类的关系是多对多的,一个用例可以由一到多个实体类协作实现,一个实体类可以参与一到多个用例的实现。领域概念名称

执行者先把消息发给边界类对象。边界类对象履行它有能力履行的 责任,然后把它没有能力履行的责任委托给控制类对象。控制类对象就 像总裁办,不做具体工作,只是将责任分解后分配给实体类对象。

分配给实体类对象时,如果某个对象被其他对象组合,应该先分配 给组合它的对象,再由该对象分配给它。DDD 话语体系中的“聚合 (Aggregate)”和这一点类似,本书在后文讲述类关系的章节中会进 一步阐述其中差别以及“聚合(Aggregate)”的伪创新。

最后,由边界类对象向外系统反馈信息,完成一个交互回合。

8.2.2.2 关于边界类

边界类的责任是接受输入、提供输出以及做简单的过滤。

边界类的映射方法——每个有接口的外系统映射 一个边界类。这里说的"有接口的外系统"不仅包括系统执行者,还包括 仅接受系统输出信息的外系统。

以 2018 版《软件方法(上)》UMLChina 系统案例中的"时间→发 送公开课通知"用例为例。该用例进行过程中,系统会向软件开发人员 发送公开课通知,同时还要向 UMLChina 助理反馈发送通知的进展。 软件开发人员和 UMLChina 助理在这个用例中仅仅是接受输出,没有 输入信息给系统,但系统可以分别设置一个边界类来封装向软件开发人 员和 UMLChina 助理反馈信息的责任。

外系统如果是人,对应的边界类也可以叫“XX界面”,例如“助理 界面”。本书就不区分了,一律起名“XX接口”。

分析工作流的边界类不暗示任何实现方案。在总责任相等的前提下, 它和实现的映射是多样的,可以是图形、文本、语音、远程调用…… 即使使用图形界面实现,也不能简单认为一个边界类对应一个窗体 (Form)。一个边界类的责任可以拆解到多个窗体上,一个窗体也可以 和多个外系统交互。如何组织这些责任,应该从外系统的角度来考虑, 而不是从用例或实体类的角度来考虑。

“助理接口”边界类被圈住的几个责任来自不同用例 的步骤,但在使用图形界面实现时,可以放在面向助理的、通知专用的 同一个窗体中。

类似的例子还有:一份申请,需要通过系统审批三次,也就是三个 不同的用例。在图形界面实现中,可能不需要准备三个窗体,部门主管、 财务、副总三个审批人可以在同一窗体上工作,但部门主管、财务、副 总各自有对应的分析边界类。

如果某个外系统和系统的交互很多,对应边界类的责任可能会有很 多。有的做法推荐按"外系统+用例"的组合映射边界类,这样可以减少 一个边界类上的操作个数。本书不推荐这样做,因为这已经隐含着先入 为主“按用例划分边界”的意思,不利于最后得到合理的边界。

尽量保持一个外系统映射一个分析边界类,如果操作很多,可以将 从外系统角度观察可能要分在一组的操作移到一起,EA 等工具可以随 意定制属性和操作的上下显示顺序。

需要提醒的是,外系统映射的只是边界类,并不映射实体类。在外 系统是人的时候,经常会有人犯这样的错误。例如以下用例规约片段:

  1. 助理选择公开课,请求创建通知任务
  2. 系统验证所选公开课适合创建通知任务

“助理”是执行者,映射一个边界类“助理接口”是可以的,但如 果映射一个“助理”类,那就错了.系统是否需要一个“助理”类,要看系统是否需要维护助理的信息。 如果需要,会在某个用例规约的某个地方体现,例如,可能会有一个步 骤:

  1. 系统保存通知任务

绑定一个字段列表: 7. 通知任务=4+创建时间+创建人

这个“创建人”就是助理,说明系统需要记住助理的信息,这时才 会有“助理”类。

但并不是所有的系统都需要保留人的信息。例如,乘客坐电梯上楼, 乘客是电梯系统的执行者,但电梯系统可能不需要"乘客"实体类,因为 它不需要记住乘客的信息。

当然,有朝一日,电梯升级为防疫电梯,用例规约里有:

4 乘客提供身份标识 5 系统验证身份标识合法 6 系统记录乘客信息和入厢时间

这时,电梯系统里就有"乘客"实体类了,因为系统要记住乘客的信 息。

当然,虽然电梯系统没有"乘客"类,但会有"乘客接口"类,可能的 类图和常见的实现方式.

8.2.2.3 关于控制类

控制类相当于用例在系统中的“代理”,它的责任是控制用例流, 为实体类分配责任。如果在分配责任时发现控制类只起到传递的作用, 没有起到分解和分配的作用,也可以把控制类去掉。

因为每个用例直接映射一个控制类,可以用“用例名称+控制”来 为控制类命名。因为构造型已经包含“边界类”、“控制类”的概念,严格来说, “员工接口”、“审批控制”类名后面的“接口”、“控制”可以省掉,在分析 映射设计时,再通过映射套路把它补上去。不过,考虑到可能还会有“员 工”、“审批”实体类,而且很可能会一起出现在同一张序列图上,仍然 保留“接口”、“控制”的尾巴看起来更顺眼一些。

8.2.2.4 关于实体类

边界类与外系统、控制类与用例的映射关系很明显,所以识别边界 类和控制类不需要思考,直接按照上面的套路映射即可,甚至可以先不 映射,推迟到画分析序列图时再加上去。

有的分析方法学如 ICONIX 提倡一种 Robustness Diagram,认 为可以通过它来帮助寻找类。开发人员一用确实感觉很舒服,噼里啪啦 就发现好多类,有一种"我已经取得了不小成绩"的错觉,不过要是仔细 看看,就知道"发现"的大多是边界类、控制类。这些类其实用不着刻意 去发现,只要按照套路映射即可。

最难的工作——寻找实体类以及它们之间的协作,Robustness Diagram 却是寥寥带过,甚至容易误导建模人员把实体类和用例一一 对应。所以,本书不推荐开发人员额外花时间画 Robustness Diagram。

和前文多次提到的一样,凡是不需要思考就可以得到很多“成果” 的“方法”,都容易成为懒人摸鱼的遮羞布。

建模人员的思考工作量应该花在识别实体类上。一个用例需要哪些 实体类协作实现、如何协作,一个实体类会参与哪些用例的实现,这是 一个多对多的映射,需要由建模人员的大脑决定哪种映射最好。

因此,本章以下内容提到的“类”,缺省意思为“实体类”。

边界类、控制类的命名有“XX接口”、“XX控制”的尾巴,实体类的 命名就不要尾巴了,直接用核心域概念命名即可。例如,命名为“员工” 而不是“员工实体”。

8.2.2.5 不存在“系统”这个类

"系统"的概念是需求工作流的概念。在需求工作流,我们把系统看 作一个对外提供服务的整体。在分析工作流,"系统"的概念已经被打碎 成很多个类,"系统"这个词不需要识别成类。

在业务建模工作流,研究范围是组织,而组织中有很多系统,在业 务序列图上提到目标系统时,只是说“系统”二字无法让人理解指的是 哪一个系统,需要写出目标系统的名字“XXX系统”。

在需求工作流,研究范围是目标系统整体,此时,聚光灯已经打在 目标系统身上,不需要再写目标系统的名字,写“系统”二字即可。

就像公司开表彰会,老总宣布优秀员工名单时,要说名字“罗永昊”、 “罗阵宇”,轮到优秀员工罗永昊上台发言时,罗永昊称呼自己就不好 再说“罗永昊”,说“我”、“在下”、“小弟”就行了。

*用自己的名字以及第三人称来称呼自己,往往代表一种极度的“自 信”。例如:

“凯撒注意到这事,把他的军队撤到最近的一座山上去”(《高卢战 记》)

“老胡觉得这件事实在不应该” “婷婷想吃冰淇淋嘛”*

而在分析工作流,研究范围是系统的内部构成。系统已经被分解成 很多个类,这时就不能再说“系统”了。

8.2.3 提炼类和属性

8.2.3.1 从需求规约之外的其他素材提炼

建模类及类的关系,可能在软件开发的分析工作流之前就已经发生 了。

第 7 章“需求启发”中就提到,我们在研究资料的时候,可以通 过画类图来整理领域的概念。整理领域概念时,有时还可以加上状态机 图。即使不是为了开发软件,也可以通过这些手段来整理领域知识,帮 助我们更快地理解和掌握领域知识。

以刘慈欣的《三体 III:死神永生》中的片段为例,原文如下,我 们把其中值得提炼的概念标红:


宇宙的熵在升高,有序度在降低,像平衡鹏那无边无际的黑翅膀, 向存在的一切压下来,压下来。可是低熵体不一样,低熵体的熵还在降 低,有序度还在上升,像漆黑海面上升起的磷火,这就是意义,最高层 的意义,比乐趣的意义层次要高。要维持这种意义,低熵体就必须存在 和延续。

至于这意义之塔的更高端,不要去想,想也想不出什么来,还有危 险,更不用说意义之塔的塔顶了,可能根本没有塔顶。

回到坐标上来,空间中有许多坐标在穿行,如同母世界的天空中飞 翔的矩阵虫。坐标拾取由主核进行,主核吞下空间中弥散的所有信息, 中膜的、长膜的和轻膜的,也许有一天还能吞下短膜的。主核记着所有 星星的位置,把信息以点阵方式与各种组合的位置模式进行匹配,识别 出其中的坐标。据说,主核可以匹配五亿时间颗粒前的位置模式,歌者 没有试过,没有意义。在那个遥远的时代,宇宙中的低熵群落比较稀疏, 也还都没有进化出隐藏基因和清理基因。而现在——

藏好自己,做好清理。

但所有坐标中,只有一部分是有诚意的。相信没有诚意的坐标常常 意味着清理空旷的世界,这样做浪费精力,还有一点点害处,因为这些 空世界以后还可能用得着。无诚意坐标的发送者真是不可理喻,它们会 得到报应的。


根据以上描述,可以画出如图 8-27 的类图来整理其中的领域知识。 整理后,可以看出各个概念之间的关系,还可以看出,领域知识可以分 为 4 个部分。

text
       图 8-27 用类图整理《三体 III:死神永生》中的片段
@startuml
left to right direction
class 对象 {
    - 熵
    - /有序度
}
class 宇宙
class 世界
class 低熵体
class 熵变化趋势 {
    上升
    下降
}
class 基因类型 {
    隐藏
    清理
}
class 矩阵虫 {
}
class 平衡鹏 {
    - 翅膀
}
class 空间
class 坐标 {
    - 诚意
}
class 主核
class 信息
class 膜长类型 {




}
class 位置模式
class 位置 {
    - 点阵
}
class 星星 
' 关系
对象 -- 宇宙 : 1
宇宙 -- 世界 : *
世界 --> 低熵体 : 母世界
低熵体 --|> 对象
低熵体 --熵变化趋势 : 1
低熵体 --基因类型 : 拥有
低熵体 -- 坐标 : 发送者
坐标 -- 主核 : 识别出
主核 -- 信息 : 吞下
主核 -- 位置模式 : 匹配
位置模式 -- 位置 : 1
位置 -- 星星 : *
坐标 -- 信息 : 1..*
信息 -- 膜长类型 : n..1
@enduml

图 8-27 可以称为“宇宙的领域模型”。这个模型可能和信息系统 没有关系,因此不能称为“宇宙的核心域模型”或“宇宙的分析模型” 。

如果某个信息系统“三体游戏”要封装图 8-27 的领域知识,那么 图 8-27 可以称为“三体游戏的核心域模型”或“三体游戏的分析模型” , 但不适合称为“三体游戏的领域模型”,因为“三体游戏”中封装了核 心域和非核心域的知识。

从上面例子可以看出来,对于以上提到的几个用语,本书是按以下 定义使用的:

领域模型:描述某个领域中的概念及概念之间关系的模型。 分析模型:从核心域视角描述的信息系统的模型。 核心域模型:等同于分析模型。

它们之间的关系如图 8-28。

text
图 8-28 领域模型和分析模型
@startuml
skinparam defaultTextAlignment center
cloud "各种素材" as materials
rectangle "分析模型" as analysisModel
rectangle "领域模型" as domainModel
domainModel --> analysisModel : 分析模型的参考
materials --> domainModel : 提炼出
@enduml

模型可以用各种表示形式来表示,相应的形式可以叫“领域类图”、 “领域 ER 图”、“分析类图”、“分析状态机图”、“分析类文档”、“分析 状态机文档”…… 我们可以从各种素材中提炼某个领域的类和属性,不过这些类只能 叫领域类,不一定是合适的分析类。到底哪些领域类会成为目标系统的 分析类,需要从目标系统的需求模型(如果用本书上册的方法表示,就 是系统用例规约)来判断该系统是否需要这个类。

没有需求规约,目标系统的边界以及应该承担的责任没有理清楚, 那么,提炼得到的类到底是不是目标系统将来需要维护的概念,是没法 判断的。也就是说,虽然在很多时间点、从很多素材都可以提炼类,让 我们在确定分析类时,省去了许多思考,但分析模型的最终依据只能是 需求模型。 当然,如果你脑子里对于系统该干什么不该干什么清清楚楚,只不 过没写出来,那也算是有需求模型了——不过你得确定你真的懂,而 不是拿这个当遮羞布。

如 果 按 照 本 书 所 采 用 的 面 向 对 象 建 模 、 UML 表 示 法 和 Ivar Jacoson 三种类的分割,某个领域模型可能会包含某个系统的分析模 型中的实体类部分,但不会包含边界类和控制类部分。

另外,在用类图表达领域模型时,我们只使用泛化、关联等静态关 系,不使用动态的依赖关系,也不使用序列图来表达类之间的协作,因 为这些内容在相关的信息系统运行时才会出现,而此时我们并没有在构 思或建造信息系统。

★注意,刚才说的序列图和第 4 章的业务序列图的区别,也可以 参考图 8-25。

例如,图 8-27 中,“主核”和“信息” 、“位置模式”以及“坐标” 之间是关联关系。这是合理的,因为宇宙系统能把所有关系都记录下来, 包括哪个坐标是由哪个主核识别出来的——当然,也包括导致太阳系 被降成二维的那几次和三体世界的通信。

但是,如果我们开发一个信息系统“三体游戏”,即使游戏中有“主 核识别坐标”的内容,系统未必要记录哪个坐标是由哪个主核识别出来 的。此时,“主核”和“信息”、“位置模式”以及“坐标”之间不存在 关联关系,只存在动态的依赖关系。某种协作方式(当然可以有其他协 作方式)下的类图和序列图可能。

有些书籍和文章作者,对软件开发的工作流没有清晰的概念,把所 有用“业务语言”表达的模型,包括组织流程,系统需求规约等,通通 叫作“领域模型”。这是不正确的,我们在阅读时要注意分辨。

8.2.3.2 从需求规约提炼

这是提炼类最严格的依据了。 阅读用例规约的基本路径、扩展路径、字段列表和业务规则部分(即 所谓“功能需求”部分),针对表示名词或事件的词汇,逐个思考,这 是不是系统要记住的核心域概念?如果是,那么它是类,还是某个类的 属性?

关于“系统要记住的”概念,可以这样思考,系统需要懂得哪些概 念以及概念之间的关系,才能根据执行者的请求提供恰当的价值?

如果您有关系数据库建模的经验,也可以这样简单地思考:如果系 统需要维护的信息都采用关系数据库来保存,那么数据库里应该会有哪 些表和列?这样思考得到的表、列和实体类、属性基本上是一一映射的。 表对应类,列对应属性,行对应对象,关系对应关联。后面我们会讲到, 类模型可以直接转换成关系数据库模型,不需要再花工作量做一遍关系 数据库建模。

如果关系数据库建模技能掌握得好,得到的数据模型符合 1NF、 2NF 和 3NF,那么用关系数据库建模的思考方式得到的类图极有可能 也是合格的。

当然,我们画类图的目的是整理核心域概念和逻辑,将来映射关系 数据库只是顺带的便利。面向对象和关系数据库(或任何的具体存储方 式)没有必然的绑定关系。任何系统都可以用面向对象的方式来构造, 不管它用什么方式来存储对象。

以电梯为例。我们把为乘客提供电梯服务所需要的所有软硬部件看 作一个整体,称为“电梯系统”。

乘客在发出某次召唤之前,电梯系统要懂得各部电梯当前运行方向、 当前所处楼层、待去往的目标楼层集合以及楼层的结构,才能够合理应 对乘客的召唤。这些可以看作系统要记住的信息。

乘客姓名、乘客召唤电梯的时间、电梯曾经去过的楼层等,系统则 不需要知道。

text
图 8-31 电梯系统可能需要的实体类
@startuml
class 电梯 {
    - 方向
}
class 楼层 {
    - 层号
}
电梯 "1" -- "*" 楼层 : 当前楼层
电梯 "*" -- "1" 楼层 : 目标楼层
楼层 "-上层0..1" -- "-下层0..1" 楼层
@enduml

这些信息不一定需要被“持久存储” 。图 8-31 中的概念中,当前 楼层、目标楼层、方向的信息一直在不断变化,甚至在电梯系统停止服 务时会被清空,但不影响图 8-31 的类结构。

8.2.3.3 没有需求规约时的思考

有可能你是“敏捷”地做需求,只有一个用例图,甚至连用例图都 没有,只是说要做什么功能——当然,做这个功能的理由也是“敏捷” 的。

即使是这样的情况,前面所说的思考“系统需要懂得哪些概念以及 概念之间的关系,才能根据执行者的请求提供恰当的价值”依然有用, 但需要建模人员有更强的抽象能力。

日常工作和生活中,可以有意识训练这方面的能力。针对任何一个 系统,我们都可以思考:它需要输入什么,能输出什么,为了能把输入 变成输出,系统需要懂得哪些概念以及概念之间的关系?

例如,针对日常生活中的网约车系统,我们使用时,第一个交互可 能是这样的:

输入:上车地址和目的地址

输出:各个车辆类型的预估费用。

为了能把输入变成输出,系统需要懂得哪些概念以及概念之间的关 系?

要能输出预估费用,需要知道车辆类型的公里价格还有订单要走的 路程长度。路程长度需要通过上车地址和目的地址来计算,这个计算又 涉及到很多和网约车并非特定相关的复杂概念,由专门的组件来完成是 更合适的。思考结果如图 8-33。

text
图 8-33 网约车系统思考结果
@startuml
class 订单 {
    - 预估路程长度
}
class 地址
class 车辆类型 {
    - 公里价格
}
class 地图组件
订单 "1" -- "*" 地址 : 上车地址
订单 "1" -- "*" 地址 : 目的地地址
@enduml

读者可以自行做类似思考,例如,以取款机为研究对象: (1)如果输入为:账号、密码、取款金额,要提供取现金的价值, 系统需要懂得哪些概念及关系? (2)如果进一步减少输入,改为输入:账号、密码, 系统需要 懂得哪些概念及关系? (3)如果把输入改为刷脸, 系统需要懂得哪些概念及关系?


如果没有这样的抽象能力,很容易出现“类图长得像用例图”的现 象。

例如,一个电商系统,开发人员一开始心里有一个想法,要做一个 “搜索商品”的功能。那么要实现这个功能,需要什么类呢?开发人员 干脆就按看到的表面现象来找类,得到的类图长得非常像用例图。

但是,系统之所以能够为顾客查询“屏幕尺寸为 6 英寸的 Android 手机”,不是因为它记住了“哪位顾客查询过什么商品” ,而是因为它 记住了各种商品的类别和特征。我们需要的、更合适的类图应该类似于 图 8-35。

图 8-35 更合适的类图
@startuml
class 商品 {
}
class 商品类别 {
}
class 商品特征 {
}
class 商品类别特征 {
}
商品 -- 商品类别
商品 -- 商品特征
商品类别 -- 商品类别特征
商品特征 -- 商品类别特征
@enduml

如果拥有上面所说的思考能力,看到长得像用例图的类图心里就会 响起警铃。影片店系统要提供“租借影片”的价值,系统需要懂得的关 键信息很可能并非哪位顾客租借了哪部影片, 而是其他的信息,如影片的价格体系和库存、顾客的等级。 有了这个警铃,就不用多此一举,先假装弱智写出弱智代码,再“重 构”成稍微聪明的样子——这里的强行降智和第 1 章所提到的伪创新 “割裂历史”类似。

8.2.3.4 长得像用例图的类图

系统的需求可以看作解决组织问题的解决方案,而系统的类可以看 作解决系统需求的解决方案。 如果一个解决方案不需要什么思考就可以得到,要么这个解决方案是错的, 要么要解决的问题价值已经很小。

长得像用例图的类图,需要的思考非常少,这样的类图很可能带来 的价值是非常小的。要么类图错了,要么系统的需求已经没有多少价值。

上文说到,“顾客→查询商品”的用例对应的合适类图是 图 8-35。

我们可以进一步思考,如果“哪位顾客在什么时间查询了什么商品” 成为系统需要记住的关键信息,那么图 8-38 的类图是合适的:

text
   图 8-38 “哪位顾客在什么时间查询了什么商品”成为系统需要记
              住的关键信息
@startuml
class 顾客
class 查询 {
    - 时间
}
class 查询条件 {
    - 名称
    - 值
}
class 查询结果
class 商品
顾客 "1" -- "*" 查询 : 发起
查询 "1" -- "*" 查询条件 : 使用
查询 "1" -- "*" 查询结果 : 得到
查询结果 "*" -- "1" 商品 : 指向
@enduml

此时,系统的用例很可能并非“顾客→查询商品” , 而是类似于图 8-39:

text
 图 8-38 对应的合适用例图
@startuml
skinparam defaultTextAlignment center
actor "市场人员" as 市场人员
actor "顾客" as 顾客
(查询顾客喜好) as 查询顾客喜好
(重做之前的查询) as 重做之前的查询
市场人员 --> 查询顾客喜好 
顾客 --> 重做之前的查询
@enduml

8.2.4 类和属性的命名

8.2.4.1 使用严谨的领域术语命名领域模型中的元素

领域模型中各个元素的名称尽量来自该领域的术语体系。

一个领域之所以能作为“领域”为人认知,必定会在发展过程中沉 淀出一套日益完善和精确的术语体系。每个术语有其独特的、其他术语 不能替代的含义。

例如物理学中的质量、重量、重力、引力、衰变、裂变、聚变……, 各自有各自的含义,不是另一个概念可以取代的。

涉众习惯的用语不一定是严谨的领域术语

也许是出于自己的知识局限,或者出于字数少使用方便,涉众有时 习惯于使用一些不严谨、感性的用语,这些用语不一定适合用来命名领 域模型中的元素。

这些用语经常会用颜色、大小等感性认识来称呼领域概念。例如, 货车司机可能会把一张单子称为“绿单”,因为单子的颜色是绿色的, 但更精确的名称可能是“送货单”;购物时的“小票”,名称来源于面积 较小(和面积更大的“发票”比较),更精确的名称可能是“收据”。

这些用语所依赖的信息,稳定性往往比真正的领域内涵要差。有关 机构可以改变送货单的颜色,购物收据也可以变成面积比发票要大的大 长条,“绿单”、 “小票”等用语就不再合适了。即使已经形成了习惯不 得不一直沿用下去,真实的情况和字面的意思已经大相径庭。

涉众喜欢用不严谨的用语,这是正常的。某类涉众的领域知识可能 会很片面,对领域概念认识不深刻,怎么能寄望一个使用探探来交友的 屌丝青年清楚社交六度空间理论呢?

第 7 章说到,涉众关注的是涉众利益,涉众没有资格也没有责任 提供需求。同样,涉众也没有(或更没有)资格和责任提供分析。精确 使用领域术语是建模人员的责任,不是涉众的责任。

同一领域概念,不同涉众可能会有不同的习惯用语

例如:老鼠-耗子、马铃薯-土豆、祖母-奶奶、宝贝-商品、购物- 买东西。

如果怀疑两个用语描述的是同一个概念,可以这样问:有没有不是 商品的宝贝?有没有不是宝贝的商品?如果回答都是否,就清除掉其中 一个,否则,应该继续研究两个用语背后的真正含义,必要时在模型中 表达其中差别。

  • 清理冗余的用语 几个概念中,经过审查,认为"宝贝"和"商品"、 "顾客"和"客户"含义相同,去除其中一个;"用户"、"顾客"、"会员" 含义有差别,在类图上更精细地表达。

    当然,在和人类执行者交互的界面上,针对各自的习惯,依然可以 使用不同用语如“宝贝”、“商品”称呼同一概念。

    也可以对“同一概念不同涉众用语不同”这部分知识建模,以备在 界面上使用,如图 8-40。上面提到的“宝贝”、“商品”就成为了“用 语”类的对象。

text
图 8-40 建模“同一概念不同用语”
@startuml
class 概念 {
}
class 用语 {
}
class 涉众 {
}
概念 "1" -- "*" 用语
用语 "*" -- "*" 涉众
@enduml

图 8-40 中所描述的类以及类的关系适用于各个领域,和某个特定 领域并不相关,因此不要把这部分知识和特定领域的知识混在一起。假 如建模成图 8-41,就搞混了“是一个”和“有一个”的区别,又变成 废话刷工作量了。在后面“识别类的关系”内容中会再详细讲述。

text
图 8-41 “同一概念不同用语”的错误建模
@startuml
class 商品 {
}
class 宝贝 {
}
class 祖母 {
}
class 奶奶 {
}
class 姨驰 {
}
商品 "1" -- "1" 宝贝
祖母 "1" -- "1" 奶奶
奶奶 "1" -- "1" 姨驰
note right : 同一概念不同用语
@enduml
  • 领域专家 建模人员除了和各类涉众交流,还需要从领域专家那里获取严谨的 领域知识和领域术语。

    首先要提醒的是,“领域专家”这个词已经被严重污染。

    很多人把“经验较多的涉众”当成了领域专家,例如跑运输多年的 老司机,例如通过某社交软件约了几百人的老司机。但正如上文所述, 这些涉众不一定是领域专家,他们也许能提供丰富的素材,但未必能严 谨表达和深入思考。

    ★得了某个病很多年的老患者,可能并不是这个病的领域专家; 有的久病就是久病,不会成良医。没有得这个病但研究这个病很多 年的老医生,是这个病的领域专家。

    领域专家要能够从研究学问的角度来看待他所在的领域。他应该有 该领域比较扎实的基础知识,经常阅读该领域的最新文献,了解该领域 当前研究进展。领域专家可能是该领域的“研究人员”,至少也是该领 域的“科普人员”。

    “研究人员”、“科普人员”加了引号的意思是他们不一定要有“教 授”、“研究员” 、“老师”等正式的职位,但至少满足上面所说的要求。 当然,上面提到的货运老司机、社交老司机等涉众如果能深入研究自己 所在领域的学问,也可以成为领域专家。

    领域专家很可能并不是涉众(当然也可以是)。例如,为 A 公司开 发供应链系统,建模人员可以向 B 高校研究供应链的学者请教领域知 识。学者是领域专家,但他可能跟 A 公司没有关系,这个系统最终会 是什么样子,也不涉及到他的特定利益,他并不是涉众。

    如果有条件,建模人员可以和领域专家直接交流,借助领域专家的 指点,尽快得到确实反映领域内涵的模型。如果没有条件直接交流,可 以通过阅读领域专家的各种产出,间接获得帮助。

    上文也提到,领域的术语体系是日益完善的。在熟练掌握领域现有 术语体系的基础上,辅以严谨的建模技能(背后是一些数学思想),建 模人员可以起到“反哺”的作用——帮助清理领域当前术语体系中存 在的冗余和矛盾并改善。注意其前提条件:熟练掌握现有术语体系+严 谨的建模技能,这不是随便就能达到的!

8.2.4.2 关于 DDD 话语中的“通用语言”

DDD ( 领 域 驱 动 设 计 ) 话 语 中 有 “ 通 用 语 言 ( Ubiquitous Language)”的用语,这是一个伪创新。

  • (1)类似“通用语言”的概念早已有之 “术语表(Glossary)”、“数据字典(Data Dictionary)”等内容, 几十年前的开发规范中早就已经有了。

  • (2)“通用语言”迎合了“广大开发人员”呆在舒适区的需要 那么,DDD 圈子为什么要重新发明“通用语言”,并强力吹捧呢? 我们在上文强调,建模人员要虚心学习领域知识,而这是很辛苦的 事情,很多人是不愿意吃这个苦的。 “通用语言”妙就妙在它告诉你,可以不用吃苦不用走出舒适区! “术语”的交集,用本书的话来说,就是“非核心域术语”和“核心域术 语”的交集。 问题是,有交集吗? 技术术语(非核心域术语),例如浏览器相关的术语:请求、回应、 渲染、DOM、序列化、反序列化…… 业务术语(核心域术语),例如商场相关的术语:商品、顾客、订 单、库存、收银、盘点…… “技术术语”和“业务术语”哪里有交集?莫非是把这些正交的术语随 意组合,以达到废话刷工作量的效果?此处可以回顾我们在 8.1.3 域 之间映射和协作的套路中关于 a×b×c 的陈述。 DDD 话语中所谓的“技术术语”,根本和“技术”无关,其实只 是“技术人员”在没有虚心学习领域知识的情况下编造的“业务术语” ——“技术人员编造的业务术语”。 而“通用语言”使得“技术人员”编造“业务术语”变得理直气壮。 这是一个大倒退。 现实中,难免会有“技术人员”懒得去调研涉众,懒得去学习领域 知识,乱做一通了事的现象。这是人性使然,很难杜绝,但至少我们还 知道这样做是不好的,如果要做更好,应该怎么做。 如果把偷懒变成理直气壮,味道就变了。 就像我们说“人是自私的”,这是低调描述一个事实,但如果理直 气壮地说“人不为己,天诛地灭”,味道就不一样了。 看,不是业务语言,不是工业标准(Industry Standard,译为行 业标准更好)术语,而是团队自己创建的!也就是说,同一个领域,开 发团队不同,团队里的人不同,所得到的“通用语言”可能就不一样。 观测会影响结果,这莫非是软件开发的“薛定谔的猫”?你有你的 物理学,我有我的物理学,本开发团队自有“队情”在,“技术人员” 这小日子过得可真是舒坦啊! 更妙的是,“技术人员”还会自我感动。本来我是高大上的“技术”, 现在向你“业务”开了个口子,让你也参与进来,你“业务”应该感恩 戴德了! 这是一种智力上的优越感所带来的傲慢(当然还有金钱、Quan 力, 不便展开,就不提了)。 如果某个领域的从业人员的平均智力水平(学历、学校、智商等) 不如软件开发行业,那么软件开发人员,即所谓的“技术人员”在面对 该领域的“业务人员”时,是有一种优越感的。 这种优越感让“技术人员”在做供应链系统、商场系统时有足够的 底气来编造“通用语言”,因为他觉得货车司机、仓管、外卖小哥、商 场经理的智力水平不如他。 如果所开发系统的核心域是凝聚态物理、非线性分析之类的,“技 术人员”面对智力水平超过他的“业务人员”,这份优越感就不会有了。 这时,“技术人员”可能就会虚心去学习相关领域知识,因为他觉得这 是一件高大上的事情,有面子!

  • (3)不要让利益博弈压倒“客观规律” 有一种论调值得警惕。该论调认为“通用语言”让“技术人员”一 方有了话语权,不用受“业务人员”一方主导,不用低三下四地去学习 领域知识,这对“技术人员”一方有好处。 这样的论调是利益博弈压倒了领域的“客观规律”,不可取。 如果系统的核心域模型没有准确体现领域的“客观规律”,而是发 生较大的偏离,那么最终的结果必然是该系统容易出错或者应变成本很 高。这种情况也许对某些摸鱼的开发人员有“多劳多得”的好处,但对 于整个开发团队以及涉众来说,肯定是有害的。 注意,上面说的“客观规律”加了引号,意思只是说,这些规律不 会受开发团队、实现技术等变化的影响,不代表这些规律符合科学。 最常见的,游戏中的知识体系和各种规律,像王者荣耀中的攻击、 防御、移动,是魔法不是科学,但建模人员依然要认真去学习和体会这 一套体系中的各种“学问” 。 同理,上文提到,建模技能可以帮助清理术语中的冗余和矛盾,但 仅止于此,建模技能并不能帮助判断该领域的知识是否科学。

  • (4)“语言”过于宏大 “通用语言”的“语言(Language)”这个词太大。语言要有自 己的语法,汉语算,C 算,UML 也算,“通用语言”哪里有?术语集 或术语表的称呼更合适。

8.2.4.3 核心域透镜

在为了软件开发而建模时,建模人员可能会用自己熟悉的非核心域 术语来代替不那么熟悉的核心域术语,还引以为豪。例如,面对一段集 装箱领域装箱规则的描述,建模人员立即在大脑中把它转换成自己熟悉 的概念:栈、链表、树……而且认为这是“透过现象看本质”,甚至宣 称“我就是程序,程序就是我”! Fred Brooks 在《人月神话》中引用了 James Coggins 的一段话: The problem is that programmers in O-O have been experimenting in incestuous applications and aiming low in abstraction, instead of high. For example, they have been building classes such as linked-list or set instead of classes such as user-interface or radiation beam or finite-element model. 问题是面向对象程序员在开发错综复杂的应用时,关注的是低层次, 而不是高层次的抽象。例如,他们开发了很多像链表或集合这样的类, 而不是用户界面、射线束或者有限元模型。

不同领域有不同的难题。不能因为觉得困难就想方设法逃避,对真 正要解决的核心域问题视而不见,却花精力去做那些自己熟悉的、他人 已解决的非核心域问题。

为了避免核心域概念被非核心域概念掩盖,我们可以采用一种 所示的“核心域透镜”的思考方式:如果从核心域的视角去看这 个概念,或者说把这个概念映射到核心域,我们应该得到什么概念?

对每一个用语我们都可以这样过一下:这个用语属于核心域概念吗? 如果不属于,映射到核心域概念意味着什么?

例如,“商品”有一个“加粗显示”的属性来标记 它是否加粗显示。如果核心域是“商品”相关的领域,那么“加粗显示” 不属于核心域的概念。我们可以追问:为什么要“加粗显示”,能在核 心域找到原因吗?回答可能是:因为该“商品”是“热销商品”。“热销” 才是核心域概念。 这里涉及到两个知识: 知识一:某商品是热销商品。这是商品领域的知识,已经存在很多年。 知识二:如何表示“热销”的概念。这和表示方式、时代审美、展 示的目标人群等有关。图形接口的表示和文本接口、语音接口不同,十 年前的图形接口和现在的图形接口不同,面向年轻人的图形接口和面向 老年人的图形接口不同……

8.2.4.4 命名中不带冗余内容

如果把模型元素命名中的某个部分删去,不影响建模人员对该模型 元素的认识,那么这个部分没有必要存在。

在“人员”后加一个“类”字,实际上也是把两个 不同领域的知识叠加在一起。

知识一:人员是一个类。 知识二:用 UML 表示法的图形表示,类是一个方框。

知识一和知识二是正交的。在对知识二已经有了共识的情况下,再 加上一个“类”字是冗余的。

  • 关于类和属性的命名,常犯的冗余错误有:

    • (1)在类名的最后加"类"字或在类名的前面加"Class"或"C";

    • (2)在类名中加入“领域”、“实体”等词;

    • (3)在类名的最后加"情况"、"信息"、"记录"、"数据"、"表"、"库"、"单"等;

    • (4)在属性名前加类名。

      如,按"类的属性"念出来,"人员的姓名"很好,"人员的人员姓名"就冗余了。

    • (5)给类加上标识属性

      对象有标识,这是一个共识,不需要为类专门加一个如“XXID”之 类的标识属性。

      如何表达对象的标识,这是另一个领域的知识,其实现规律和当前 所研究领域的知识无关(除非当前所研究领域就是“如何实现对象标识” 的领域)。一旦确定实现的套路,在设计工作流通过人工或工具按照套 路加上即可。

      此处说的标识是为了区分对象而添加的标识,在设计工作流中可以 以整数、GUID 等各种方式实现。它应该不带任何领域知识,因为一旦 带有领域知识,就意味着一旦领域知识发生变化,它也要跟着变化。

      除了标识之外,可能还有其他在类的对象集合内值唯一的“编号” 属性,如订单编号、人员身份证号、房间号等。这些“编号”属性往往 带有领域知识,例如房间号“203”会暗示这个房间是 2 楼第 3 个房间。

      这样的暗示,是为了让人方便记忆和识别,计算机并不需要标识中 含有领域知识,除非对象没有其他属性,而是把各种领域知识都凝结在 标识中,需要计算机来解析。

      在设计工作流,虽然这样的属性也可以作为标识使用,但尽量不要 这样做,因为其中的领域知识一旦发生变化,就会带来风险。

      本书作者的身份证号,最开始是 15 位的“3401XX74XXXXXX1”,1999 年升位,变成了 18 位的“3401XX1974XXXXXX19”。身份证号中暗示了 许多知识:3401XXX→安徽省合肥市蜀山区,1974XXXX1974 年XX月XX 日生,XX1→当日出生的男性顺序号,9→校验码。这些信息发生变化 时,原有的暗示就失效了。

      应该把这样的“编号”属性当作普通的属性,把它和对象标识区分 开。“编号”属性带有领域含义,而且不是每个类都需要,因此需要出 现在分析类中,而标识不带领域含义,是所有类都有的,因此不需要出 现在分析类中。分析模型中,只出现核心域的概念,如果有个类有个标识 属性,那么这个标识肯定有特殊的不得不存在的领域含义,而不是因为非核心 域的映射套路需要的。

      对象标识仅用于标识对象以及在非人系统或系统内部组件之间传 递,不需要在人机交互的界面上出现。也就是说,不需要人类执行者输 入对象标识,也不需要向人类执行者展示对象标识。如果需要,在人机 交互的界面上出现的应该是“编号”属性。

    • (6)给类加上名为“状态”的属性

      很多建模人员会自作聪明地给类加一个名叫“状态”的属性,还洋洋得意, “哇塞,我知道这个类有状态噢,我真厉害!”。

      和对象有标识一样,对象有状态,这是面向对象这个领域的共识, 和“人员”等具体的类没有特定关系。如果加上去,又变成了批量刷废 话。

      至于类的状态如何实现,是添加一个名为“状态” 的属性,还是采用其他的实现方式,属于另外一个领域的问题。

      注意区分名为“状态”的属性和状态属性,“人员”有 一个“已婚”的状态属性(类型可能是 bool),这背后包含特定的领域 知识,可以暂时留在那里。当然,后面会引入状态机,将类的状态属性 变成状态机中的状态,然后从类的属性列表中清除它们。

      关于状态机的建模以及实现,后面章节再详细描述。

      “信息”、“数据”等也不是不可以作为类的名称。如果系统关注的 焦点是"信息处理",处理的信息是什么内容无所谓,"信息"、"数据"也 可以作为类的名称,类图可能如图 8-56。

      类的名字就叫“信息” ,同样也不能像图 8-55 那样出现一个"会议 信息"。“会议”和“信息”不会出现在同一个抽象级别。

8.2.4.5 命名的词性

类和属性应该用名词命名。上文出现的类“人员” 、“商品”、“姓名” 等就是名词。

在汉语中,动词可以不做任何变化,直接作为名词使用。例如,“我 的奋斗” 、“嫌疑人 X 的献身”以及“面向对象设计” ,其中的“奋斗”、 “献身”和“设计”就是动词的名词化。

因此,“申请”、“审批”“入库”、“出库”等同样可以作为类的名 称。虽然它们看起来是动词,但在作为类的名称的时候,已经被当作名 词了。

有的建模人员不理解这一点,总觉得看着不舒服,非得在后面加一 个“单” 、“记录”、“信息”、“事件”之类,变成“申请单”、“申请记录”、 “申请信息”、“申请事件”。

其实并不需要,否则每个动词刷一个后缀,又变成废话刷工作量了。 “事件”还说得过去,像“单” 、“记录”、“信息”等后缀已经假设了其 具体形态,就更不可取了。即使建模时面对的素材是一张纸质的申请单, 类的名字也应该写“申请”,因为这才是本质的概念。 “动词”直接作为类的名字,可以不用加后缀

如果一个用语在某个领域中已经存在很久,成为了该领域的术语, 即使它看起来犯了以上提到的冗余错误,用来作为模型元素的命名也无 妨。例如"订单"带有"单"字,实际上描述的是一次"购买"或“交易”, 不过"订单"已经在领域中广泛使用,而且把“单”去掉留下“订”也不 合适,所以“订单”可以作为类名。

在其他语言中,动词不一定能直接用作名词,可能要做一些变化。 在英语中,像“设计”是可以直接使用的:design→design,但 像“献身”可能就要加一个词缀:devote→devotion。其他词缀可能 还有 ment、ance、ing、ness 等。

★领域驱动设计话语中的“领域事件(Domain Event)”采用了 动词的过去式来命名,例如 OrderShipped,这是错误的。应该用名词 命名,关于这方面的阐述,可参见我写的这篇文章《DDD 话语批评之 一 : 评 “ 状 态 和 事 件 本 质 相 同 ” 》 , https://mp.weixin.qq.com/s/QhAhSdET5psZQEEW6f9KoA

因此,在汉语中, “支付”可以作为类的名称(名词)也可以作为 操作的名称(动词),而在英语中,类的名称可能叫 Payment,操作的 名称叫 Pay。

还可以看出,由于英语名词后面加了词缀,像在汉语中忍不住加一 个“记录”变成“支付记录”那样,在后面再加一个“Records”之类 的概率小了很多。

但也不是所有时候都忍得住,例如,图 8-59 是“Domain-Driven Design: Tackling Complexity in the Heart of Software”中的一张 类图,其中,Delivery 和 Handling 已经是名词,History 和 Event 没有带来有价值的额外信息,可以删掉。

形容词也可以名词化,例如,“沉默是金”、“把悲伤留给自己” 。

但是,和动词不同的是,即使把形容词当名词使用。这个词也不会 直接用作类和属性的名称。也就是说,“沉默”、“悲伤”不会成为类和 属性的名称。

对于很多形容词,领域中另有术语用于测量它。例如,测量“重”, 有“重量”,测量“浓” ,有“浓度”。重量”“浓度”等词就可以作为 类和属性的名称。

如果某些形容词,像上面的“悲伤”,领域知识中还没有术语用于 测量它,需不需要软件开发人员造一个?例如,在形容词后面加个“值”“度” 、“指数”等,“悲伤”→“悲伤度”?

不需要,也没资格。

如果领域知识中没有测量“悲伤”的术语,说明这方面的领域知识 可能还不存在,怎么可能会让软件开发人员在软件中封装这些知识呢?

如果涉众硬是需要软件开发人员在软件中封装这些知识,那么此时 软件开发人员就不只是软件开发人员了,他要扮演两个角色:首先,他 是一名研究“悲伤”的学问的领域专家;其次,他是一名把某个领域专 家的研究结果封装到软件中的软件开发人员。这两个角色可以由同一个 人担任,也可以分开。

★同前文所述,此处的研究不一定是科学研究,也可以是修仙、漫 威、哈利波特、王者荣耀。

形容词可以作为类的状态的名称。当还没有给类添加状态机时,形 容词可以作为状态属性暂时在类中出现,但要注意,状态属性并不是真 正的属性,有了状态机之后,要将其删除。

如,“重量”是“人”的属性,出现在上方的类框中, “重” 是“人”的状态,出现在下方的“人”状态机中。从状态机图上看,状 态“轻”、“重”代表了“重量”属性的一个取值区间。

在实现状态机时,有很多实现方式可以选择,其中有一种实现选择 是,把“重”作为一个布尔类型的状态属性,重新放回“人”这个类中。

关于状态和状态机建模,后文会再详述。

8.2.4.6 命名所用的语言

这里说的不是编程语言,而是汉语、英语、日语……

给核心域元素命名,使用的语言应该首先考虑精确体现核心域内涵 和方便开发团队思考和交流核心域知识。该用汉语就用汉语,该用英语 就用英语,该用日语就用日语。

以前经常会考虑转换到编程语言时需要改名的问题。在设计工作流, 如果我们使用的编程语言只能用英语命名类、属性、操作等——更严 谨的说法应该是编译器广泛支持的字符集比较小,那么还需要一个对编 程语言合法的名字。

建模工具例如 EA,一般会提供别名(Alias),真实名称用编译器 支持的字符集,再加一个别名用于显示。

随着时代的发展,编译器、DBMS 等支持的字符集越来越大,上 面提到的问题慢慢不再是问题。如果团队成员更熟悉汉语,那么在代码 中使用汉语命名即可,省去蹩脚英语或者模糊的汉语拼音缩写(啥?你 说“可以加注释嘛”?)给后续工作带来的麻烦。

其他如在文本编辑器中编码时切换输入法、版式不好看、中文标点 符号等问题,这和特定编程语言的语法有关,如果能尽量在图形建模时 表达核心域逻辑,生成尽可能多的编程语言文本,这方面的麻烦就会小 很多。 以上所说仅是针对目标系统的核心域元素的命名,不涉及非核心域 部分。非核心域部分内容,和计算机和软件领域有关。这方面的“外语”, 不管是计算机和软件专业的各个概念,还是编程语言的关键字,要求软 件人员熟练掌握是理所应当的。

例如,浑元形意太极掌门马老师找我们为他的门派搞一套“修炼辅 助系统” 。作为软件开发人员,能认真去体会马老师说的“化劲” 、“武 德”、“真气” 、“掌门”之类的概念就已经不易,还要把它们变成英语就 更难为人了,估计马老师自己也不知道。

但是,作为软件开发人员,不知道什么叫 DTO,什么叫 Iterator 就不应该了,因此,命名为“武德 DTO”之类是没有问题的。

如果目标系统是面向国际的,那怎么办呢?还是前面说的,首先考 虑精确体现核心域内涵和方便开发团队思考和交流核心域知识。该用汉 语就用汉语,该用英语就用英语,该用日语就用日语。

8.2.4.7 类命名用单数

类的名称已经是一个抽象概念,既代表属于这个类的所有对象的集 合,也指代集合中的任何一个对象。

例如,“人”这个概念既代表所有符合“人”的特征的对象的集合, 我们可以说“人是一种动物”;同时, “人”可以代表“任何一个人的个 体”,我们可以说“人有姓名、身高”。这两种用法,分别对应于后文要 讲解的泛化和关联关系。

如果用复数表达,例如汉语“人们”,英语“people”,第二种用 法就很别扭了,实例“某个人们”是什么?特别是在和其他类关联时, 如果本方多重性为多,又该如何处理?

如果“某个人们”另有含义,那么应该有另外一个类。例如, “组织”。“组织”和“人”关联, “人”一端的多重性为多, 此时,“人”一端的角色名,也就是“组织”中类型为“人”的属性名, 可以用复数“人们”或“people”代表“人”的集合。

有一些常见的开发习惯,如数据库表名用复数,甚至有的框架在类 转换表时,直接就在名称后面加上 s,理由是表里有很多行。其实"类"、 "表"的概念已经隐含了"多个对象"、"多行"的意思,不用再加了。而且, 如果英语不熟,还得费心思去想正确的复数形式,何必呢?

8.2.5 审查类和属性

8.2.5.1 类名中是否有形容词

如果存在“形容词(的)名词”这样的类名,例如“待支付(的) 订单”、“合适(的)会议室”,可以先把形容词从类名移除,转成类的 一个状态属性。很多时候形容词和名词之间没有“的”,也要能识别出 来。 状态属性只是临时的建模产物。在后续的建模步骤中,状态属性中 包含的知识会放到更合理的位置。

“名词的名词”

有时候,名词前面的修饰词可能是名词。例如,“员工的部门”中 的“员工”是名词。如果出现这样的类名,那不是什么状态属性,而是 在识别类时没有识别出该有的“员工”和“部门”类。

有一些用语比较模糊,要注意理清楚真正的含义,并用更严谨的用 语来表达。

例如,“会员订单”,看起来和“员工的部门”相似。经过进一步调 研发现,其真实意思不是“某个会员所下的订单”,而是“下单者身份 是会员的订单”。此时,“下单者身份是会员”就成了修饰“订单”的形 容词,那么可以从类名删除“会员”,把“下单者身份是会员”作为“订 单”的一个状态属性。当然,这个状态属性的计算可能也是通过“订单” 和“会员”的关联来达到的。

碰到类似的含糊用语,可以用以下方法辨别:

形容词修饰名词,相当于在该名词所代表概念的实例全集中取一个 子集,它应该存在一个补集,二者的并集等于此概念的实例全集。

例如,“待支付订单”相当于在订单全集中取一个子集,同时还应 该存在“非待支付(已支付)订单”——如果不存在“非待支付订单”, 划分出“待支付订单”也就没有意义了。“待支付订单”和“非待支付 订单”实例集的并集即为订单实例的全集。

而“员工的部门”则不同,存在补集“非员工的部门”吗?移除形容词的限度 “合适会议室”缩减为“会议室”,但“会议室”也可以看作“会议的室” ,是否需要再缩减为“室(房间)”?

这要看系统所关注核心域知识的范围。

如果做一个“会议室管理系统”,关注到“会议室”就可以了;如 果做一个“物业管理系统” ,合适的类可能是“房间(室)”,“作为会议 室使用”可以暂时作为“房间”的状态属性,后续步骤在处理状态属性 时,可能会添加“用途”类, “会议室”是用途的一个实例。

8.2.5.2 属性是否直接描述类

类和属性连在一起说"类的属性",应该能直接说得通,否则类和属 性的搭配是不合适的。这个时候应该找到或建立合适的类,把该属性移 进去。 例如,人员“人员的组织名称”是“人员的组织的名称” →组织→名称,中间隔了个“组织”,"类的属性"不能直接说得通,需 要添加一个“组织”类,把“名称”挪过去。

“属性要直接描述类”这个要求和关系数据库的第三范式“不存在 传递的函数依赖”相似。

对于每个属性,我们都这样思考:

“类”能决定“属性”吗?如果能,是怎么决定的?如果不能,还 需要什么?

--这个人的姓名叫什么? --潘加宇 --他为什么叫“潘加宇”? --没啥理由,他就叫这个。

“姓名”作为“人员”的属性是合适的。

★如果进一步探究,这个人为什么叫“潘加宇”也是有理由的。

理由之一可能是:“潘”是家族的姓, “加”是字辈,这由家族的规 则决定。 “宇”则来自鲁迅的《无题》 :心事浩茫连广宇,于无声处听惊 雷。了解了这个规律,还可以进一步推导,“潘加宇”如果有个弟弟, 那有可能叫“潘加雷”——但这些知识,不属于系统关注的范围,所 以探究到此处就可以——“没啥理由”。

--如果知道专家是潘加宇,小时课酬能定吗? --能,是 1500 元/小时。 --是怎么决定的? --因为潘加宇的级别是“金牌专家”公司规定“金牌专家”的课 酬是 1500 元/小时。

决定的方式是:专家→专家级别→小时课酬。需要添加一个“专家 级别”类,“小时课酬”作为它的属性。

--如果知道商品是“百事可乐罐装 330ml”,数量能定吗? --不能。 --还需要什么? --还需要知道是哪张订单。

因此需要一个类,既知道订单,又知道商品。添加“订单项”类, “数量”作为“订单项”的属性。

属性如果放在了错误的类,极有可能会导致大量不同对象的某些属 性值相同,而这反过来也可以作为线索:当发现大量不同对象有相同属 性值时,可以检查一下,是否有属性放在了错误的类。

注意,以上是说“可以检查一下”,不一定就需要调整。多个对象 的部分属性值相同的情况很常见,好多人姓名叫“张伟”,好多人年龄 是 18 岁,并不一定要分出来另一个类。

  • 可以合并的情况

如果 A 和 B 两个类存在 1 对 1 的关联,而且 B 只有一个属性,那 么,可以把 B 合并进 A。

例如“人”和“出生”。一次“出生”事件只针对一个 人,一个人只会出生一次,而且出生只关注一个属性:日期,那就可以 把“出生”删掉,在“人”中放置“生日”属性。

如果一个人可以出生多次(带着前世记忆轮回,可能性趋近于 0), 或者出生除了时间之外,还要关注出生医院等其他属性(可能性较大), 那就不宜合并了。

还可以看出,时间类型的属性实际上是某种事件发生的属性,只不 过有的时候被合并到其他类中,看起来成为了该类的属性。如, “生日”成为了“人”的属性,类似的还有: “下单时间”成为“订 单”的属性等。

如果多重性不是 1 对 1,而是多对 1,那么即使 1 这一端的类只有 一个属性,也不好合并。

即使“组织”只关注一个属性“名称”,如果把“组织”取消,把 “组织名称”合并到“人员”中,就会出现图 8-67 的情况,因为可以 有很多人员关联到同一个组织。

如果组织的名称需要修改,例如“腾讯科技(深圳)有限公司”改 名为“腾讯控股有限公司”,那么很多个“人员”的属性值都要修改。

同理,如果图 8-69 中的“出生”事件被多人共享,也是不适合合 并的。

导致出现“属性不直接描述类”错误的原因有:

  • (1)缺少抽象能力

缺少抽象能力的建模人员经常会把手上素材的信息,一一对应地映 射为类和属性,导致本来属于多个类的信息被合并在一个类中。

如图 8-70,建模人员对照着一张货物运输托运单,直接把它映射 成类,照抄上面的每一栏作为属性。

     图 8-70 错误:直接把素材照搬成类

建模人员有时还觉得挺符合“类的属性”的。“货物运输托运单的 货物名称”,没错啊,手边这张货物运输托运单上确实明晃晃地印有“货 物名称”四个字嘛。

托运单、出库单、销售单等各种单据,以及身份证、工作证、图书 卡、设备卡等各种卡片和证件,在信息时代之前就已经存在了。它们相 当于某种“信息化”的存储结构,存储一个或多个概念的信息,只不过 保存的载体不是电子载体,而是甲骨、铜器、竹片、木片、帛布、纸张。

现在,既然用信息系统取代了这些单据、卡片和证件,那么要建模 的实体类应该是它们所存储的领域概念,而不是单据、卡片和证件本身。

图 8-70 右侧的类“货物运输托运单”应该变成如图 8-71。

text
图 8-71 建模早期信息存储载体所存储的领域概念
@startuml
class 站点 {
    -名称
    -发站
    -到站
}
class 托运 {
    -日期
}
class 发货单位 {
    -单位
    -名称
    -地址
}
class 收货单位 {
    -单位
    -名称
    -地址
}
class 人员 {
    -联系人
    -电话
}
class 托运项 {
    -数量
}
class 货物 {
    -名称
}
站点 "1" -- "1" 托运 : -发站
站点 "1" -- "1" 托运 : -到站
托运 "1" -- "1..*" 托运项
托运 "1" -- "1" 发货单位 : -发货单位
托运 "1" -- "1" 收货单位 : -收货单位
发货单位 "1" -- "1..*" 人员 : -联系人
托运项 "1" -- "1..*" 货物
@enduml

开发团队中可能会有人唧唧歪歪,什么“性能”啦,什么“我们之 前都是这样做”啦。碰到这种情况,也不用祭出鲁迅先生的“从来如此, 便对么?”,你只需要问他,扔给他一张类似图 8-70 的XX单,他有能 力干脆利索地得到类似图 8-71 吗,没能力就让他 TM 的闭嘴。

★其实,如果一个人知道如何得到一个清晰、无冗余的模型,说明 他具备了一定的建模能力,往往也不会在分析的时候担心这些问题,因 为他知道,如果碰到性能问题,可以按照某些套路添加冗余,这些套路 和目前所思考的领域知识没有关系,没有必要混杂进来。

在有能力得到图 8-71 的基础上,可以再来考虑需不需要一个如图 8-70 右侧的“货物运输托运单”类来维护当时的快照。因为随着时间 的推移,对象的属性值会变化。

例如图 8-71 中,随着时间的推移,如果某个单位的名称或地址改 掉了,此时从图 8-71 计算出图 8-70 左侧的托运单快照,和事件发生 的当时按照图 8-70 左侧填写的信息是不一样的。

如果这样的快照很重要,可以另外加一个如图 8-70 右侧的快照类 来维护这些信息,这个类是孤立的,和图 8-71 的类不存在关联。

但这样做很容易出现沿着关联线向外延伸,雪球越滚越大的情况。 如图 8-71,单位的联系人会换人,人员的电话会改变……最终可能需 要一个巨大的类,把所有通过关联线连在一起的类的所有属性组合在一 起,而背后的数据量也是庞大的。

更合理的做法是记录本质的变更来源。如果单位变更名称和地址是 值得关注的事情,应该添加一个类记录单位变更名称和地址的细节,或 者说,记录所有值得记录的对象属性值变更的细节。在此基础上,需要 类似图 8-70 的某个时间点来自多个对象的属性值组合快照时,可以通 过计算来还原。

在这里,我们要认清楚非常重要的一点:本质是对象之间存在关联, 而不是对象属性值之间存在关联。如图 8-71,两个单位之间曾经存在 的某次托运关系,并不会因为单位后来改名或改地址而变化,相对于如 图 8-70 列出一堆属性值,记住“托运”和“单位”之间的关联是更本 质的——当然,并不影响从本质模型还原出各种视图或报表。

正如上文提到的,如图 8-70 是在信息化时代之前的一种“信息化” 的存储结构。当时不要说没有方法学,即使有方法学让你先分解概念为 图 8-71,也没有计算设施把它们按需要组合成图 8-70 或更多的视图。

现在,既然有了条件,就没有必要再去模仿条件不足时的拙劣“模 型”了。

很可能在这个时候,某些领域驱动设计伪创新又乘机迎合那些没有 抽象能力又不愿意学习的无能之辈。伪创新的说法是:图 8-70 的快照 是发生过的事情,是不会变化的,各种所谓的“流水”才是本质!于是, 无能之辈非常开心,热烈拥抱伪创新。

我们可以用科学研究类比一下:背后的规律没搞清楚时,要推测某 个数据,可能是靠“经验”来推测。 “经验”其实就是发生过的“流水” 的记忆。当科学家研究巨量已有的“流水”(即实验数据) ,探索出其中 的规律后,这时再推测数据,就没有必要从之前的巨量“流水”来推测 了,根据归纳出来的公式推测即可。当然,之前或之后的各种“流水” 可以继续保留,但它们不是本质,是现象。

何况,不是所有的系统都需要保存“流水”。电梯每天上上下下, 不知发生多少次“召唤”事件,但目前的电梯系统并不会记录“召唤” 事件的细节——谁召唤的、什么时候召唤的……系统只需要维护本质的 行为规则,如果采用本书的方法学,可以选择用状态机来表达。

当然,也许有一天,电梯系统有了足够的存储资源,会记录所有的 “召唤”流水,但这和背后的规律没有关系。系统是否记录某个事件的 细节,不影响事件是否发生、事件是否产生效果,以及背后的行为规则。

  • (2)受关系数据库建模的影响

建模人员有时会犯这样的错误,在一个类中放上另外一个类的属性 作为“外键”。比如针对上面的例子,建模人员会想:“人员”里放“组 织名称”确实不合适,但是放个“组织编码”作为外键总可以吧?其实 也不可以。"组织编码"是“组织”的属性,是封装在“组织”中的秘密, “人员”不应该拥有“组织”的任何属性,它只能通过关联拥有“组织” 对象,然后通过访问“组织”对象公开的操作来间接访问“组织”的属 性。

“人员”里放“组织编码”不合适,放一个无意义的标识“组织 ID”呢?同样也不可以。因为这个“组织 ID”是“组织”的标识,前 文已经说了,标识属性此时不需要存在,所以“组织 ID”在“组织” 里不存在,更不要说放到其他类中作为“外键”了。

在设计工作流,需要把类图映射到关系数据库时,确实需要把"组 织"表的主键(可能是"编码"也可能是生成的代理主键)放在"人员"表 中作为外键,但正如上文所说,这同样是另一个领域的知识,而且映射 规律和核心域知识无关。

  • 状态属性和类的匹配

状态属性的名称是一个形容词,类型为布尔,或者枚举,用来标记对象是 否处在某个状态。

这些状态属性可能是来自素材中的定语,例如,用例规约提到“可 享受优惠的顾客”,那么在识别的时候可能会先把“可享受优惠”放在 “顾客”类中。

和“类的属性”刚好相反,状态属性和类连在一起说,要能说得通 "属性的类"。例如,“可享受优惠的顾客”是说得通的。

8.2.5.3 属性是否可以从其他地方推导

如果一个属性可以从其他地方推导出来,那么这个属性就是冗余的, 可以删掉。

比如,人的年龄可以从出生日期计算得到,应该把年龄删掉。

这个“其他地方”也可以是所关联的类的属性。如,订单 的总金额可以由各个订单项的金额合计得到,那么可以考虑把总金额删 掉。

状态属性的冗余

状态属性是冗余的,背后往往隐藏着更多的逻辑,需要进一步思考, 把它变成更合适的模型内容,但这涉及到还没讲到的知识点,此处只简 单举例,后文还会详述。

以“订单”为例,如果问一个“订单”对象,你是“待 支付的订单”吗?如果“订单”通过自己的资源就能回答这个问题— —例如,查询是否有“支付”对象和自己关联,那么“待支付”就可 以作为“订单”的状态机中的一个状态。

再看“会议室”。如果问一个“会议室”对象,你是 “合适的会议室”吗?这时的回答是犹豫的,因为会议室合适不合适, 还需要看开哪个会议,会议 A 也许不合适,但会议 B 可能就合适。这 样含义的“合适”不能成为“会议室”的状态。

这个“合适”的演变可能并不简单,只是在“会议室”和“会议” 之间建立一个“合适”的多对多关联,可能并不能满足要求,甚至是冗 余的,因为计算哪些会议室对某个会议合适,正是系统的一个责任。如 果是这样,就需要进一步建模背后的规则。

text
图 8-78 会议背后的规则
@startuml
class "会议室" as meetingRoom {
    合适=?
}
note right of meetingRoom : 问:你是“合适的会议室”吗?\n答:不知道。

class 会议 {
    -人数
}

class 会议室 {
    -容纳人数
}

class 设施要求 {
    -数量
}
class 设施类型 {
    -名称
}

class 设施 {
    -安装
}
会议 "1" -- "0..*" 设施要求 : has
设施要求 "1" -- "1" 设施类型 : for
设施类型 "1" -- "0..*" 设施 : has
会议室 "1" -- "0..*" 设施 : has
@enduml

但是,如果“合适”的含义是“当前使用会议室的会议是否合适”, 有图 8-78 的加持,再加上一个“当前会议”的关联,“会议室”是可 以回答这个问题的。此时,“当前会议合适”就可以作为“会议室”的 状态,其他状态可能有“当前会议不合适”、“无当前会议” 、“装修中”。

★把状态放在“会议”,来一个“当前会议室合适”可以吗?也可 以。放在哪里更好,后文还会再探讨。

8.2.5.4 属性是否在本领域内可分解

如果属性再分解就得到另一个领域的概念,那么这个属性可以留在 类中。如果属性可以继续分解成本领域的概念,那么可以考虑把这个属 性独立出去变成另一个类。

如,"人员"的"称呼"属性的类型是 String,相当于"人员" 关联到 String 类。String 属于基础语义领域,已经不属于"人员"所在 的“人员管理”领域,那么"称呼"可以留在“人员”中作为属性存在。

而"组织"还可以像右侧所示分解成“名称”、“办公地址”等,这些 概念依然属于“人员管理”领域,所以可以考虑将“组织”分离为一个 类,“人员”关联到“组织” 。

注意以上的用词。我们只是说“再分解就得到另一个领域的概念”, 并没有暗示这个另一个领域是什么,更没有暗示这个领域比核心域简单。 任何领域,只要愿意深入研究,都是复杂的。

就拿“称呼”的类型 String 来说,String 如果用.NET 中的 String 类实现,这个类有 157 个操作,远远超过“人员管理”领域中某个类 所拥有的操作数目。只不过 String 类如何构造是别人负责操心的事情, 不是我们目标系统的建模人员操心的事情。

在分析工作流,我们只需要判断某个属性再分解就到了另一个领域, 或者说类型是另一个领域的类。至于属性的类型具体是哪一个类,UML 提供了一些原生类型,如果认为属性的类型刚好是这些类型之一,可以 指定,否则可以先不指定,因为分析模型没有绑定到任何一个具体的编 程语言和数据存储平台,而不同的编程语言和数据存储平台的类型体系 还是有差别的。

8.2.5.5 是否有多重性大于 1 的属性

如果某个属性经过了 8.2.5.4 属性是否在本领域内可分解的检验, 但该属性多重性大于 1,即所谓多值属性。例如,人员有多个手机号。

这时可以:

  • (1)把“手机”属性留在“人员”类中,多重性设为多.

注意,只需要说明多重性为多,不要放上一个该属性的数组或 List 之类。人员有多个手机号,这是一个领域的知识;用编程语言如何实现 多值属性,是另一个领域的知识。一旦混杂,又会导致批量刷工作量。

  • (2)更推荐的做法是,把该属性分离出去,如图 8-83。
text
图 8-83 分离多值属性
@startuml
class 人员 {
}
class 手机 {
    -号码
}
人员 "1" -- "*" 手机 : has
@enduml

人员有多个手机号,背后往往是有原因的。虽然现在不关注,很可 能以后就会关注。例如,有的手机号是私人用的,有的手机号是办公用 的,如果需要关注这些知识,那么就需要从图 8-83 转成图 8-84 中的 某一个,此时只需要添加关联或者在“手机”类添加一个属性。

text
图 8-84 当需要关注更多的知识时,类图发生变化
@startuml
class 人员 {
}
class 手机 {
    -号码
}
人员 "1" -- "*" 手机 : -私人手机
人员 "1" -- "*" 手机 : -办公手机
@enduml
@startuml
class 人员 {
}
class 手机 {
    -号码
    -用途
}
人员 "1" -- "*" 手机 : has
@enduml

当然,这也不是建模“人员有多个手机”的最好做法。后文我们介 绍到人员相关的分析模式时,会详细描述如何建模这个领域。

以下做法是不好的:

  • (1)在“人员”类中放上多个属性“手机 1”、“手机 2”、“手机 3”…….

这样的做法相当于把抽象级别降到了对象级别,或者用关系数据库 建模的说法,这是违反第一范式的。如果人员没有那么多手机号,某些 属性就会出现空值;如果人员拥有手机号的数量大于预设的个数,就会 放不下。

  • (2)只有一个“手机”属性,属性值里用逗号分隔各个电话号码。

如果这样的做法是好的,那不如更进一步。各个属性也不用分了, 就一个字符串。还可以再进一步,类也不用分了,也串在一起……持久 存储或网络传输时的序列化不就是这样干的吗? 我们之所以引入各种抽象,根源就在于软件是人做的,人脑的容量 和运行速度有限,这些抽象可以帮助人脑应对复杂性。

8.2.5.6 属性是否对所有对象都有意义

如果通不过 8.2.5.2 属性是否直接描述类的检验,那么类和属性 放在一起是不合适的,但这只是必要条件,不是充分条件,即使通过了 也未必合适。

如图 8-86,人的姓名,人的▲▲(▲▲是男性特有的器官),人的 〇〇(〇〇是女性特有的器官)好像都说得通,但如果问:是不是所有 对象都应该有这个属性呢?得到的答案就不同了。

是不是有人有姓名——是。 是不是所有人都应该有姓名——是。 是不是有人有▲▲——是。 是不是所有人都应该有▲▲——不是,只有一部分人有。 是不是有人有〇〇——是。 是不是所有人都应该有〇〇——不是,只有一部分人有。

说明“人”发生了分裂,分裂成“男人”、“女人”两个子集(子 类)。▲▲是男人的属性,〇〇是女人的属性。

text
图 8-86 分解只属于部分对象的属性到子类
@startuml
class 人 {
    -姓名
}
class 男人 {
    -▲▲
}
class 女人 {
    -OO
}
' 继承关系
人 <|-- 男人
人 <|-- 女人
@enduml

注意,我们说的是“应该有”,并非时时刻刻都要有。一个人刚出 生,没来得及起名,这个可以理解,在后面的生命周期中,他应该会有 姓名,但一个人如果出生时没有〇〇,并不是还没来得及长出来。