8.2 建模步骤3-1 识别类和属性
8.2.1 面向对象的假设
当使用面向对象的方法来分析系统时,我们假设系统由"对象"这样一种东西构成,对象封装了数据和行为。
在分析工作流,我们假设系统中的对象在一个虚的"对象空间"中运行。这个空间不是内存,也不是硬盘,只是人脑中的一个逻辑空间,将它想象成宇宙空间也未尝不可。在"对象空间"中,不存在性能问题,存储空间无限大,对象的创建、对象之间的通信以及各种计算无限快,支持无限多的人并发……。显然,也不存在什么硬盘、内存、Cache、加载……等概念。万事俱备,只欠你分析的核心域逻辑。
图8-36 虚的"对象空间"
上面提到的这一点,可以用来判断你思考的问题是分析问题还是设计问题。
我们可以针对分析模型里的元素,一个一个问,“如果没有它,会怎么样”,如果回答是“会有性能问题”,那么,可以从分析模型中把它删掉。
例如,类图中有一个冗余的类,问“如果没有它,会怎么样”,答“查询可能会慢”,那就把它删掉。状态机图里有一个状态“Transient”,问“如果没有它,会怎么样”,答“会漏掉某些数据没有持久化”。这些都可以删掉。
但如果回答是“没有它,系统就没法履行自己的责任了,因为要做的系统就是一个持久化框架”,那就不一样了。
分析模型受到设计的污染,很容易导致批量的废话刷工作量,导致没有时间思考应该重点思考的核心域逻辑。当然,正如前文所说,这也可能正是某些人乐意的。
注意上文提到的"假设"二字。面向对象就是一个假设,如果不认可“系统由对象构成”,也可以分析系统的核心域逻辑,只不过用的方法不叫“面向对象方法”,叫“面向过程”、“面向组件”、“面向肥皂”都行,看你的假设是什么了。
面向对象的思考方式比目前的其他思考方式要好一点,原因不是计算机喜欢面向对象或者面向对象更接近于计算机的底层,而是面向对象的思考方式更能帮助人脑去剖析复杂问题。如果计算机有感情,估计它应该更"喜欢"人类用机器语言直接给它发指令,因为这样自己就不用受累搞什么编译、链接。
正如前文提到的,三角函数更能解决复杂问题,不意味着它比全等三角形、相似三角形更容易掌握。面向对象更能帮助剖析复杂问题,不意味着面向对象的思考方式比其他的思考方式更容易掌握,而且随着你掌握了更强有力的思考工具,更复杂的问题就会扑面而来。这些问题之前已经存在,只是之前你没有能力来发现和对付它们——“古人很少死于癌症”。
8.2.2 分析类
8.2.2.1 三种分析类
在接受“面向对象”假设的前提下,我们接下来就要做第一步思考:系统由什么样的对象构成。
在这一步思考中,我们通过抽象思维把具有共同特征的对象集合归纳为"类",对象看作类的实例。
_*_归类是人类认知的一种基本技能,其哲学讨论可以追溯到柏拉图的理型论(Theory of Forms)。*
依照Ivar Jacoson在“Object-Oriented Software Engineering: A Use Case Driven Approach”(Jacobson 1992)中的思想,在分析工作流我们进一步假设系统中存在三种类:边界类(Boundary Class)、控制类(Control Class)和实体类(Entity Class)。
在模型中,我们可以用Ivar Jacoson建议的构造型(Stereotype)来表示三种分析类,如图8-37。
图8-37 三种分析类的构造型
一些UML工具(如Enterprise Architect、Visual Paradigm)已经内置了这些分析类构造型。如果使用的建模工具没有内置这些构造型,可以自己添加如“<<边界>>”等文字构造型;或者不用构造型区分,通过给类起名"某某接口",“某某控制”,也有助于了解该类在系统中扮演的角色。这一点,和第3章讲到业务工人、业务实体时的做法是一样的。
三种分析类只是一种逻辑上的思考方式,如果你乐意,可以换成另外的思考方式。在设计工作流,三种分析类可以映射到任何实现架构,包括但不限于MVC、MVP、MVVM、六边形、洋葱型……甚至映射到不做任何分割的“架构”。
图8-38展示了三种分析类的责任、和需求的关系以及命名。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
图8-38 分析类的责任、和需求的关系以及命名。
图8-39用序列图展示了三种分析类之间的协作。
图8-39 三种分析类在系统中的协作
执行者先把消息发给边界类对象,边界类对象能履行的就履行,无法履行的责任,再发给控制类对象。控制类对象就像总裁办,不做具体工作,只是将责任分解后分配给实体类对象。
实体类可以按照它们之间的耦合程度组成若干组合(Composition),类似于公司的部门。组合之间还可以再组成更大的组合,类似于大部门中有小部门。也有可能有的类既不组合其他类,也不被其他类组合,类似于自成一个部门。
控制类对象发送消息时,先发给组合的整体对象,再由整体对象分配(可能还伴随着分解)给组合内的其他对象,如果组合内的对象还组合了更小的对象,还可以继续分配。最后,由边界类对象向外系统反馈信息,完成一个交互回合。
以上说的“组合(Composition)”和DDD话语体系中的“聚合(Aggregate)”类似。关于这一点,本书在讲述类的关系时会详细说明。
8.2.2.2 关于边界类
边界类的责任是接受输入、提供输出以及做简单的过滤。
图8-38中提到边界类的映射方法——每个有接口的外系统映射一个边界类。这里说的"有接口的外系统"不仅包括系统执行者,还包括仅接受系统输出信息的外系统。
以《软件方法》上册案例中的"时间→发送公开课通知"用例为例。该用例进行过程中,系统会向软件开发人员发送公开课通知,同时还要向UMLChina助理反馈发送通知的进展。软件开发人员和UMLChina助理在这个用例中仅仅是接受输出,没有输入信息给系统,但系统可以分别设置一个边界类来封装向软件开发人员和UMLChina助理反馈信息的责任,如图8-40所示。
图8-40 外系统映射边界类
图8-40中,“时间”映射一个“时间接口”,“软件开发人员”映射一个“软件开发人员接口”,“助理”映射一个“助理接口”。
外系统如果是人,对应的边界类也可以叫“**界面”,例如“助理界面”。本书就不区分了,一律起名“**接口”。
分析工作流的边界类不暗示任何实现方案。在总责任相等的前提下,它和实现的映射是多样的,可以用图形界面实现,也可以用非图形界面(包括文本、声音……)实现。
即使使用图形界面实现,也不能简单认为一个边界类对应一个窗体(Form)。一个边界类的责任可以拆解到多个窗体上,一个窗体也可以和多个外系统交互。如何组织这些责任,应该从外系统的角度来考虑,而不是从用例或实体类的角度来考虑。
图8-41中,“助理接口”边界类被圈住的几个责任来自不同用例的步骤,但在使用图形界面实现时,可以放在面向助理的、通知专用的同一个窗体中。
图8-41 边界类责任的组织
类似的例子还有:一份申请,需要通过系统审批三次,也就是三个不同的用例。在图形界面实现中,可能不需要准备三个窗体,部门主管、财务、副总三个审批人可以在同一窗体上工作,但部门主管、财务、副总各自有对应的分析边界类。
如果某个外系统和系统的交互很多,对应边界类的责任可能会有很多。有的做法推荐按"外系统+用例"的组合映射边界类,这样可以减少一个边界类上的操作个数。本书不推荐这样做,因为这已经隐含着先入为主“按用例划分边界”的意思,不利于最后得到合理的边界。
尽量保持一个外系统映射一个分析边界类,如果操作很多,可以将从外系统角度观察可能要分在一组的操作移到一起,EA等工具可以随意定制属性和操作的上下显示顺序。
需要提醒的是,外系统映射的只是边界类,并不映射实体类。在外系统是人的时候,经常会有人犯这样的错误。例如以下用例规约片段:
1.助理选择公开课,请求创建通知任务
2.系统验证所选公开课适合创建通知任务
“助理”是执行者,映射一个边界类“助理接口”是可以的,但如果映射一个“助理”类,如图8-42,那就错了。
图8-42 外系统不映射实体类
系统是否需要一个“助理”类,要看系统是否需要维护助理的信息。如果需要,会在某个用例规约的某个地方体现,例如,可能会有一个步骤:
7. 系统保存通知任务
绑定一个字段列表:
7. 通知任务=4+创建时间+创建人
这个“创建人”就是助理,说明系统需要记住助理的信息,这时才会有“助理”类。
但并不是所有的系统都需要保留人的信息。例如,乘客坐电梯上楼,乘客是电梯系统的执行者,但电梯系统可能不需要"乘客"实体类,因为它不需要记住乘客的信息。
当然,有朝一日,电梯升级为防疫电梯,用例规约里有:
4 乘客提供身份标识
5 系统验证身份标识合法
6 系统记录乘客信息和入厢时间
这时,电梯系统里就有"乘客"实体类了,因为系统要记住乘客的信息。
当然,虽然电梯系统没有"乘客"类,但会有"乘客接口"类,可能的类图和常见的实现方式如图8-43。
图8-43 “乘客接口”及其常见的实现
8.2.2.3 关于控制类
控制类相当于用例在系统中的“代理”,它的责任是控制用例流,为实体类分配责任。如果在分配责任时发现控制类只起到传递的作用,没有起到分解和分配的作用,也可以把控制类去掉。
因为每个用例直接映射一个控制类,可以用“用例名称+控制”来为控制类命名。
因为构造型已经包含“边界类”、“控制类”的概念,严格来说,“员工接口”、“审批控制”类名后面的“接口”、“控制”可以省掉,在分析映射设计时,再通过映射规则中把它补上去。不过,考虑到可能还会有“员工”、“审批”实体类,而且很可能会一起出现在同一张序列图上,这会看起来有点别扭。
8.2.2.4 精力应该花在实体类上
边界类与外系统、控制类与用例的映射关系很明显,所以识别边界类和控制类不需要思考,直接按照上面的套路映射即可,甚至可以推迟到画分析序列图时再加上去。
有的分析方法学如ICONIX提倡一种Robustness Diagram,认为可以通过它来帮助寻找类。开发人员一用确实感觉很舒服,噼里啪啦就发现好多类,有一种"我已经取得了不小成绩"的错觉,不过要是仔细看看,就知道"发现"的大多是边界类、控制类。这些类其实用不着刻意去发现,只要按照图8-38的套路映射即可。
最难的工作——寻找实体类以及它们之间的协作,Robustness Diagram却是寥寥带过,甚至容易误导建模人员把实体类和用例一一对应。所以,本书不推荐开发人员额外花时间画Robustness Diagram。
*和前文多次提到的一样,凡是不需要思考就可以得到很多“成果”的“方法”,都容易成为懒人摸鱼的遮羞布。*
建模人员的思考工作量应该花在识别实体类上。一个用例需要哪些实体类协作实现、如何协作,一个实体类会参与哪些用例的实现,这是一个多对多的映射,需要由建模人员的大脑决定哪种映射最好。
因此,本章以下内容提到的“类”,缺省意思为“实体类”。
8.2.2.5 不存在“系统”这个类
"系统"的概念是需求工作流的概念。在需求工作流,我们把系统看作一个对外提供服务的整体。在分析工作流,"系统"的概念已经被打碎成很多个类,"系统"这个词不需要识别成类。
图8-44表达了不同工作流视角下的目标系统。
图8-44 各工作流如何称呼目标系统
在业务建模工作流,研究范围是组织,而组织中有很多系统,在业务序列图上提到目标系统时,只是说“系统”二字无法让人理解指的是哪一个系统,需要写出目标系统的名字“***系统”。
在需求工作流,研究范围是目标系统整体,此时,聚光灯已经打在目标系统身上,不需要再写目标系统的名字,写“系统”二字即可。
就像公司开表彰会,老总宣布优秀员工名单时,要说名字“罗永昊”、“罗阵宇”,轮到优秀员工罗永昊上台发言时,罗永昊称呼自己就不好再说“罗永昊”,说“我”、“在下”、“小弟”就行了。
*用自己的名字以及第三人称来称呼自己,往往代表一种__极度的"自信"__。例如:
“凯撒注意到这事,把他的军队撤到最近的一座山上去”(《高卢战记》)
“老胡觉得这件事实在不应该”
“婷婷想吃冰淇淋嘛”*
而在分析工作流,研究范围是系统的内部构成。系统已经被分解成很多个类,这时就不能再说“系统”了。
有的建模人员会把"系统"识别成一个类,画成这样:
图8-45 无意义的类图
这种图画出来既没有根据(你怎么知道是这样分解?)又含义模糊(模块指的是需求包还是组件?),对剖析系统的复杂性没有帮助,但给建模人员带来一种虚假的成就感:我描述了几个类之间的关系,而且还是组合关联(还可以联想到高大上的“组合优于继承”),已经开始剖析系统的复杂性了呢——更妙的是,不用思考,信手拈来!
扫码或访问http://www.umlchina.com/book/quiz8_1_2.html完成在线测试,做到全对以获得答案。
1. [单选]方法学家马宝国老师在推行一种“面向浑元形意太极”的分析方法,按照本书的知识点,看到这个名字,我们最应该想到的是:
A) 你家老公会劈砖,小哥会打那太极拳
B) 马老师的建模方法是伪创新
C) 马老师的方法假设系统由“浑元形意太极”构成
D) 我左手一式太极拳,右手一剑刺身前
2. [单选]为什么面向对象分析设计方法比面向过程好?
A) 面向对象更适合人脑去把握系统的复杂性。
B) 面向对象和需求的映射更直接。
C) 面向对象方法更容易掌握。
D) 面向对象更符合计算机的底层。
3. [单选]铁路售票处,售票员使用售票系统来售票,在用例进行过程中,系统需要不断向旅客反馈车次、车票和价格信息,系统还需要和银行系统交互。"售票"用例的分析序列图中,会出现_____个边界类,_____个控制类,_____个实体类。
A) 1,2,3
B) 3,1,2
C) 不定,1,3
D) 3,1,不定
E) 3,2,3
F) 3,1,3
G) 不定,1,不定
H) 3,3,3
微信:umlchina2