8.2 建模步骤C-1 识别类和属性
8.2.4?类和属性的命名
8.2.4.2 关于DDD话语中的“通用语言”
DDD(领域驱动设计)话语中有“通用语言(Ubiquitous Language)”的用语,这是一个伪创新。
(1)类似“通用语言”的概念早已有之
“术语表(Glossary)”或“数据字典(Data Dictionary)”等内容,几十年前的开发规范中应该就已经有了。
下面是出版日期在Eric Evans的《领域驱动设计》之前的几本书的截图。?
图8-35 摘自《软件复用:结构、过程和组织》(Ivar Jacobson等,英文原版出版于1997年)
图8-36 摘自《UML对象、组件和框架——Catalysis方法》(Desmond Francis D’Souza 等,英文原版出版于1998年)
图8-37 摘自《程序员修炼之道——从小工到专家》(Andrew Hunt 等,英文原版出版于1999年)
(2)“通用语言”迎合了呆在舒适区的需要
那么,DDD圈子为什么要重新发明“通用语言”,并强力吹捧呢?
原因就是第1章关于伪创新的内容中阐述的:它迎合了“广大开发人员”呆在舒适区的需要。
我们在上文强调,建模人员要虚心学习领域知识,而这是很辛苦的事情,很多人是不愿意吃这个苦的。
“通用语言”妙就妙在它告诉你,可以不用吃苦不用走出舒适区!
我们先来看一下Eric Evans在《领域驱动设计》中对“通用语言”的陈述,如图8-38。
图8-38 摘自《领域驱动设计》(Eric Evans,英文原版出版于2003年)
从图8-38我们可以看到,“通用语言”是“技术术语”和“业务术语”的交集,用本书的话来说,就是“非核心域术语”和“核心域术语”的交集。
问题是,有交集吗??
技术术语(非核心域术语),例如浏览器相关的术语:请求、回应、渲染、DOM、序列化、反序列化……。
业务术语(核心域术语),例如商场相关的术语:商品、顾客、订单、库存、收银、盘点……。
“技术术语”和“业务术语”哪里有交集?莫非是把这些正交的术语随意组合,以达到废话刷工作量的效果?此处可以回顾我们在8.1.3 域之间的映射和协作的套路中关于a×b×c的陈述。
DDD话语中所谓的“技术术语”,根本和“技术”无关,其实只是“技术人员”在没有虚心学习领域知识的情况下编造的“业务术语”——“技术人员编造的业务术语”。
而“通用语言”使得“技术人员”编造“业务术语”变得理直气壮,这是一个大倒退。
现实中,难免会有“技术人员”懒得去调研涉众,懒得去学习领域知识,乱做一通了事的现象。这是人性使然,很难杜绝,但至少我们还知道这样做是不好的,如果要做更好,应该怎么做。
如果把偷懒变成理直气壮,味道就变了。
就像我们说“人是自私的”,这是低调描述一个事实,但如果理直气壮地说“人不为己,天诛地灭”,味道就不一样了。
图8-39是Vaughn Vernon在《实现领域驱动设计》中的陈述:
图8-39 摘自《实现领域驱动设计》(Vaughn Vernon,英文原版出版于2013年)
看,不是业务语言,不是工业标准(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-40所示的“核心域透镜”的思考方式:如果从核心域的视角去看这个概念,或者说把这个概念映射到核心域,我们应该得到什么概念?
图8-40 用核心域透镜映射各种概念
例如,以“学习和考试”作为核心域,经过透镜前后的概念对比如图8-41。
图8-41 经过透镜前后的概念对比
对每一个用语我们都可以这样过一下:这个用语属于核心域概念吗?如果不属于,映射到核心域概念意味着什么?
例如,图8-42左侧,“商品”有一个“加粗显示”的属性来标记它是否加粗显示。如果核心域是“商品”相关的领域,那么“加粗显示”不属于核心域的概念。我们可以追问:为什么要“加粗显示”,能在核心域找到原因吗?回答可能是:因为该“商品”是“热销商品”。“热销”才是核心域概念,可以改成图8-42右侧。
图8-42 “加粗显示”的映射
这里面涉及到两个知识:
知识一:某商品是热销商品。这是商品领域的知识,已经存在很多年。
知识二:如何表示“热销”的概念。这和表示方式、时代审美、展示的目标人群等有关。图形接口的表示和文本接口、语音接口不同,十年前的图形接口和现在的图形接口不同,面向年轻人的图形接口和面向老年人的图形接口不同……
8.2.4.4 命名中不带冗余内容
如果把模型元素命名中的某个部分删去,不影响建模人员对该模型元素的认识,那么这个部分没有必要存在。
如图8-43中,在“人员”后加一个“类”字,实际上也是把两个不同领域的知识叠加在一起。
知识一:人员是一个类。
知识二:用UML表示法的图形表示,类是一个方框。
知识一和知识二是正交的。在对知识二已经有了共识的情况下,再加上一个“类”字是冗余的。
图8-43 删除命名中的冗余部分
关于类和属性的命名,常犯的冗余错误有:
(1)在类名的最后加"类"字或在类名的前面加"Class"或"C";
(2)在类名中加入“领域”、“实体”等词;
(3)在类名的最后加"情况"、"信息"、"记录"、"数据"、"表"、"库"、"单"等;
(4)在属性名前加类名
如图8-44,按"类的属性"念出来,"人员的姓名"很好,"人员的人员姓名"就冗余了。
图8-44 属性名称前不需要加类名
(5)给类加上标识属性
对象有标识,这是一个共识,不需要为类专门加一个如“**ID”之类的标识属性。
如何表达对象的标识,这是另一个领域的知识,其实现规律和当前所研究领域的知识无关(除非当前所研究领域就是“如何实现对象标识”的领域)。一旦确定实现的套路,在设计工作流通过人工或工具按照套路加上即可。
此处说的标识是为了区分对象而添加的标识,在设计工作流中可以以整数、GUID等各种方式实现。它应该不带任何领域知识,因为一旦带有领域知识,就意味着一旦领域知识发生变化,它也要跟着变化。
除了标识之外,可能还有其他在类的对象集合内值唯一的“编号”属性,如订单编号、人员身份证号、房间号等。这些“编号”属性往往带有领域知识,例如房间号“203”会暗示这个房间是2楼第3个房间。
这样的暗示,是为了让人方便记忆和识别,计算机并不需要标识中含有领域知识,除非对象没有其他属性,而是把各种领域知识都凝结在标识中,需要计算机来解析。
在设计工作流,虽然这样的属性也可以作为标识使用,但尽量不要这样做,因为其中的领域知识一旦发生变化,就会带来风险。
本书作者的身份证号,最开始是15位的“3401**74******1”,1999年升位,变成了18位的“3401**1974******19”。身份证号中暗示了许多知识:3401**→安徽省合肥市蜀山区,1974****→1974年**月**日生,**1→当日出生的男性顺序号,9→校验码。这些信息发生变化时,原有的暗示就失效了。
应该把这样的“编号”属性当作普通的属性,把它和对象标识区分开。“编号”属性带有领域含义,而且不是每个类都需要,因此需要出现在分析类中,而标识不带领域含义,是所有类都有的,因此不需要出现在分析类中。
对象标识仅用于标识对象以及在非人系统或系统内部组件之间传递,不需要在人机交互的界面上出现。也就是说,不需要人类执行者输入对象标识,也不需要向人类执行者展示对象标识。如果需要,在人机交互的界面上出现的应该是“编号”属性。
(6)给类加上名为“状态”的属性
很多建模人员会自作聪明地给类加一个名叫“状态”的属性,如图8-45,还洋洋得意,“哇塞,我知道这个类有状态噢,我真厉害!”。
和对象有标识一样,对象有状态,这是面向对象这个领域的共识,和“人员”等具体的类没有特定关系。如果加上去,又变成了批量刷废话。
图8-45 不需要名为“状态”的属性
至于类的状态如何实现,是像图8-45那样添加一个名为“状态”的属性,还是采用其他的实现方式,属于另外一个领域的问题。
注意区分名为“状态”的属性和状态属性。如图8-46,“人员”有一个“已婚”的状态属性(类型可能是bool),这背后包含特定的领域知识,可以暂时留在那里。当然,后面会引入状态机,将类的状态属性变成状态机中的状态,然后从类的属性列表中清除它们。
图8-46 状态属性
关于状态机的建模以及实现,后面章节再详细描述。
*********
如果犯了以上所列举的错误,相当于强行把正交的知识叠加在一起,往往就会得到“废话刷工作量”的结果——记住这个要诀,如果有一天读者想要故意“废话刷工作量”,就把它用上。
举个“废话刷工作量”的例子以加深印象,如图8-47,如何“废话刷工作量”?
图8-47 无冗余的类图,等待“废话刷工作量”
按照上面提到的冗余错误,一个个用上去,可以得到“废话刷工作量”的步骤如下:
(1)因为方框上部是类名,下部是属性,连线是关联,所以,给类和关联的名称加上“类”、“属性”、“关联”。
(2)可以看出,这些类是领域类或分析类,所以,把“领域”加到类名称后面。
(3)因为这些类都代表了一些信息,所以,把“信息”加到类和属性名中。
(4)因为类的属性是“类的”属性,所以,给属性前面加上类的名称;因为关联也是属性的一种,只不过类型是另一个类,所以,每个类为关联加一个属性。
(5)因为每个对象都有标识,所以,给每个类加上ID属性。
(6)因为每个对象都有状态,所以,给每个类加上名为“状态”的属性。
(关联、状态等相关知识,后文讲述。)
演变过程如图8-48。
图8-48 废话刷工作量的过程
类图上的字从最开始图8-48顶部图形的28字增加到底部图形的135字,但没有增加任何有价值的信息。
********************
“信息”、“数据”等也不是不可以作为类的名称。如果系统关注的焦点是"信息处理",处理的信息是什么内容无所谓,"信息"、"数据"也可以作为类的名称,但这个时候就不再有"人员"了,它们不在一个抽象级别。类图可能如图8-49。
图8-49 "信息"作为一个类的名称
8.2.4.5 命名的词性
类和属性应该用名词命名。上文出现的类“人员”、“商品”、“姓名”等就是名词。
在汉语中,动词可以不做任何变化,直接作为名词使用。例如,“我的奋斗”、“嫌疑人X的献身”以及“面向对象设计”,其中的“奋斗”、“献身”和“设计”就是动词的名词化。
因此,“申请”、“审批”、“入库”、“出库”等同样可以作为类的名称。虽然它们看起来是动词,但在作为类的名称的时候,已经被当作名词了。
有的建模人员不理解这一点,总觉得看着不舒服,非得在后面加一个“单”、“记录”、“信息”、“事件”之类,变成“申请单”、“申请记录”、“申请信息”、“申请事件”。
其实并不需要,否则每个动词刷一个后缀,又变成废话刷工作量了。“事件”还说得过去,像“单”、“记录”、“信息”等后缀已经假设了其具体形态,就更不可取了。即使建模时面对的素材是一张纸质的申请单,类的名字也应该写“申请”,因为这才是本质的概念。
图8-50 “动词”直接作为类的名字,不用加后缀
如果一个用语在某个领域中已经存在很久,成为了该领域的术语,即使它看起来犯了以上提到的冗余错误,用来作为模型元素的命名也无妨。例如"订单"带有"单"字,实际上描述的是一次"购买"或“交易”,不过"订单"已经在领域中广泛使用,而且把“单”去掉留下“订”也不合适,所以“订单”可以作为类名。
在其他语言中,动词不一定能直接用作名词,可能要做一些变化。
在英语中,像“设计”是可以直接使用的:design→design,但像“献身”可能就要加一个词缀:devote→devotion。其他词缀可能还有ment、ance、ing、ness等。
★领域驱动设计话语中的“领域事件(Domain Event)”采用了动词的过去式来命名,例如OrderShipped,这是错误的。应该用名词命名,关于这方面的阐述,可参见我写的这篇文章《DDD话语批评之一:评“状态和事件本质相同”》,https://mp.weixin.qq.com/s/QhAhSdET5psZQEEW6f9KoA
因此,在汉语中,“支付”可以作为类的名称(名词),也可以作为操作的名称(动词),而在英语中,类的名称可能叫Payment,操作的名称叫Pay,如图8-51。
图8-51 汉语和英语的类名和操作名
还可以看出,由于英语名词后面加了词缀,像在汉语中忍不住加一个“记录”变成“支付记录”那样,在后面再加一个“Records”之类的概率小了很多。
但也不是所有时候都忍得住,例如,图8-52是“Domain-Driven Design: Tackling Complexity in the Heart of Software”中的一张类图,其中,Delivery和Handling已经是名词,History和Event没有带来有价值的额外信息,可以删掉。
8-52 摘自“Domain-Driven Design: Tackling Complexity in the Heart of Software”
以上说的是动词的名词化。
形容词也可以名词化,例如,“沉默是金”、“把悲伤留给自己”。
不过,在用作类和属性的名称时,形容词和动词不同。首先,形容词不会用作类的名称;其次,形容词用作属性名称时,也不会像动词一样直接把该形容词作为属性名称。
如果需要把一个形容词作为属性,目的必然是测量它的值。这种情况下,领域中往往已经有了另一个术语来专门描述测量值,例如:测量“重”,有“重量”,测量“浓”,有“浓度”。我们应该使用领域中已有的术语。
那如果领域知识中没有这个术语呢,需不需要软件开发人员造一个?例如,在形容词后面加个“值”、“度”、“指数”之类,“美”→“美丽度”或“颜值”,“高尚”→“高尚指数”?
不需要,也没资格。
如果领域知识中没有测量“美”的术语,说明这方面的领域知识可能还不存在,怎么可能会让软件开发人员在软件中封装这些知识呢?
如果涉众硬是需要软件开发人员在软件中封装这些知识,那么此时软件开发人员就不只是软件开发人员了,他要扮演两个角色:首先,他是一名研究“美”的学问的领域专家;其次,他是一名把某个领域专家的研究结果封装到软件中的软件开发人员。这两个角色可以由同一个人担任,也可以分开。
★同前文所述,此处的研究不一定是科学研究,也可以是修仙、漫威、哈利波特、王者荣耀。
当形容词原样出现在类的属性中时,它是作为状态属性出现的。严格来说,状态属性并不是属性,要注意状态属性和上面提到的形容词测量值的区别。
如图8-53,“人”的“重”和“重量”不是一个概念。“重量”是属性,“重”是状态。状态应该用状态机描述,在领域逻辑上,状态“重”也许会仅由“重量”的属性值区间决定,也许会由更多属性的属性值区间决定,在实现上,状态“重”也许会如图8-53左侧实现为一个“重”的布尔类型状态属性,也许不会。
图8-53 状态属性和测量值
关于状态和状态机建模,后文会再详述。
8.2.4.6 命名所用的语言
这里说的不是编程语言,而是汉语、英语、日语……
给核心域元素命名,使用的语言应该首先考虑精确体现核心域内涵和方便开发团队思考和交流核心域知识。该用汉语就用汉语,该用英语就用英语,该用日语就用日语。
以前经常会考虑转换到编程语言时需要改名的问题。在设计工作流,如果我们使用的编程语言只能用英语命名类、属性、操作等——更严谨的说法应该是编译器广泛支持的字符集比较小,那么还需要一个对编程语言合法的名字。
建模工具例如EA,一般会提供别名(Alias),真实名称用编译器支持的字符集,再加一个别名用于显示。
随着时代的发展,编译器、DBMS等支持的字符集越来越大,上面提到的问题慢慢不再是问题。如果团队成员更熟悉汉语,那么在代码中使用汉语命名即可,省去蹩脚英语或者模糊的汉语拼音缩写(啥?你说“可以加注释嘛”?)给后续工作带来的麻烦。
其他如在文本编辑器中切换输入法太麻烦、版式不好看等问题,在图形建模时影响较小。
以上所说仅是针对目标系统的核心域元素的命名,不涉及非核心域部分。非核心域部分内容,和计算机和软件领域有关。这方面的“外语”,不管是计算机和软件专业的各个概念,还是编程语言的关键字,要求软件人员熟练掌握是理所应当的。
例如,浑元形意太极掌门马老师找我们为他的门派搞一套“修炼辅助系统”。作为软件开发人员,能认真去体会马老师说的“化劲”、“武德”、“真气”、“掌门”之类的概念就已经不易,还要把它们变成英语就更难为人了,估计马老师自己也不知道。
但是,作为软件开发人员,不知道什么叫DTO,什么叫Iterator就不应该了,因此,命名为“武德DTO”之类是没有问题的。
如果目标系统是面向国际的,那怎么办呢?还是前面说的,首先考虑精确体现核心域内涵和方便开发团队思考和交流核心域知识。该用汉语就用汉语,该用英语就用英语,该用日语就用日语。
8.2.4.7 类命名用单数
类的名称已经是一个抽象概念,既代表属于这个类的所有对象的集合,也指代集合中的任何一个对象。
例如,“人”这个概念既代表所有符合“人”的特征的对象的集合,我们可以说“人是一种动物”;同时,“人”可以代表“任何一个人的个体”,我们可以说“人有姓名、身高”。这两种用法,分别对应于后文要讲解的泛化和关联关系。
如果用复数表达,例如汉语“人们”,英语“people”,第二种用法就很别扭了,实例“某个人们”是什么?
图8-54 类命名用单数
如果“某个人们”另有含义,那么应该有另外一个类。例如,社区团购系统中,“某个顾客们”另有含义“团”,那么应该添加一个类“团”。
有一些常见的开发习惯,如数据库表名用复数,甚至有的框架在类转换表时,直接就在名称后面加上s,理由是表里有很多行。其实"类"、"表"的概念已经隐含了"多个对象"、"多行"的意思,不用再加了。而且,如果英语不熟,还得费心思去想正确的复数形式,何必呢?
8.2.4.8 自测题
扫码或访问http://www.umlchina.com/book/quiz8_2_4.html完成在线测试,做到全对以获得答案。
1. [单选]
针对下图,以下描述最正确的是:
? A) 这是“打车”领域的分析类图。
? B) 这是“迪迪打车”系统的核心域类图。
? C) 这是“打车”领域的核心域类图。
? D) 这是“迪迪打车”系统的领域类图。
2. [多选]
给类命名时,要注意不要在类名的最后加"情况"、"信息"、"记录"、"数据"、"表"等。请问,针对以下哪些系统的核心域建模,类名中可以出现刚才提到的这些文字?
? A) 雨量数据信息监测系统
? B) DBMS(数据库管理系统)
? C) 疫情信息情况数据查询系统
? D) 搜索引擎
3. [单选]
关于“宝贝”和“商品”,经过讨论和思考,建模人员认为在目前所关注范围内这两个词可以认为相同,“商品”更适合作为分析模型中实体类的名字,那么,以下说法正确的是:
? A) 实体类起名“商品”,但如果某类涉众觉得“宝贝”更顺眼,和他交互的界面上依然可以写“宝贝”。
? B) 所开发系统的所有成分应一律使用“商品”,以建立“通用语言”。
? C) 实体类一律使用“商品”,和人交互的界面上一律使用“宝贝”。
? D) 应该以“宝贝”为抓手,将“商品”下沉到底层架构,击穿程序员心智,打出一套敏捷组合拳。
4. [单选]
以下给类和属性命名,最合理的是:
? A)??
? B)?
? C)?
? D)??
5. [单选]
关于去除分析模型中不该有的内容,以下做法合适的是:
? A) 需求规约(用例规约)里没提到的内容要去除,提到的内容要保留。
? B) 观察模型中各元素的命名,是名词(包括名词化后的动词)的保留,其他去除。
? C) 观察模型中各元素的命名,涉及计算机、软件、网络术语的内容要去除。
? D) 逐个思考模型中的元素,如果去除它会不会影响到功能。
6. [多选]
以餐馆的取号机为目标系统。食客输入有几人就餐,请求取号,系统打印出排号单,上面的信息有餐台类型(大中小……),排队号码。
请问:如果对目标系统使用面向对象分析,根据以上信息判断,以下选项中,属于系统的分析类且命名合适的有:
? A) 食客
? B) 取号界面
? C) 取号控制
? D) 餐台类型
? E) 餐台
7. [多选]
软件开发人员和涉众交流某个问题时,涉众提到一个他工作中的专业术语“差动保护”,开发人员很难理解。对此,以下说法错误的有:
?A) 开发人员应该认真去研究相关的领域知识。
?B) 说明“差动保护”不是双方能用于沟通的“通用语言(Ubiquitous Language)”,开发人员应该带领相关涉众建立方便开发人员理解和沟通领域知识的“通用语言”。
?C) 有的岗位的涉众学历较低、能力较差、知识面较窄,对领域的认识有时还不如开发人员,对这类涉众所说的,如果听不懂,可以先处理其他好理解的问题,然后有空再来处理该涉众的这个问题。
?D) 开发人员应该用敏捷+DDD打法,给涉众赋能,连接涉众心智,通过通用语言(Ubiquitous Language)拉齐水位,形成闭环。
8. [单选]
产品经理王婉菲精读《软件方法》(上),做对书上所有题目以及所有强化自测题,所写的用例规约提交给潘老师评点也得到了好评。这天,王婉菲把一份用例规约交给架构师刘庚宏,刘庚宏做面向对象的分析时,发现得到的类图中,“人员”这个类没有“姓名”属性,以下描述最正确的可能是:?
?A) 系统目前用不到“姓名”的信息。
?B) 用例规约不能覆盖所有需求,需要补一份软件需求规约。
?C) “人员”有“姓名”属性是缺省的,觉得哪里缺,自己加上就可以。
?D) 王婉菲应该在用例规约中补充相应的内容再交给刘庚宏。
9. [多选]
A公司正在开发“新一代供应链系统”,目标组织定位为动力电池厂商B公司,以下可以算作领域专家的有:
?A) A公司架构师张三,曾负责公司多个供应链系统的业务流程建模、实体-关系建模。
?B) 著名教材《供应链管理》的作者、某高校教授李四。
?C) B公司仓库助理王五,熟练使用公司当前的供应链系统的仓储相关功能。
?D) B公司创始人、动力电池专家尼古拉斯·赵六。
10. [单选]
除了中国之外,世界上很多国家也有传统医学。例如,印国的阿育吠陀医学已经延续了五千年。阿育吠陀医学(以下简称印医)认为世界万物包括人类由土、水、火、气和空间五种元素组成,人体有中脉、左脉、右脉三条脉络,然后形成七个脉轮,一万两千个脉的支道。关于致病和治病,印医也有一套十分庞杂的知识体系,包括使用牛的粪尿、水蛭吸血以及念咒语等。
近年来,印医逐渐没落,印国的印医药管理局为了振兴印医,委托A公司开发一套名为“随身老印医”的系统推广全国。该系统打算封装印医知识体系和著名老印医的经验,让印国患者可随时享受正宗的印医服务。
针对“随身老印医”系统的分析工作流,以下说法正确的是:
?A) 应该用现代医学知识建立该系统的分析模型,为患者提供更好的服务。
?B) 印医的知识体系是否科学,不影响软件开发。
?C) A公司开发人员可以和印医领域专家协作,用软件开发术语来帮助提升印医术语,得到通用语言。
?D) 可以借此机会构建一套名为“印医驱动设计的敏锐建模方法”的全新软件开发方法学。
11. [多选]
以下文字中存在“形容词用作名词”的有:?
?A) 假烟假酒假朋友,假情假意假温柔。
?B) 我怕我的眼泪,我的白发,像羞耻的笑话。
?C) 在华丽的城市,等待醒来。
?D) 凋零下的无暇,是收获谜底的代价。
12. [单选]
核酸检测已经成为日常生活的一部分。假设某系统需要某个类来记住人们每一次核酸检测的时间、方式(咽拭子、鼻拭子、肛拭子)、结果等,以下选项中最适合作为这个类的名字的是:
?A) 核酸检测
?B) 核酸检测事件
?C) 核酸检测记录
?D) 核酸检测单
13. [多选]
关于任务和分派,以下类图中命名合适的有:?
?A)?
?B)?
?C)?
?D)?
14. [单选]?
以下类图,抛开其他方面的问题不谈,只谈类名和属性名。
如果在不修改类个数的情况下,删除冗余属性、类名和属性名中的冗余内容,大概有多少比例的内容可以删除?计算方法:(被删除的类名和属性名字数/图中原有的类名和属性名字数)×100%,2个英文字符算一个字。
? A) 36%
? B) 46%
? C) 56%
? D) 66%
8.2.5 审查类和属性
8.2.5.1 属性是否直接描述类
类和属性连在一起说"类的属性",应该能直接说得通,否则类和属性的搭配是不合适的。这个时候应该找到或建立合适的类,把该属性移进去。
★“属性要直接描述类”这个要求和关系数据库的第三范式“不存在传递的函数依赖”相似。
例如图8-55,“人员的组织名称”是“人员的组织的名称”,人员→组织→名称,中间隔了个“组织”,"类的属性"不能直接说得通,需要添加一个“组织”类,把“名称”挪过去。
图8-55 属性要能直接描述类
对于每个属性,我们都这样思考:
“类”能决定“属性”吗?如果能,是怎么决定的?如果不能,还需要什么?
如图8-56左侧,我们可以问:
如果知道专家是潘加宇,小时课酬能定吗?能,是1500元/小时。是怎么决定的?因为潘加宇的级别是“金牌专家”,公司规定“金牌专家”的课酬是1500元/小时。
由此可知,专家→专家级别→小时课酬。添加一个“专家级别”类,“小时课酬”作为它的属性。
图8-56 “类的属性”的调整
如图8-57左侧,我们可以问:
如果知道商品是“百事可乐罐装330ml”,数量能定吗?不能。还需要什么?还需要知道是哪张订单,因此需要一个类,既知道订单,又知道商品。添加“订单项”类,“数量”作为“订单项”的属性。
图8-57 “类的属性”的调整
属性如果放在了错误的类,极有可能会导致大量不同对象的某些属性值相同,而这也可以反过来思考,当发现大量不同对象有相同属性值时,可以检查一下,是否有属性放在了错误的类。
就以图8-55为例,如果按照左侧,可能会有如图8-58的一些对象,发现其中“人员”对象345677和345679的“组织名称”相同,可以检查一下,是否可能漏了一个“组织”类。
图8-58 一些人员对象
注意,以上是说“可以检查一下”,不一定就需要调整。多个对象的部分属性值相同的情况很常见,好多人姓名叫“张伟”,好多人年龄是18岁,并不一定要分出来另一个类。
可以合并的情况
如果确定两个类A和B的关联多重性是1对1,而且其中一个类B只有一个属性,那么,可以把B合并进A,如图8-59。
图8-59 特定条件下合并类
图8-55提到的“人员”和“组织”并不符合合并的条件,因为一个组织可以有很多人员就职。符合合并条件的例子如图8-60的“人”和“出生”。一个人只会出生一次,而且出生只关注一个属性:日期,那就可以把“出生”删掉,在“人”中放置“生日”属性。
图8-60 特定条件下合并类示例
“一个人只会出生一次”这一点很稳定,但“出生只关注一个属性”就没那么稳定。如果“出生”需要关注多个属性,例如除了关注出生时间,还要关注出生医院,那就不宜合并了。
还可以看出,时间类型的属性实际上是某种事件发生的属性,只不过有的时候被合并到其他类中,看起来成为了该类的属性。如图8-60中,“生日”成为了“人”的属性,类似的还有:“下单时间”成为“订单”的属性等。
导致出现违反本要点的错误的原因有:
(1)缺少抽象能力
缺少抽象能力的建模人员经常会把手上素材的信息,一一对应地映射为类和属性,导致本来属于多个类的信息被合并在一个类中。
如图8-61,建模人员对照着一张货物运输托运单,直接把它映射成类,照抄上面的每一栏作为属性。
图8-61 错误:直接把素材照搬成类
建模人员有时还觉得挺符合“类的属性”的。“货物运输托运单的货物名称”,没错啊,手边这张货物运输托运单上确实明晃晃地印有“货物名称”四个字嘛。
托运单、出库单、销售单等各种单据,以及身份证、工作证、图书卡、设备卡等各种卡片和证件,在信息时代之前就已经存在了。它们相当于某种“信息化”的存储结构,存储一个或多个概念的信息,只不过保存的载体不是电子载体,而是甲骨、铜器、竹片、木片、帛布、纸张。
现在,既然用信息系统取代了这些单据、卡片和证件,那么要建模的实体类应该是它们所存储的领域概念,而不是单据、卡片和证件本身。
图8-61右侧的类“货物运输托运单”应该变成如图8-62。
图8-62 建模早期信息存储载体所存储的领域概念
★开发团队中可能会有人唧唧歪歪,什么“性能”啦,什么“我们之前都是这样做”啦。碰到这种情况,也不用祭出鲁迅先生的“从来如此,便对么?”,你只需要问他,扔给他一张类似图8-61的**单,他有能力干脆利索地得到类似图8-62吗,没能力就让他闭嘴。
在有能力得到图8-62的基础上,可以再来考虑需不需要一个如图8-61右侧的“货物运输托运单”类来维护当时的快照。因为随着时间的推移,对象的属性值会变化。
例如图8-62中,随着时间的推移,如果某个单位的名称或地址改掉了,此时从图8-62计算出图8-61左侧的托运单快照,和事件发生的当时按照图8-61左侧填写的信息是不一样的。
如果这样的快照很重要,可以另外加一个如图8-61右侧的快照类来维护这些信息,这个类是孤立的,和图8-62的类不存在关联。
但这样做很容易出现沿着关联线向外延伸,雪球越滚越大的情况。如图8-62,单位的联系人会换人,人员的电话会改变……最终可能需要一个巨大的类,把所有通过关联线连在一起的类的所有属性组合在一起,而背后的数据量也是庞大的。
更合理的做法是记录本质的变更来源。如果单位变更名称和地址是值得关注的事情,应该添加一个类记录单位变更名称和地址的细节,或者说,记录所有值得记录的对象属性值变更的细节。在此基础上,需要类似图8-61的某个时间点来自多个对象的属性值组合快照时,可以通过计算来还原。
在这里,我们要认清楚非常重要的一点:本质是对象之间存在关联,而不是对象属性值之间存在关联。如图8-62,两个单位之间曾经存在的某次托运关系,并不会因为单位后来改名或改地址而变化,相对于如图8-61列出一堆属性值,记住“托运”和“单位”之间的关联是更本质的——当然,并不影响从本质模型还原出各种视图或报表。
正如上文提到的,如图8-61是在信息化时代之前的一种“信息化”的存储结构。当时不要说没有方法学,即使有方法学让你先分解概念为图8-62,也没有计算设施把它们按需要组合成图8-61或更多的视图。
现在,既然有了条件,就没有必要再去模仿条件不足时的拙劣“模型”了。
很可能在这个时候,伪创新又乘机迎合那些没有抽象能力又不愿意学习的无能之辈。伪创新的说法是:图8-61的快照是发生过的事情,是不会变化的,各种所谓的“流水”才是本质!于是,无能之辈非常开心,热烈拥抱伪创新。
我们可以用科学研究类比一下:背后的规律没搞清楚时,要推测某个数据,可能是靠“经验”来推测。“经验”其实就是发生过的“流水”的记忆。当科学家研究巨量已有的“流水”(即实验数据),探索出其中的规律后,这时再推测数据,就没有必要从之前的巨量“流水”来推测了,根据归纳出来的公式推测即可。当然,之前或之后的各种“流水”可以继续保留,但它们不是本质,是现象。
何况,不是所有的系统都需要保存“流水”。电梯每天上上下下,不知发生多少次“召唤”事件,但目前的电梯系统并不会记录“召唤”事件的细节——谁召唤的、什么时候召唤的……系统只需要维护本质的行为规则,如果采用本书的方法学,可以选择用状态机来表达。
当然,也许有一天,电梯系统有了足够的存储资源,会记录所有的“召唤”流水,但这和背后的规律没有关系。系统是否记录某个事件的细节,不影响事件是否发生、事件是否产生效果,以及背后的行为规则。
(2)受关系数据库建模的影响
建模人员有时会犯这样的错误,在一个类里放上另外一个类的属性作为“外键”。比如针对上面的例子,建模人员会想:“人员”里放“组织名称”确实不合适,但是放个“组织编码”作为外键总可以吧?其实也不可以。"组织编码"是“组织”的属性,是封装在“组织”中的秘密,“人员”不应该拥有“组织”的任何属性,它只能通过关联拥有“组织”对象,然后通过访问“组织”对象公开的操作来间接访问“组织”的属性。
图8-63 不需要“编码”作为“外键”
“人员”里放“组织编码”不合适,放一个无意义的标识“组织ID”呢?同样也不可以。因为这个“组织ID”是“组织”的标识,前文已经说了,标识属性此时不需要存在,所以“组织ID”在“组织”里不存在,更不要说放到其他类中作为“外键”了。
图8-64 不需要“ID”作为“外键”
在设计工作流,需要把类图映射到关系数据库时,确实需要把"组织"表的主键(可能是"编码"也可能是生成的代理主键)放在"人员"表中作为外键,但正如上文所说,这同样是另一个领域的知识,而且映射规律和核心域知识无关。
状态属性是否合适
前文说到词性时,提到了状态属性。状态属性的名称是一个形容词,类型为布尔类型,用来标记对象是否处在某个状态,如图8-65。
图8-65 状态属性
这些状态属性可能是来自素材中的定语,例如,用例规约提到“可享受优惠的顾客”,那么在识别的时候可能会先把“可享受优惠”放在“顾客”类中。
和“类的属性”刚好相反,状态属性和类连在一起说,要能说得通"属性的类"。例如,图8-65中,“可享受优惠的顾客”是说得通的。
不过,说得通"属性的类",也并不表明状态属性是最终的建模结果。后面的审查步骤中,我们会再审查状态属性。
8.2.5.2 属性是否可以从其他地方推导
如果一个属性可以从其他地方推导出来,那么这个属性就是冗余的,可以删掉。
如图8-66,人的年龄可以从出生日期计算得到,应该把年龄删掉。
图8-66 年龄可以从出生日期推导
这个“其他地方”也可以是所关联的类的属性。如图8-67,订单的总金额可以由各个订单项的金额合计得到,那么可以考虑把总金额删掉。
图8-67 总金额可以从各订单项金额合计
状态属性的冗余
状态属性是冗余的,背后往往隐藏着更多的逻辑,需要进一步推导。
例如,图8-65中,“顾客”有一个状态属性叫“可享受优惠”,我们可以问:为什么一名顾客可享受优惠,理由是什么?可能是年龄≥70岁,可能是他有个消费账户余额≥5000元,那么应该把这些规则通过类图、状态机图显式建模出来,然后把状态属性去掉。
例如,可以画状态机图如图8-68。
图8-68 顾客的状态机片段
*********
清除冗余时,也经常会有人以“性能”作为遮羞布拒绝思考和废话刷工作量。
如果提出性能问题的这个人知道如何在不考虑性能的情况下得到一个清晰、无冗余的模型,那么他还有资格说一说性能问题。
不过,如果他知道如何得到一个清晰、无冗余的模型,说明他具备了一定的建模能力,往往也不会在分析的时候担心性能问题,因为他知道,如果碰到性能问题,可以按照某些套路添加冗余,这些套路和目前所思考的领域知识没有关系,没有必要混杂进来。
在分析工作流不断提出性能问题的人,您可以尝试让他整理一下核心域逻辑,也可以去看看他之前写的代码,大概率会发现其中逻辑的组织是很糟糕的。
/*
这是人的本性中急于回到自己的舒适区的一种表现,不只是在分析工作流如此,其他工作流也会有类似现象。
例如,开发人员没有掌握需求技能,也不愿意学习,即使给他时间去做需求,他也不知道应该怎么做。只好随便找人问问,开个会,走走过场,然后就着急回到编码的舒适区,甚至在讨论需求的时候都已经在不由自主地思考实现小细节。
为什么“现场客户”之类的东西会吸引开发人员,就是迎合人的这些本性,让开发人员可以安然坐在电脑前面,呆在自己的舒适区里。
呆在舒适区并不一定是错的。
如果出于个人爱好和天赋,专精某方面的技能,在某个领域深入挖掘,解决该领域各种难题,这是非常好的选择。不注意自己的爱好和天赋,什么都想学习,很可能什么都浅尝即止,以“学习新知识”来逃避碰到的各种难题。
错的是自欺欺人。
当形势需要人走出舒适区来应对困难的时候,如果说“我只擅长我手上的东西,这个我对付不了,我也没有兴趣和勇气去对付,还是让我解决我擅长的问题吧”,这个可以理解;如果说“这个只需要用我擅长的东西对付就足够了”,那就属于自欺欺人了。
*/
8.2.5.3 属性是否在本领域内可分解
如果属性再分解就得到另一个领域的概念,那么这个属性可以留在类中。如果属性可以继续分解成本领域的概念,那么可以考虑把这个属性独立出去变成另一个类。
如图8-69,"人员"的"称呼"属性的类型是String,相当于"人员"关联到String类。String属于基础语义领域,已经不属于"人员"所在的“人员管理”领域,那么"称呼"可以留在“人员”中作为属性存在。
图8-69 String可以留在类中
而"组织"还可以像右侧所示分解成“名称”、“办公地址”等,这些概念依然属于“人员管理”领域,所以可以考虑将“组织”分离为一个类,“人员”关联到“组织”。
图8-70 组织需要分离出来
注意以上的用词。我们只是说“再分解就得到另一个领域的概念”,并没有暗示这个另一个领域是什么,更没有暗示这个领域比核心域简单。任何领域,只要愿意深入研究,都是复杂的。
就拿“称呼”的类型String来说,String如果用.NET中的String类实现,这个类有157个操作,远远超过“人员管理”领域中某个类所拥有的操作数目。只不过String类如何构造是别人负责操心的事情,不是我们目标系统的建模人员操心的事情。
在分析工作流,我们只需要判断某个属性再分解就到了另一个领域,或者说类型是另一个领域的类。至于属性的类型具体是哪一个类,UML提供了一些原生类型,如果认为属性的类型刚好是这些类型之一,可以指定,否则可以先不指定,因为分析模型没有绑定到任何一个具体的编程语言和数据存储平台,而不同的编程语言和数据存储平台的类型体系还是有差别的。
在EA中,把类的语言设成none,在属性的类型选择中,就看到UML定义的原生类型,如图8-71。
图8-71 UML定义的原生类型
8.2.5.4 是否有多重性大于1的属性
如果某个属性经过了8.2.5.3 属性是否在本领域内可分解的检验,但该属性多重性大于1,即所谓多值属性。例如,人员有多个手机号。
这时可以:
(1)把“手机”属性留在“人员”类中,多重性设为多,如图8-72。
图8-72 把属性的多重性设为多
注意,只需要说明多重性为多,不要放上一个该属性的数组或List之类。人员有多个手机号,这是一个领域的知识;用编程语言如何实现多值属性,是另一个领域的知识。一旦混杂,又会导致批量刷工作量。
(2)更推荐的做法是,把该属性分离出去,如图8-73。
图8-73 分离多值属性
人员有多个手机号,背后往往是有原因的。虽然现在不关注,很可能以后就会关注。例如,有的手机号是私人用的,有的手机号是办公用的,如果需要关注这些知识,那么就需要从图8-73转成图8-74中的某一个,此时只需要添加关联或者在“手机”类添加一个属性。
图8-74 当需要关注更多的知识时,类图发生变化
当然,这也不是建模“人员有多个手机”的最好做法。后文我们介绍到人员相关的分析模式时,会详细描述如何建模这个领域。
以下做法是不好的:
(1)在“人员”类中放上多个属性“手机1”、“手机2”、“手机3”……,如图8-75。
图8-75 错误:放上多个属性
这样的做法相当于把抽象级别降到了对象级别,或者用关系数据库建模的说法,这是违反第一范式的。如果人员没有那么多手机号,某些属性就会出现空值;如果人员拥有手机号的数量大于预设的个数,就会放不下。
(2)只有一个“手机”属性,属性值里用逗号分隔各个电话号码。
如果这样的做法是好的,那不如更进一步。各个属性也不用分了,就一个字符串。还可以再进一步,类也不用分了,也串在一起……持久存储或网络传输时的序列化不就是这样干的吗?
我们之所以引入各种抽象,根源就在于软件是人做的,人脑的容量和运行速度有限,这些抽象可以帮助人脑应对复杂性。
8.2.5.5 属性是否对所有对象都有意义
如果通不过8.2.5.1 属性直接描述类的检验,那么类和属性放在一起是不合适的,但这只是必要条件,不是充分条件,即使通过了也未必合适。
如图8-76,人的姓名,人的▲▲(▲▲是男性特有的器官),人的〇〇(〇〇是女性特有的器官)好像都说得通,但如果问:是不是所有对象都应该有这个属性呢?得到的答案就不同了。
是不是有人有姓名——是。
是不是所有人都应该有姓名——是。
是不是有人有▲▲——是。
是不是所有人都应该有▲▲——不是,只有一部分人有。
是不是有人有〇〇——是。
是不是所有人都应该有〇〇——不是,只有一部分人有。
说明“人”发生了分裂,分裂成“男人”、“女人”两个子集(子类)。▲▲是男人的属性,〇〇是女人的属性。
图8-76 分解只属于部分对象的属性到子类
8.2.5.6 自测题
扫码或访问http://www.umlchina.com/book/quiz8_2_5.html完成在线测试,做到全对以获得答案。
1. [单选]
以下符合“属性直接描述类”的是:
? A)?
? B)?
? C)?
? D)?
2. [多选]
在符合某些条件时,这样建模是可以的,请把这些条件选上。
? A) 手机没有子类
? B) 手机只有一个摄像头。
? C) 摄像头只关注一个属性:像素。
? D) 用关系数据库来保存手机对象。
3. [单选]
电子商务系统有一个类“商品”,“商品”有属性“名称”和“演示动画”。开发人员初步打算在实现时,“名称”的类型设为编程语言的String,“演示动画”的类型设为某个通用3D类库的“Animation”类。以下说法正确的是:
A 因为String比较简单,所以“名称”属性可以留在“商品”类内。
B 因为Animation比较复杂,所以“演示动画”属性应该分离出去变成关联。
C 因为String和Animation都比较复杂,所以两个属性都应该分离出去变成关联。
D 因为String和Animation都不属于核心域概念,所以两个属性都可以留在“商品”类内。
4. [单选]
在某个系统中有一个“电子邮箱”类,它的对象用关系数据库存储,放在数据库的“电子邮箱”表中。“电子邮箱”表的部分行如下:
作为一名学习了本部分内容的读者,看到这些行,应该想到的是:
?A) 类和表的结构基本一致,类的属性相当于表的列。
?B) 可能需要把一些属性移到另一个类。
?C) 密码不应该用明文存储。
?D) 这样做不方便发挥领域驱动设计的艺术来捕获领域业务用户需求的功能架构技术逻辑规则。
5. [单选]
使用关系数据库来存储数据时,发现某个表的数据中,每一行都有某些列的值为空。从面向对象建模的角度看,这可能犯了什么错误?
? A)忽略了泛化关系
? B)把泛化当成关联
? C)把关联当成泛化
? D)把设计当成需求