墙上挂了根长藤,长藤上面挂铜铃
《长藤挂铜铃》;词:元庸,曲:梅翁(姚敏),唱:逸敏,1959
可到此处下载本文档最新版本:
http://www.umlchina.com/book/softmeth08part1.pdf
您在阅读《软件方法》时如果发现错误,欢迎通过微信umlchina2告知。如果作者认为有道理,决定在下一次发布时根据您的意见修改,每个错误将付给您5.12元报酬,并在书中说明您的贡献。报酬通过微信支付。
(1)任何您认为的错误都可以,包括错别字。
(2)同一错误仅支付最先指正者报酬。
(3)请根据最新版本作指正。
下册内容目前指正人有(按指正时间排序):吴佰钊、王周文、刘学斌、成文华、黄树成、李蜀斌、杨雪鸿、王书伟、高洪江、张志坚、龙燔。
从分析工作流开始,我们每个内容都分为两章。一章讲述建模知识,一章讲述建模知识如何应用在本书案例中。这样的分割主要考虑到更符合实际的工作。
例如,在讲解分析类图时,我们是按照这样的顺序讲解知识:
(1)识别类和属性
(2)审查类和属性
(3)识别类之间的泛化
(4)识别类之间的关联
如果把案例剖析分解到每个知识点,为了让案例的剖析符合内容的顺序,可能就会出现这样的情况:
讲解完识别类和属性后,案例剖析时,先列出很多类和属性,但没有泛化和关联关系,因为类的关系还没有讲到,所以即使观察到,也故意不画上去;接下来,讲解完类之间的关系后,案例剖析时,再把关系加上。
这和实际工作不符合。实际工作中,以上列出的几项工作是交叉进行的。
为了避免造成误解,我们先完整地讲解知识部分,本书案例如果有和所讲解知识相关的,也会随时引用。然后在案例部分,按照实际工作中的思考方式灵活应用前面所讲解的知识点。
8.1 分析工作流概述
8.1.1 知识的分离
在业务建模和需求工作流,我们一直把目标系统看作是一个整体,想办法推导出涉众在意的整体表现——需求。
系统为了满足需求,必须封装一定的知识。这些知识,没法从天上掉下来,需要软件开发人员一点一点放进去。接下来,我们将思考:
(1)如何准确表达系统需要封装的知识,让系统满足需求;
以及进一步
(2)如何合理组织系统需要封装的知识,低成本地让系统满足需求。
如果不能合理组织知识,当新需求到来时,准确表达也会越来越难。如果考虑到利润,很难停留在(1)而不追求(2)。
不管是纯粹在大脑里面打转转,还是借助了纸笔或建模工具来协助,以上的思考是逃不掉的。如果需要封装的逻辑很简单,人脑的容量和运算速度能够胜任,在大脑里打转转可以勉强应付。不过,能带来利润的系统都是复杂的(参见《软件方法(上)》1.8.1 市场没有小系统),借助纸笔或建模工具来显式表达思考的过程很有必要,毕竟大脑容量和运算速度比一般人高出一个数量级的天才是很稀罕的。
有的人故意不显式表达,声称“大脑思考就够了”,背后的真相可能不是天才而是遮羞——你让他显式表达,他也表达不出来,因为没有掌握思考的方法。
思考的方法,也就是知识分离的方法,包括域和域之间的知识分离,以及域内部的知识分离。
8.1.2 核心域和非核心域
一个软件系统封装了若干领域的知识,其中一个领域的知识是系统不能抛弃或替换的,这个领域称为"核心域",其他领域称为"非核心域"。图8-1展示了不同系统类型的核心域和非核心域概念。
图8-1 不同系统类型的核心域、非核心域概念
以文档处理器为例,开发Microsoft Word和LibreOffice Writer所使用的编程语言和组件不一样,但文档、页、行、字等核心域概念是一样的。即使回到计算机诞生之前或者去到未来,这些概念也依然存在。
关于“核心域”和“非核心域”,一种常用的通俗说法是"业务"和"技术",但"业务"和"技术"的说法并不严谨。
有的开发人员在潜意识里是这样划分的:
*我懂且我感兴趣的知识→技术;(我懂Java编码,我对Java编码感兴趣,Java编码是技术)
*我懂但不感兴趣的知识→业务;(下单、收银、配送我懂一些,但不感兴趣,这些是业务)
*我不懂但感兴趣的知识→高科技;(我不懂深度学习,但很感兴趣,哇塞,高科技)
*我不懂且不感兴趣的东东→忽悠。(我不懂UML建模,也不感兴趣,妈的,忽悠)
有的开发人员则是这样划分的:
*和计算机无关→业务;
*和计算机有关→技术;
核心域不能以“懂”、“感兴趣”来判断。核心域不一定是非计算机领域,也可以是计算机领域,如图8-1中的操作系统。
另外,还要特地说明的是,本书中的“核心域”和Eric Evans以及后续的DDD(领域驱动设计)话语体系中的“核心域”(Core Domain)意思不同。
本书中的“核心域”指软件系统中不可替换的那部分内容——这个以软件开发人员的知识是可以判断的。
DDD话语体系中,把“领域”(相当于本书中的“核心域”)划分为"核心域"、“通用子域”、“支撑子域”等,例如“Delivery”是核心,“Customer”是通用,“Billing”是支撑——这个划分已经超出了软件开发人员的知识,我不认为软件开发人员有能力以及有必要做这样的判断。
一家商场之所以能击败其他对手,原因未必是下单部分有什么不同,倒有可能是在配送环节下了大力气,或者客户服务抓得好。没有经过商业竞争的思考,武断地认为某个子领域是“核心”是不合适的。
8.1.3 域之间的映射和协作
域和域之间的映射以及协作的规律,与域中的个体不直接相关。
例如,我们看一个"人员管理系统"的核心域类图,如图8-2所示。
图8-2 "人员管理系统"的核心域类图
如果将图8-2中的Person类映射为C#实现,可能会得到图8-3的C#代码:
图8-3 类的C#实现(用Enterprise Architect映射)
如果将图8-2中的类映射到关系数据库,会得到图8-4所示的数据库结构:
图8-4 将类图映射到数据库模型(用Enterprise Architect映射)
如果采用某种对象-关系映射器框架(例如微软的Entity Framework),Person对象和数据库中的Person表里的一行可能会这样联系起来:
person1=context.Persons.Find(ID)
如果将以上内容中的Person改成Dog,City改成Cat,映射的套路没有变化。即使我们调整了域之间的映射和协作的套路,得到的结果也会按照我们的调整有规律地变化,与域中的个体依然无关。
平时我们看到的一些“架构”,就是域之间映射和协作的一些套路。图8-5是现在常被提起的一些“架构”,可能在不同领域的系统中都会观察得到。
图8-5 一些常见的“架构”
既然域之间的映射有“套路”,过早地混合不同域的知识是不划算的。
如图8-6所示,假设三个域要考虑的因素分别是a、b、c个,如果分开考虑,然后再找到域和域之间映射的规律,负担最小可以变成a+b+c;如果混在一起考虑,大脑的负担最大会达到a×b×c。a、b、c都大于√3时,相乘肯定要大于相加的。
图8-6 过早混合不同域的知识会增加大脑负担
把不同域的知识分开考虑,不等于“把整个系统分析完了再编码”——“害怕分析瘫痪”是一块常用于遮掩分析能力不足的遮羞布。可参见《软件方法(上):业务建模和需求》1.3节关于“迭代”的内容。
过早地混合不同域的知识,会加重开发人员大脑的负担,导致开发人员腾不出脑力来思考核心域中更深刻的问题,只好稍微折腾一下如图8-5的“域之间的架构”,心里安慰自己,我有“架构”了!却忘了,其实还没有触碰到最需要大脑去思考的核心域概念和逻辑。而这又很可能会被巧妙地当成遮羞布——不是我不思考,而是要想的事情太多了顾不过来啊!
而这种微妙心态的进一步发展,就会导致开发人员有意无意地混合不同域的知识,把复杂度弄成a×b×c,以此达到废话刷工作量——以最少的思考得到最多的“成果”。
*近年最时髦的就是借DDD话语体系刷工作量了。例如,刚找出一个类Order,然后周围就围上一圈OrderFactory、OrderRepository、OrderService……,洋洋得意地把工作量刷了好几倍。*
我经常听软件组织的架构师向我介绍他们所开发系统的“架构”,口沫横飞,说的基本上都是图8-5的“域之间的架构”。好啊,真棒,我知道了。还有呢?没了?
构思那些“域之间的架构”是某些厂商或者方法学家的工作,我们挑一个适合自己项目的套路用上就行了。有什么问题,可以去请教用这个套路用得好的先行者。
“域内部的架构”,那些核心域概念和复杂逻辑,这是系统最值钱的地方。要是我们没有办法理清楚,别人是帮不到我们的。这才是大脑最该用的地方!
8.1.4 应对变化,不要吃错药
平时开发人员常说要“应对变化”,甚至有的人还喊口号“拥抱变化”,但我们需要认真想一想,要应对的是什么样的变化?
我列出各种“不好了,需求变了!”的情况如图8-7。
图8-7 各种“需求变化”
第一种情况实际上是最多的:需求其实没变,只是需求人员的“认识”变了。这种由于业务建模和需求技能太差导致的“需求变化”实际上是最多的。
要应对这样的“变化”,光是有分析和设计技能是没用的,需要提升业务建模和需求技能。这种情况和本章的内容就没有关系了。
*张三出现恶心、乏力、食欲减退等症状,村里的道士九叔给他诊断,认为他鬼上身了,需要搞一个驱魔仪式。九叔已经精研驱魔理论体系和实践多年,一手辟邪剑法已经练到星耀一级。
那也没用!因为张三得的是乙型肝炎。*
第二种情况是第二多的:功能需求变化,包括增加了一个用例、增加了一个步骤、输入输出的字段多了一项、某个步骤的业务规则做了调整……等。如果能恰当地建模系统要封装的核心域逻辑,使得核心域模型能精确体现核心域的内涵,会大大有助于应对这样的变化。
应对第二种情况,需要提升分析技能。
*如果充分了解肝脏的工作机制,当张三被诊断乙肝时,又观察到张三酗酒、熬夜,就可以预测很可能张三会肝硬化甚至肝癌,对应变是有帮助的。*
第三种和第四种情况发生得就没那么频繁:质量需求和设计约束发生变化,例如响应速度、并发容量、运行平台的更换等。
一些以“领域驱动设计”为名的文章,所举例子就1-2个领域类,然后就开始讨论Entity、Service、Repository、DTO、六边形架构……不是说这个知识没用,问题是软件组织缺的是这个嘛?
这些文章以为自己在说“领域驱动设计”,其实说的是“企业应用架构模式”、“互联网系统架构模式”。
强调“领域驱动设计”,背后暗含的意思应该是缺少“领域驱动”而不是缺少“设计”,结果呢?不谈领域,谈仓储、工厂。
*调查:您看过的以“领域驱动设计”为名的文章,里面有几个领域类?*
5个以下
6-10个
大于10个
在一些软件开发技术大会常可以看到这样的场景:某电子商务网站的架构师上台讲了一通,接着某视频网站的架构师上台也讲了一通,咦,两个演讲内容如此相似?原来,他们讲的都是自己系统中“域之间的架构”,而不是核心域内部的机制。究其原因也许并非不为,而是不能——架构师对自己所开发系统的核心域研究太浅。
许多“网红程序员 ”在网上谈论的内容大多是某种语言或框架的新特性,少有探讨他当前所开发系统的复杂领域逻辑,也是同样的原因:并非不为,而是不能。
说了那么多,归纳起来就是一句话:
8.1.5 重视分析工作流
分析,就是从核心域的视角构思系统的内部机理。
在现在的很多软件组织中,分析工作流的技能是非常被忽视的。很多开发人员上手就直接编码,原因并不是软件开发项目的领域逻辑简单到了不需要分析的地步,或者他的大脑比其他人发达,在大脑里就可以完成分析的工作,而是开发人员缺乏分析的技能,只好草草跳过,而且为了遮掩自己的无能,还会想各种办法来遮羞。
草草跳过的不只是分析,需求也是同样的待遇。很多需求调研就是走个形式。开发人员没有掌握需求技能,给他时间做,他也不知道怎么做,随便晃悠两下,就着急回去编码了,因为这个工作他比较熟。
用考试类比,考试时,前面几道题比较容易,扫一眼就可以写出答案。越往后题目越来越难,学霸会拿出草稿纸,列出已知条件,正推、逆推……理出解题思路,然后再答。
学渣就麻烦了,根本没有学习相关的知识和解题方法,怎么办?
遮羞利器(1):时间。
例如,抱怨考试时间太紧张,来不及思考,只好胡乱写个答案,甚至故意提前交卷,力图给人造成一种“如果时间允许,我是能做对的”的印象——真相是,给再多的时间也不会。
对应到软件开发,就是以“时间紧”、“敏捷”为借口掩盖自己没有能力剖析复杂逻辑的事实。
遮羞利器(2):空间。
例如,考试时故意选择不好写的笔和劣质的草稿纸,力图给人造成一种“如果纸和笔再好一点,我是能做对的”的印象——真相是,不会就是不会,给他再好的纸笔也不会。
对应到软件开发,就是借助“口头交流”、“白板”等容量小的介质,掩盖内容的苍白。白板就这么大,所以客观上你总不好意思让我用白板剖析复杂的逻辑吧?
伽罗瓦在决斗前一天晚上仓促写下自己的数学思想,不停哀叹“我没有时间了”。唉,早干嘛去了,不过伽罗瓦是真懂。
图8-8 伽罗瓦决斗前一天的手书
费马在书的空白处写下“费马猜想”,还写“我确信我发现一种美妙的证法,可惜这里的空白处太小,写不下”,估计费马是忽悠。
此处提到此二人纯属作者关于“时间”、“空间”不够的随意联想,无其他含义。
遮羞利器(3)听起来就比较高大上了:重构。
上世纪80年代末,Bill Opdyke(https://cseweb.ucsd.edu/~wgg/Abstracts/gristhesis.pdf)和Bill Griswold(http://laputan.org/pub/papers/opdyke-thesis.pdf)等人归纳了一些调整代码结构的手法,称为“重构”,后经Martin Fowler等人推广而广为流传。
“重构”的知识可以看作是建模知识的一个子集。如果开发人员真的熟练掌握重构的手法,很多情况下他已经有能力直接建模领域逻辑得到更合理的结构,根本不需要先走很多弯路再回正路。
还是用考试类比:如果考生有能力察觉某个解答的“坏味道”并“重构”,那么轮到他做类似题目时,他也应该有能力“建模”题目的各种条件,理清解题思路后直接给出正确的回答,并不需要故意做错再改过来。
如果考生别有用心把“重构”当遮羞布,结合前面两个遮羞利器,就会出现“我本来打算把我的答卷'重构'一下,但是没有时间了”,“我本来打算把我的答卷'重构'一下,但是答卷写满了没空间了”。
开发人员可以照此办理——“我先写快而脏的代码,然后再重构”,然后祭出遮羞利器“时间”(此处“空间”不好祭出)——“没想到啊,时间来不及了”。
摸着石头过河是难免的,但应该在不得不摸的时候才摸,不应该假装看不见已有的路和桥,无论大小事都主动追求摸着石头过河,而且,很多人不是假装看不见路,而是真的看不见路——就是个睁眼瞎。要是开发人员以“重构”为理由拒绝思考,很可能他的“重构”也是空话。
不过,大脑不用思考,凭感觉摸着石头过河不停刷工作量,也是一种躺平的幸福。
8.1.6 分析相关历史的简单回顾
1958年,John W. Young Jr.和Henry K. Kent发表“Abstract formulation of data processing problems”,第一次提出在独立于实现的抽象级别上定义系统的规范。
图8-9 来自 “An abstract formulation of data processing problems”(Young JW, Kent HK,1958)的截图
1959年,CODASYL(数据系统语言会议)成立。1962年,CODASYL提出了一个和Young/Kent类似的模型,称为“信息代数”(Information Algebra)。
1970-1980年代是结构化分析方法的时代,主要贡献者有Börje Langefors、Chris Gane、Trish Sarson、Tom DeMarco、Pin-Shan Chen、E. F. Codd等人。结构化分析的主要建模方法是数据流图和实体-关系图,这两者的结合,让软件开发人员有能力剖析大型系统。
图8-10 来自 Structured analysis and system specification(DeMarco T,1979)的截图
图8-11 来自 The Entity–Relationship model: Towards a unified view of data(Chen PPS,1976)的截图
1982年,Nastec公司开发出了DesignAid,这是第一款CASE(计算机辅助软件工程)工具。随后,其他CASE工具陆续出现。据PC Magazine的1990年1月30刊统计,当时已经有超过100家公司提供了将近200款CASE工具。
图8-12 来自PC Magazine 1990年1月30日刊的截图(被圈住的内容说明了工具的数量)
1980年代后期,面向对象的思想开始用于分析和设计。然后,UML统一了表示法。这部分历史已经在本书第1章“UML简史”部分讲述,此处不再赘述。
图8-13 来自 Object Oriented Analysis, 2nd Edition(Coad P, Yourdon E, 1990)的截图
图8-14 来自 Object lifecycles. Modeling the world in states(Shlaer S, Mellor SJ, 1992)的截图
8.1.7 互联网和敏捷的影响
互联网浪潮以及敏捷运动的冲击打断了分析的传承。
互联网浪潮到来之前,软件系统的竞争焦点是功能。
我1997年毕业,先到高校当了一年老师,然后才去软件公司做程序员。第一个参与开发的系统是酒店管理系统。这样的系统用的人不多,服务器一台,每个部门放上一台客户端电脑就差不多了,但功能很多,入住、退房、收银、客房,餐饮、娱乐、财务、电话计费、各种报表等等,能不能把领域逻辑理清楚非常关键。
互联网的兴起带来了这样一种系统:这种系统功能很简单,开发这种系统时需要思考的领域逻辑很少,但是这样的系统可以通过互联网让非常多的人使用,问题的关键变成了“如何在大用户量下保持性能”。
典型的例子是1996年出现的hotmail,推出一年多时间就有1200万的用户。hotmail是一个基于web的电子邮件系统,这样的系统,开发出来并没有太大难度,竞争的关键在于有没有背景、有没有钱买基础设施,有没有钱做推广……。
可能有人会说“邮件系统也有逻辑啊!”当然,这同样是一个领域,也有逻辑,但是其中的绝大多数逻辑已经被前人探索得很清楚,甚至有实际的可用组件提供,并不需要web电子邮件系统的开发人员从头思考。
很多开发人员就进入了类似的“互联网公司”,开发或维护类似的系统。因为不需要剖析复杂的领域逻辑,开发人员有没有掌握分析的技能已经无所谓,于是,很多打着“敏捷”旗号的“方法”就在这类公司大行其道,导致软件开发人员的分析能力普遍退步。
经常有人和我说,潘老师,敏捷这一套做工厂管理系统之类的可能不太行,但不得不承认,做互联网很管用噢!
当然管用了!
有个巫医发明了一种治疗方法。他坦言,我这个方法对付癌症可能不太行,但对付感冒很管用噢!你不信,找个感冒患者来!
感冒患者找来了,医生让患者躺在一张绘有八卦图案的方桌上,然后绕着患者绕了八八六十四圈(看到没,他也是有一套方法的!),然后对患者说,回去该吃吃该喝喝,五天之内就好了!
果然,患者好了。
医生四处宣传他的治疗方法,由于此方法简单易学,迅速收获了大批粉丝。
图8-15 电影《破坏之王》截图
给软件开发人员一段文字描述,让他提炼和表达其中的领域概念和关系(通过ER图、类图……甚至口述表达都可以)。基于我在训练班上的体会,能在这个测试中给出合格结果的开发人员占全体开发人员的比例,如果在2000年占百分之x的话,二十年之后的2020年,这个比例是否能占到千分之x都值得怀疑。
随着互联网的成熟,大部分组织都变成了“互联网组织”。以往以“互联网公司”著称的巨头们变成了行业领袖,宣称“我是做互联网的”已经不足以包装自己,必须要对领域深入挖掘了。
但是,开发人员“敏捷”惯了,怎么办呢?还能回得去吗?
图8-16 分析技能下降之后,还能回得去吗?
8.1.8 伪创新
于是,就出现了各种伪创新。
有的人(国内国外都有)没有掌握相应技能,也不愿意认真学习已有的知识,凭着一些朦胧的“领悟”,就“发明”了一些“新”方法,这就是伪创新。
软件开发的一些伪创新前些年打的是“敏捷”的旗号,最近几年打的是“领域驱动设计”的旗号。仔细观察,背后推动的人很多是重叠的。
伪创新,例一:
图8-17摘自2017年出版的某本名字中带有“Domain-Driven Design”的书,看起来有点像图8-10,对吧?但是图8-17的内容和绘制于1979年的图8-10比起来,水分要多得多。
图8-17 来自2017年出版的某本名字中带有“Domain-Driven Design”的书的截图
这么大一张图,除了Place Order和Ship Order这两个概念之外,剩下的就是废话刷工作量了。右半边,ShipOrder、Ship-Order、Shipping(咦?怎么没有和左边一样前面加个Order叫Order-Shipping呢,这样还可以多几个字母刷工作量)、OrderShipped,这是在上英语语法课吗?
相当于把2个概念刷了4倍工作量,得到2×4=8个结果。
信息浓度=2/8×100%=25%。
从图8-17的表示规律可以看出来,方框是workflow,进入箭头是command,出去箭头是event。既然如此,每个标签文字后面其实可以不用加“workflow”、“command”、“event”等字样,但是,不加怎么显得我工作量大呢?
考虑到这一点,信息浓度估计20%吧。或者反过来说,一条信息刷到5倍。
图8-17还有一个问题,混合了非核心域的知识,会造成之前说的a×b×c,具体在什么地方,留给读者观察。
伪创新,例二:
图8-18摘自2019年出版的另外一本名字中带有“Domain-Driven Design”的书。展示的就是打着“领域驱动设计”旗号的伪创新之一:事件风暴(EventStorming)。
图8-18 来自2019年出版的某本名字中带有“Domain-Driven Design”的书的截图
我把图8-18里提到的概念提炼出来,画了1个类和4个小人,如图8-19。数一数,包括类名称在内,图8-19一共有16个概念。
8-19 我提炼图8-18的概念画的图
有了图8-19,可以准备开车……不,准备刷工作量了!
(1)创建对象,销毁对象,刷2个蓝色纸片,就是图8-18中的Create an ad和Remove ad(怎么前面没有an了,刷得不整齐,重刷!)。
这个步骤刷出2个蓝色纸片。
(2)多重性为1的属性(从图8-18看应该是title、text和sell price),每个刷1个蓝色纸片,就是图8-18中的Change the ad title(怎么不和后面两个一致都用Update,刷得不整齐,重刷!)、Update the ad text和Update ad sell price(怎么前面没有the了,刷得不整齐,重刷!)。
这个步骤刷出3×1=3个蓝色纸片。
另外,本来这些都是Ad的属性,直接称title、text和sell price即可,不必再加前缀,但不加怎么能刷出工作量呢?一定要加!
(3)多重性为多的属性(从图8-18看应该是picture和category),每个刷2个蓝色纸片,Add***和Remove***。
这个步骤刷出2×2=4个蓝色纸片。
另外,本来这些都是Ad的属性,图8-18中写Add***和Remove***即可,不用加to Ad、from Ad,但不加怎么能刷出工作量呢?一定要加!
(4)每个操作刷1个蓝色纸片。
这个步骤刷出6×1=6个蓝色纸片。
咦?这些改变状态的操作看着怎么和属性没有什么关系呢?是属性漏了,还是操作错了?估计作者也不太了解操作、状态和属性的关系吧。
(5)把(1)-(4)步产出的所有的蓝色纸片变换词序,每个旁边加一个橙色纸片。
这个步骤结束后,得到(2+3+4+6)×2=30个方框形状的纸片,或者说,15对。
(6)把图8-18中的4个小人和每对方框任意组合,得到大约15个黄色小人。
最终,我们从往墙上贴了30+15=45张小纸片,数量和内容正好和图8-18中的纸片相同。
信息浓度=16/45×100%=36%。再考虑到那些刷上去的Ad,估计差不多33%的浓度。或者反过来说,一条信息刷到3倍。
要注意,这仅仅是其中一个类Ad,其他类照此办理,今年的工作量可算是有交代了。
是不是创始人英明神武,只不过其他人把经念歪了?感兴趣者可以去自行看“事件风暴”的作者Alberto Brandolini的书,看看书里面讲了什么。
这些伪创新在思想上都有共同的错误:一一对应,内外不分。
世界之所以复杂,或者说,系统之所以复杂,就是因为很多关系不是一一对应的。
组织的一个流程可能由多个系统(包括人肉系统和非人系统)协作完成,一个系统可以参与组织的多个流程;系统的一个用例(如果读者没有掌握上册讲解的用例的知识,就当作是功能吧)可能由系统的多个类协作完成,一个类可以参与系统的多个用例。类的一个操作可能会影响多个属性,一个属性可能会被多个操作影响……
正是因为如此,才需要软件开发人员大脑来找出最佳的映射方案,这才是人的脑力需要花费的地方。
而这种思考是有一定门槛的,不是所有人都能胜任。
如果一个人不能胜任,而又不愿意花时间去学习,当有一种“一一对应刷工作量”的伪创新出现在他面前时,自然而然就会产生一种虚幻的“受用”感觉,欢快地投入伪创新的怀抱——
“哥也是有方法的人了!”
最近几年的微服务浪潮中,类似“以用例为依据分割系统”甚至“以业务流程为依据分割系统”等明显内外不分的言论为什么能流传开,就是因为足够简单,方便一一对应刷工作量。
***********
初中数学里要学习全等三角形、相似三角形、SSS、SAS……,到了高中以后学了正弦定理、余弦定理等解三角形的知识……就不会再回去用初中的方法解题了。
但是,不是所有人都能学会高中的知识,比如说张三。
张三可能会这样解释:
“我这个人能力比较弱,只能掌握全等三角形、相似三角形的方法。”
这样的说法没有问题。
张三还可能会这样解释:
“这个题目比较简单,用全等三角形、相似三角形的方法做足够了,而且这样更方便广大人民群众理解。”
这样的说法也可以。不过,竞争对手不是傻子,市场中哪里有什么"简单题目"!能带来利润的题目都很复杂。
但是,张三如果这样说:
“全等三角形、相似三角形的知识比高中三角函数的知识更深刻。”
这就是自欺欺人了。
更要警惕的是,有一个李四,也许和张三一样没有掌握高中方法,也许掌握了高中方法但是为了忽悠张三们,偷偷把"全等三角形"改名为"叠合三角形",然后和张三宣传:
“我发明了"叠合三角形"新方法,比高中的三角函数有用,三角函数过时了。”
这就是可恶了。
***********
回到前面举的伪创新例子。如果说熟练掌握类图、状态机图等建模技能,并发现了其中的缺点,站在前人的肩膀上创新,这个要举双手双脚支持。
在不了解已有知识的情况下,拍脑袋搞出伪创新,甚至向大众宣传伪创新,也是个人自由。
但是,宣传伪创新时,像上面李四那样胡说“我这个方法比***好”,就不对了。
事实上,一旦付出努力,咬咬牙掌握了更严谨和更高效的方法,是羞于再回头去使用那些打着“敏捷”或“领域驱动设计”旗号的伪创新的。
8.1.8 本书使用的分析方法
分析模型描述系统要封装的核心域知识。
至于用什么建模概念来思考和描述核心域知识,可以有很多种选择。例如,“人”用不同的建模概念描述,可以说它是一个“类”,也可以说它是一个“类型”、一个“实体”。
本书使用面向对象的建模概念来描述分析模型,从三个视角来描述:
分析类模型:描述系统中各个类以及类之间的关系。
分析状态机模型:描述某个类的各个行为的逻辑。
分析交互模型:描述某些类在实现某个用例时的协作。
而面向对象的分析模型的表达形式,也可以有多种选择:语音、文本、图形等。
有的人觉得当前许多编程语言的表达能力已经很强,认为用文本已经足够,抗拒用图形来表达领域知识。
但是,和只有自上而下顺序的文本相比,能够朝四个方向扩展的平面图形(如果有三维模型就更好了)更容易让建模人员看出领域概念之间的联系。例如,图8-20和8-21的内容,如果没有图形的帮助,直接用文本一行一行地构造分析模型,人脑的负担非常重。
图8-20 餐饮领域的类图
图8-21 来自 Practical UML Statecharts in C/C++(Samek M, 2008)的截图,计算器的状态机图
说到这里,又不可避免地要提醒,故意选择文本的形式来表达领域知识,有可能也是一种遮羞利器。图8-20和8-21的内容如果用文本表达,可能会得到很多页文本——这就有了理由:因为工作量太大了,所以很多地方无法做深入的思考,可以原谅!
本书使用类图、状态机图和序列图三种UML图形来表达面向对象的分析模型。UML类图表达分析类模型,UML状态机图表达分析状态机模型,UML序列图表达分析交互模型。
图8-22 本书的分析方法所使用的UML图形
需要说明的是,虽然我们用的是面向对象的分析方法,也就是说,用面向对象的概念来剖析核心域知识,但不意味着你的系统一定要用特定的“面向对象”编程语言、特定的存储方式或物理分布形式来实现。
也许你使用的编程语言是面向过程语言,例如C;也许你使用的编程语言是函数式语言,例如F#;也许你使用的存储系统是关系数据库系统,例如SQL Server;也许你使用的存储系统是非关系数据库系统,例如MongoDB;也许你的系统运行在同一台机器上,也许是分布在很多台机器上……
不管你的系统的实现方式和运行形态如何,从分析过渡到设计时,变化的只是分析到设计的映射套路。如果设计所使用的非核心域比较“面向对象”,那么映射套路会比较直观一些,否则,就需要一定的转换。但无论如何,如前文所说,这个套路和具体的核心域知识没有关系,我们并不需要针对每一个核心域概念逐一花费脑力去思考它。
我们之所以选择在分析工作流使用面向对象的分析方法,是因为从思考深度和表示的严谨程度来看,面向对象的分析方法以及UML表示法目前仍然是剖析和整理领域逻辑的最佳选择。
1. [多选]关于分析和设计的区别,以下说法不恰当的有:
A) 分析着眼于“系统做什么”,设计着眼于“系统怎么做”。
B) 分析和设计分离的好处是,先全局思考整个系统的各个类以及类之间的关系,再有规律地映射到实现的平台和语言,这样就减少了反复试错的成本。
C) 有时候,在分析工作流也会考虑内存、网络带宽等概念。
D) 分析注重业务,设计注重技术。
2. [单选]掌握MVC、MVP、MVVM、六边形、洋葱型……等模式或架构,并不能解决分析的问题,原因是:
A) 它们描述的是域之间的协作。
B) 它们没有得到广泛使用。
C) 它们没有体现面向对象的思想。
D) 它们不够敏捷。
3. [单选]以下(1)-(4)所展示内容的共同点是:
(1)
(2)
(3)
(4)
产品愿景:为了满足内外部人员,他们的在线订餐、自动订餐统计和外部人员管理的需求,建设这个在线订餐系统,它是一个在线订餐平台,可以自动订餐统计。它可以同时支持内外网订餐,同时管理内外部人员订餐和定期订餐分析。
A) 都是UML模型
B) 都有废话内容
C) 都涉及到电子商务领域
D) 都体现了面向对象建模的思想
4. [单选]第一款CASE(计算机辅助软件工程)工具是:
A) Rose
B) ERwin
C) FlowChart/360
D) DesignAid
5. [单选]“领域驱动设计”话语体系中有“限界上下文”的概念(类似于UML的组件)。有的人提出以功能(或用例)为依据划分上下文,有的人甚至提出以业务流程为依据划分上下文。这些做法的主要问题是:
A) 没有区分核心域和非核心域
B) 内外不分
C) 没有使用UML的标准符号
D) 不够敏捷
6. [多选]以下说法正确的有:
A) 用核心域术语表达的内容就是分析模型。
B) 分析模型概要地描述核心域知识,设计模型将核心域知识细化。
C) 面向对象的分析模型不妨碍使用面向过程的设计。
D) 口头表达也可以表达分析模型。
7. [单选]有一篇文章,作者在白板上画了一个类图,然后开始掰着指头数这个类图缺什么,"没考虑到持久化","没考虑到对象的创建"……然后得出结论:画这个类图不如直接编码。根据本节的知识,以下正确的说法是:
A) 作者不了解核心域和非核心域分离的重要。
B) 别急,这个图会越来越细,逐渐添加作者认为缺少的那些东西。
C) Talk is cheap. Show me the code.
D) 敏捷是建模的精髓,加上这些就不敏捷了。
8. [多选]有开发人员说“现在开发一个应用真容易,咔咔咔几下,绝大部分工作框架都帮你弄好了,只要添加一些业务代码就可以了”,从本节的知识点评价这段话,以下说法正确的有:
A) 这段话的背后是一种无利的思维。
B) 这个说法太片面,有的业务是比较难的。
C) 非核心域的复用竞争对手也容易获得。
D) 这正是敏捷开发的优势。
9. [多选]以下描述的系统中,适合用面向对象的分析方法做领域建模的有:
A) 电磁轨道炮武器系统
B) 互联网拼单购物系统
C) PC单机角色扮演游戏
D) 电梯控制系统
8.2 建模步骤3-1 识别类和属性
8.2.1 面向对象的假设
当使用面向对象的方法来分析系统时,我们假设系统由"对象"这样一种东西构成,对象封装了数据和行为。
在分析工作流,我们认为系统中的对象在一个虚的"对象空间"中运行。这个空间不是内存,也不是硬盘,只是人脑中的一个逻辑空间,将它想象成宇宙空间也未尝不可。在"对象空间"中,速度不是问题,对象的创建和对象之间的通信都非常快。
图8-23 虚的"对象空间"
以下思考可以用来判断你思考的问题是分析问题还是设计问题。
我们可以针对分析模型里的元素,一个一个问“如果性能不成问题,速度无穷快,这个东西还有必要存在吗”,如果答案为否,那么从分析模型中把它删掉。
分析模型受到设计的污染,很容易导致批量的废话刷工作量,导致没有时间思考应该思考的问题(当然,这也可能是某些人乐意的)。
注意上文提到的"假设"二字。面向对象就是一个假设,如果不认可“系统由对象构成”,也可以分析系统的核心域逻辑,只不过用的方法不叫“面向对象方法”。
面向对象的思考方式比目前的其他思考方式要好一点,原因不是计算机喜欢面向对象或者面向对象更接近于计算机的底层,而是面向对象的思考方式更能帮助人脑去剖析复杂问题——估计计算机更"喜欢"人类用机器语言直接给它发指令,不用自己受累编译、链接。
正如前文提到的,三角函数更能解决复杂问题,不意味着它比全等三角形、相似三角形更容易掌握。面向对象更能帮助剖析复杂问题,不意味着面向对象的思考方式比其他的思考方式更容易掌握,而且随着你掌握了更强有力的思考工具,更复杂的问题就会扑面而来。这些问题之前已经存在,只是之前你没有能力来发现和对付它们——“古人很少死于癌症”。
在接受“面向对象”假设的前提下,我们接下来就要做第一步思考:系统由什么样的对象构成。
在这一步思考中,我们通过抽象思维把具有共同特征的对象集合归纳为"类",对象看作类的实例。
归类是人类认知的一种基本技能,其哲学讨论可以追溯到柏拉图的理型论(Theory of Forms)。
8.2.2 分析类
8.2.2.1 三种分析类
依照Ivar Jacoson在“Object-Oriented Software Engineering: A Use Case Driven Approach”(Jacobson 1992)中的思想,在分析工作流我们进一步假设系统中存在三种类:边界类(Boundary Class)、控制类(Control Class)和实体类(Entity Class)。
在模型中,我们可以用Ivar Jacoson建议的构造型(Stereotype)来表示三种分析类,如图8-24。
图8-24 三种分析类的构造型
一些UML工具(如Enterprise Architect、Visual Paradigm)已经内置了这些分析类构造型。如果使用的建模工具没有内置这些构造型,可以自己添加如“<<边界>>”等文字构造型;或者不用构造型区分,通过给类起名"某某接口","某某控制",也有助于了解该类在系统中扮演的角色。这一点,和第3章讲到业务工人、业务实体时的做法是一样的。
三种分析类只是一种逻辑上的思考方式,如果你乐意,可以换成另外的思考方式。在设计工作流,三种分析类可以映射到任何实现架构,包括但不限于MVC、MVP、MVVM、六边形、洋葱型……甚至映射到不做任何分割的“架构”。
图8-25展示了三种分析类的责任、和需求的关系以及命名。
图8-25 分析类的责任、和需求的关系以及命名。
图8-26用序列图展示了三种分析类之间的协作。
图8-26 三种分析类在系统中的协作
执行者先把消息发给边界类对象,边界类对象能履行的就履行,无法履行的责任,再发给控制类对象。控制类对象就像总裁办,不做具体工作,只是将责任分解后分配给实体类对象。
实体类可以按照它们之间的耦合程度组成若干组合(Composition),类似于公司的部门。组合之间还可以再组成更大的组合,类似于大部门中有小部门。也有可能有的类既不组合其他类,也不被其他类组合,类似于自成一个部门。
控制类对象发送消息时,先发给组合的整体对象,再由整体对象分配(可能还有分解)给组合内的其他对象,如果组合内的对象还组合了更小的对象,还可以继续分配。最后,由边界类对象反馈信息,完成一个交互回合。
以上说的“组合(Composition)”和DDD话语体系中的“聚合(Aggregate)”是类似的。面向对象的分析设计方法学早已有Aggregation、Composition的概念,Eric Evans在塑造DDD话语体系时借用了Aggregation一词,其实借用Composition更合适。关于这一点,本书在讲述类的关系时会详细说明。
8.2.2.2 关于边界类
边界类的责任是接受输入、提供输出以及做简单的过滤。
图8-25中提到边界类的映射方法——每个有接口的外系统映射一个边界类。这里说的"有接口的外系统"不仅包括系统执行者,还包括仅接受系统输出信息的外系统。
以《软件方法》上册案例中的"时间→发送公开课通知"用例为例。该用例进行过程中,系统会向软件开发人员发送公开课通知,同时还要向UMLChina助理反馈发送通知的进展。软件开发人员和UMLChina助理在这个用例中仅仅是接受输出,没有输入信息给系统,但系统可以分别设置一个边界类来封装向软件开发人员和UMLChina助理反馈信息的责任,如图8-27所示。
图8-27 外系统映射边界类
图8-27中,“时间”映射一个“时间接口”,“软件开发人员”映射一个“软件开发人员接口”,“助理”映射一个“助理接口”。
分析工作流的边界类不暗示任何实现方案。在总责任相等的前提下,它和实现的映射是多样的,可以用图形界面实现,也可以用非图形界面(包括文本、声音……)实现。
即使使用图形界面实现,也不能简单认为一个边界类对应一个窗体(Form)。一个边界类的责任可以拆解到多个窗体上,一个窗体也可以和多个外系统交互。如何组织这些责任,应该从外系统的角度来考虑,而不是从用例或实体类的角度来考虑。
图8-28中,“助理接口”边界类被圈住的几个责任来自不同用例的步骤,但在使用图形界面实现时,可以放在面向助理的、通知专用的同一个窗体中。
图8-28 边界类责任的组织
类似的例子还有:一份申请,需要通过系统审批三次,也就是三个不同的用例。在图形界面实现中,可能不需要准备三个窗体,部门主管、财务、副总三个审批人可以在同一窗体上工作,但部门主管、财务、副总各自有对应的分析边界类。
如果某个外系统和系统的交互很多,对应边界类的责任可能会有很多。另一种做法是按"外系统+用例"的组合映射边界类,这样可以减少一个边界类上的操作个数。不过,这样的做法已经暗示“按用例来划分边界”,所以还是建议尽量保持一个外系统一个边界类,如果操作很多,可以将从外系统角度观察可能要分在一组的操作移到一起,EA等工具可以随意定制属性和操作的上下显示顺序。
需要提醒的是,外系统映射的只是边界类,并不映射实体类。在外系统是人的时候,经常会有人犯这样的错误。例如以下用例规约片段:
1. 助理选择公开课,请求创建通知任务
2. 系统验证所选公开课适合创建通知任务
“助理”是执行者,映射一个边界类“助理接口”是可以的,但如果映射一个“助理”类,如图8-29,那就错了。
图8-29 外系统不映射实体类
系统是否需要一个“助理”类,要看系统是否需要维护助理的信息。如果需要,会在某个用例规约的某个地方体现,例如,可能会有一个步骤:
7. 系统保存通知任务
绑定一个字段列表:
7. 通知任务=4+创建时间+创建人
这个“创建人”就是助理,说明系统需要记住助理的信息,这时才会有“助理”类。
但并不是所有的系统都会这样。例如,乘客坐电梯上楼,乘客是电梯系统的执行者,但电梯系统可能不需要"乘客"实体类,因为它不需要记住乘客的信息。
当然,有朝一日,电梯升级为防疫电梯,用例规约里有:
4 乘客提供身份标识
5 系统验证身份标识合法
6 系统记录乘客信息和入厢时间
这时,电梯系统里就有"乘客"实体类了,因为系统要记住乘客的信息。
电梯系统虽然没有"乘客"类,但会有"乘客接口"类,可能的类图和常见的实现方式如图8-30。
图8-30 “乘客接口”及其常见的实现
8.2.2.3 关于控制类
控制类相当于用例在系统中的“代理”,它的责任是控制用例流,为实体类分配责任。
把每个用例直接映射一个控制类,可以用“用例名称+控制”来命名,后面的“控制”二字最好加上,因为可能还有记录行为细节的实体类,名称和用例的名称一样。例如,用例名为“审批”,控制类起名“审批控制”,如果需要记录审批的细节,还会有一个实体类“审批”。
如果在分配责任时发现控制类只起到传递的作用,没有起到分解和分配的作用,也可以把控制类去掉。
8.2.2.4 精力应该花在实体类上
边界类与外系统、控制类与用例的映射关系很明显,所以识别边界类和控制类不需要思考,直接按照上面的套路映射即可,甚至可以推迟到画分析序列图时再加上去。
有的分析方法学如ICONIX提倡一种Robustness Diagram,认为可以通过它来帮助寻找类。开发人员一用确实感觉很舒服,噼里啪啦就发现好多类,有一种"我已经取得了不小成绩"的错觉,不过要是仔细看看,就知道"发现"的多是边界类、控制类。这些类其实用不着刻意去发现,只要按照图8-25的套路映射即可。
最难的工作——寻找实体类以及它们之间的协作,Robustness Diagram却是寥寥带过,甚至容易误导建模人员把实体类和用例一一对应。所以,本书不推荐开发人员额外花时间画Robustness Diagram。
和前文多次提到的一样,凡是不需要思考就可以得到很多“成果”的“方法”,都容易成为懒人摸鱼的遮羞布。
建模人员的思考工作量应该花在识别实体类上。一个用例需要哪些实体类协作实现、如何协作,一个实体类会参与哪些用例的实现,这是一个多对多的映射,需要由建模人员的大脑决定哪种映射最好。
因此,本章以下内容提到的“类”,缺省意思为“实体类”。
8.2.3 提炼类和属性
8.2.3.1 从需求规约之外的素材提炼
建模类及类之间的关系,可能在软件开发的分析工作流之前就已经发生了。
第7章“需求启发”中就提到,我们在研究资料的时候,可以通过画类图来整理领域的概念。整理领域概念时,有时还可以加上状态机图(但不会使用序列图,自行思考一下为什么)。即使不是为了开发软件,也可以通过这些手段来整理领域知识,帮助我们更快掌握。
拿一篇上过春晚的经典科学论文《母猪的产后护理》为例,如图8-31。
图8-31 《母猪的产后护理》
通过类图整理图8-31的素材,如图8-32。
图8-32 用类图整理《母猪的产后护理》领域知识
图8-32可以称为“母猪的产后护理的领域模型”。这个模型可能和信息系统没有关系,因此不能称为“母猪的产后护理的核心域模型”或“母猪的产后护理的分析模型”。
如果某个信息系统“母猪产护宝”要封装图8-32的领域知识,那么图8-32可以称为“母猪产护宝的核心域模型”或“母猪产护宝的分析模型”,但不适合称为“母猪产护宝的领域模型”,因为“母猪产护宝”中封装了多个领域的知识。
对于以上提到的几个用语,本书按以下定义使用。
领域模型:描述某个领域中的概念及概念之间关系的模型。
分析模型:从核心域视角描述的软件系统的模型。
核心域模型:等同于分析模型。
它们之间的关系如图8-33。
图8-33 领域模型和分析模型
模型可以用各种表示法来表示,相应的图可以叫“领域类图”、“领域ER图”、“分析类图”、“分析状态机图”……。
我们可以从各种素材中提炼某个领域的类和属性,不过这些类不能叫分析类,只能叫领域类。到底哪些领域类会成为目标系统的分析类,需要从目标系统的需求模型(如果用本书上册的方法表达,就是系统用例规约)来判断该系统是否需要这个类。
没有需求规约,目标系统的边界以及应该承担的责任没有理清楚,那么,提炼得到的类到底是不是目标系统将来需要维护的概念,是没法判断的。也就是说,虽然在很多时间点、从很多素材都可以提炼类,让我们在确定分析类时,省去了许多思考,但分析模型的最终依据只能是需求模型。
当然,如果你脑子里对于系统该干什么不该干什么清清楚楚,只不过没写出来,那也算是有需求模型了——不过你得确定你真的懂,而不是拿这个当遮羞布。
如果按照本书所采用的面向对象建模、UML表示法和Ivar Jacoson三种类的分割,某个领域模型可能会包含某个系统的分析模型中的实体类部分,但不会包含边界类和控制类部分。
有些书籍和文章作者,对软件开发的工作流没有清晰的概念,把所有用“业务语言”表达的模型,包括组织流程,系统需求规约等,通通叫作“领域模型”。这是不正确的,我们在阅读时要注意分辨。
8.2.3.2 从需求规约提炼
阅读用例规约的基本路径、扩展路径、字段列表和业务规则部分(即所谓“功能需求”部分),针对表示名词或事件的词汇,逐个思考,这是不是系统要记住的核心域概念?如果是,那么它是类,还是某个类的属性?如图8-34。
图8-34 从用例规约识别类和属性
关于“系统要记住的”概念,可以这样思考,在执行者向系统发出某个请求之前,系统需要懂得哪些信息?
如果您有关系数据库建模的经验,也可以这样简单地思考:如果系统采用关系数据库来保存这些内容,那么数据库里应该会有哪些表?这样思考得到的表和实体类基本上是一一映射的。表对应类,列对应属性,行对应对象,关系对应关联。后面我们会讲到,类模型可以直接转换成关系数据库模型,不需要再花工作量再做一遍关系数据库建模。
如果关系数据库建模技能掌握得好,得到的数据模型符合1NF、2NF和3NF,那么用关系数据库建模的思考方式得到的类图极有可能也是合格的。反过来,也有理由怀疑,自诩关系数据库建模能力强的人,如果类建模做得乱七八糟,估计也有吹牛的成分。
当然,我们画类图的目的不仅是为了得到关系数据库,面向对象和关系数据库(或任何的具体存储方式)也没有必然的绑定关系。任何系统都可以用面向对象的方式来构造,不管它用什么方式来存储对象。
以电梯为例。我们把为乘客提供电梯服务所需要的所有软硬部件看作一个整体,称为“电梯系统”。乘客在发出某次召唤之前,电梯系统要懂得各部电梯当前运行方向、当前所处楼层、待去往的目标楼层集合以及楼层的结构,才能够合理应对乘客的召唤。这些可以看作系统要记住的信息。乘客姓名、乘客召唤电梯的时间、电梯曾经去过的楼层等,系统不需要知道。画出类图如图8-35。
图8-35 电梯系统可能需要的实体类
这些信息不一定需要被“持久存储”。图8-35中的概念中,当前楼层、目标楼层、方向的信息一直在不断变化,甚至在电梯系统停止服务时会被清空,但不影响图8-35的类结构。
8.2.4 类和属性的命名
8.2.4.1 使用精确的领域术语命名领域模型中的元素
领域模型中各个元素的名称都应该来自该领域的术语体系。
一个领域之所以能作为“领域”为人认知,必定会在发展过程中沉淀出一套日益完善的、精确的术语体系。每个术语有其独特的、其他术语不能替代的含义。例如物理学中的质量、重量、重力、引力、衰变、裂变、聚变……,各自有各自的含义,不是另一个概念可以取代的。
涉众常使用的词汇不一定适合用来命名领域模型中的元素。
也许是出于自己的领域知识局限,或者出于字数少使用方便,涉众有时更习惯于使用一些不严谨、感性的称呼。
这些称呼经常会根据颜色、大小等感性认识来起名。例如,货车司机可能会把一张单子称为“绿单”,因为单子的颜色是绿色的,但更精确的名称可能是“送货单”;购物时的“小票”,名称来源于面积较小(和更大的“发票”比较),更精确的名称可能是“收据”。
这些称呼所依赖的信息,稳定性往往比真正的领域内涵要差。有关机构可以改变送货单的颜色,购物收据也可以变成面积比发票要大的大长条,“绿单”、“小票”等称呼就得变化了。即使已经形成了习惯不得不一直沿用下去,真实的情况和字面的意思已经大相径庭。
涉众喜欢用不严谨的称呼,这是正常的。某类涉众的领域知识可能会很片面,对领域概念认识不深刻,怎么能寄望一个使用探探来交友的屌丝青年清楚社交六度空间理论呢?正如第7章所说,涉众关注的是涉众利益,不能指望涉众提供需求,更何况是分析了。精确使用领域术语是建模人员的责任,不是涉众的责任。
当然,在和涉众交互的界面上,依然可以针对不同的涉众使用涉众习惯的不严谨称呼。
涉众有很多种,不同类型的涉众可能对同一概念使用的称呼不一样,例如有的叫"宝贝",有的叫"商品"。如果怀疑两个称呼描述的是同一个概念,可以这样问:有没有不是商品的宝贝?有没有不是宝贝的商品?如果回答都是否,就清除掉其中一个,否则,应该继续研究两个称呼背后的真正含义,必要时在模型中表达其中差别。
如图8-36,左侧的几个概念中,经过审查,认为"宝贝"和"商品"、"顾客"和"客户"含义相同,去除其中一个;"用户"、"顾客"、"会员"含义有差别,在类图上更精细地表达。
图8-36 清理冗余的称呼
8.2.4.2 不胡乱发明“新”术语
建模人员要尊重并认真学习领域的已有知识,不要胡乱搞术语“创新”。
互联网是术语“创新”的重灾区。赋能、抓手、对齐、拉通、打法、链路……这些胡乱创新的“术语”,难道之前没有合适的词汇来描述吗?非也,有心人就是要通过这样的“创新”,割裂和已有知识的联系——我是“新”的,不受已有知识的约束,所以我不用花时间学习已有知识,随意胡说就行了。
上个世纪末,互联网的创业者们纷纷宣称自己是“新经济”,意思是以往的经济规律对我没用。
Shapiro和Varian在“Information Rules: A Strategic Guide to the Network Economy(信息规则:网络经济的策略指导)”一书中说:
we kept hearing that we are living in a "New Economy." The implication was that a "New Economics" was needed as well, a new set of principles to guide business strategy and public policy. But wait, we said, have you read the literature on differential pricing, bundling, signaling, licensing, lock-in, or network economics?
我们一直在听人说,我们生活在“新经济”中。言下之意是需要一门“新经济学”,一套指导商业策略和公共政策的新原则。但我们说,等一下,你读过关于差别定价、捆绑、信号、许可、锁定或网络经济学的文献吗?
在软件开发领域,“敏捷”和“领域驱动设计”圈子也有类似现象。
8.2.4.3 不要把“限界上下文”当作偷懒的遮羞布
“领域驱动设计”乱象中,和本节内容相关的一个现象是:把“限界上下文”当作遮羞布,闭门造车炮制“通用语言”。
在不同上下文中使用同一称呼,而且有不同含义,这种情况当然是存在的,但应该在调查和思考之后确认。要提防因为没有能力或者懒得去剖析背后的区别,就随便乱说——我的上下文中,鹿就是马的意思,马就是鹿的意思,我的上下文里的商品和你的上下文里的商品不同……
例如,针对下面这段描述:
同样规格的联想ThinkPad X1 Carbon 2021笔记本电脑,在淘宝的A店铺卖9999元,B店铺卖9888元,某单位从B店铺买了20台,贴上单位的条码编号,分配给员工使用。
有的人“哇,我发现(没错,他们不喜欢研读,喜欢'发现')商品在不同的上下文有不同的含义!”,活生生地把领域建模搞成玄学。
其实,好好思考一下,或者看看前人的归纳,就知道这里面涉及到不同的概念,在模型中如实描述即可,如图8-37,不需要故弄玄虚。
图8-37 如实描述领域概念
图8-37描述的领域概念,与是否开发信息系统无关,与信息系统是所谓“单体架构”还是“微服务架构”也无关。
*用了“所谓”一词,意思是这种buzzword并不严谨。
如果在后面出于某种所谓“架构”的考虑,将图8-37的知识分割成两部分来部署,例如,店铺和商品规格一起,另外三个一起,领域知识并不会发生变化。而如何分割更合理,需要结合类模型、以及后面要讲述的状态机模型来判断。
在非必要和证据不足的情况下,生造“上下文”来分割模型,会造成模型中存在各种冗余信息,导致总体复杂度增加,甚至会陷入一一对应的功能分解。
Martin Fowler在《企业应用架构模式》中说了他的“分布对象设计第一定律”:不要分布对象。
Hence, we get to my First Law of Distributed Object Design: Don’t distribute your objects!
如果一定要分割模型,更合理的做法应该是:
按照愿景排序用例,一个个做增量开发,在分析和设计的过程中,模型不断成长。到了由于种种原因不得不分割模型的地步,再根据当前模型已整理出的领域逻辑来分割。
现实中很多做法是,在非必要和证据不足的情况下胡乱分割,甚至在模型内容为0的时候就闭门造车“上下文”。
目前我看到的各种书籍和文章中过度鼓吹“限界上下文”的内容,从作者所给的模型来看,绝大多数作者都不具备基本的建模能力,只会做拍脑袋的功能分解。
具备了基本的建模能力之后,对这些遮羞布多半是看不上的。这个基本的建模能力也不用太高,甚至也不用什么类建模,能做出15个表以上符合范式的关系数据库模型就够了。
和之前多次提到的情形类似:这也是很多开发人员喜欢去“拥抱”的——因为这样一来,事情看起来变得“容易了”,而且不用做艰苦的思考,而且用的方法还很有名气(某些造词工厂的造词能力和宣传能力可不是一般的强)!这样一来,哥也是有方法的人了,心理上获得了满足。
因此,就有了不具备基本的建模能力的人“炮制”一些“简单”的方法,去迎合不具备基本的建模能力的人的心理需要。
8.2.4.4 核心域透镜
在为了软件开发而建模时,建模人员可能会用自己熟悉的非核心域术语体系来代替不那么熟悉的核心域术语体系,还引以为豪。例如,面对一段集装箱领域装箱规则的描述,建模人员立即在大脑中把它转换成自己熟悉的概念:栈、链表、树……而且认为这是“透过现象看本质”,甚至宣称“我就是程序,程序就是我”!
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-38所示的“核心域透镜”的思考方式:如果从核心域的视角去看这个概念,或者说把这个概念映射到核心域,我们应该得到什么概念?
图8-38 用核心域透镜映射各种概念
例如,以“学习和考试”作为核心域,经过透镜前后的概念对比如图8-39。
原描述 | 映射后的核心域概念 | 原描述过去变体 | 原描述将来变体(猜想) |
PowerPoint | 演示工具 | 黑板、玻璃幻灯片、赛璐珞幻灯片 | 全息 |
检查IP地址 | 检查重复听课学生 | 看脸、看签名 | 检查另一种协议地址 检查大脑芯片标识 |
点击“开始”按钮 | 开始考试 | 观察到考生开始书写 | ? |
向数据库“答题”表添加一条答题记录 | 答题 | 在答卷上涂黑一格 | ? |
图
图8-39 经过透镜前后的概念对比
8.2.4.5 命名中不带冗余内容
如果把模型元素的命名中的某个部分删去,不影响建模人员对该模型元素的认识,那么这个部分没有必要存在。
如图8-40中,在“人员”后加一个“类”字,实际上是把两个不同领域的知识叠加在一起。
图8-40 删除命名中的冗余部分
知识一:人员是一个类。
知识二:用UML表示法的图形表示,类是一个方框。
知识一和知识二是正交的。在对知识二已经有了共识的情况下,再加上一个“类”字是冗余的。
关于类和属性的命名,常犯的冗余错误有:
(1)在类名的最后加"类"字;
(2)在类名的前面加"Class"或"C";
(3)在类名的最后加"情况"、"信息"、"记录"、"数据"、"表"、"库"、"单"等;
(4)在属性名前加类名
如图8-41,按"类的属性"念出来,"人员的姓名"很好,"人员的人员姓名"就冗余了。
图8-41 属性名称前不需要加类名
(5)给类加上ID或标识属性
对象都有标识,不需要为每个类加一个ID或标识属性。如何表达对象的标识,这是另一个领域的知识,其实现规律和当前所研究领域的知识无关(除非当前所研究领域就是“如何实现对象标识”的领域)。
此处说的标识是为了区分对象而添加的标识,在设计工作流中可以以整数、GUID等各种方式实现。它应该不带任何领域知识,因为一旦带有领域知识,就意味着一旦领域知识发生变化,它也要跟着变化。
可能有人会想到DDD话语中的“值对象不需要标识,后面会跟进一步说明。
在类的对象集合内唯一的、带有领域知识的“编码”属性,如订单的订单号,人员的身份证号等,则可以像其他属性一样,出现在领域模型中,需要的时候它的值可以变化。
在设计工作流时,直接把某个“编码”属性作为标识,也不是不可以,但是就要面临上面提到的风险。
本书作者的身份证号,最开始是15位的“3401**74******1”,1999年升位,变成了18位的“3401**1974******19”。身份证号中暗示了许多知识:3401**→安徽省合肥市蜀山区,1974****→1974年**月**日生,**1→当日出生的男性顺序号,9→校验码。这些信息发生变化时,原有的暗示就失效了。
对象标识仅在内部用于表示对象,不需要在人机交互的界面上出现,也就是说,不需要人类执行者输入对象标识,也不需要向人类执行者展示对象标识。如果需要,在人机交互的界面上出现的应该是“编号”属性。
*********
如果犯了以上所列举的错误,相当于强行把正交的知识叠加在一起,往往就会得到“废话刷工作量”的结果——记住这个要诀,如果有一天读者想要故意“废话刷工作量”,就把它用上。
举个“废话刷工作量”的例子以加深印象,如图8-42,如何“废话刷工作量”?
图8-42 无冗余的类图,等待“废话刷工作量”
按照上面提到的冗余错误,一个个用上去,可以得到“废话刷工作量”的步骤如下:
(1)因为方框上部是类名,下部是属性,连线是关联,所以,给类和关联的名称加上“类”、“属性”、“关联”。
(2)可以看出,这些类是领域类或分析类,所以,把“领域”加到类名称后面。
(3)因为这些类都代表了一些信息,所以,把“信息”加到类和属性名中。
(4)因为类的属性是“类的”属性,所以,给属性前面加上类的名称。
(5)因为关联也是属性的一种,只不过类型是另一个类,所以,每个类为关联加一个属性。
(6)因为每个对象都有标识,每个对象都有状态,所以,给每个类加上ID和状态属性。
(关联、状态等相关知识,后文讲述。)
演变过程如图8-43。
图8-43 废话刷工作量的过程
类图上的字从最开始图8-43顶部图形的28字增加到底部图形的135字,但没有增加任何有价值的信息。
********************
“信息”、“数据”等也不是不可以作为类的名称。如果系统关注的焦点是"信息处理",处理的信息是什么内容无所谓,"信息"、"数据"也可以作为类的名称,但这个时候就不再有"人员"了,它们不在一个抽象级别。类图可能如图8-44。
图8-44 "信息"作为一个类的名称
另外,如果一个称呼在某个领域中已经存在很久,成为了该领域的术语,即使它看起来犯了以上的冗余错误,用来作为模型元素的命名也无妨。例如"订单"带有"单"字,实际上描述的是一次"购买"或“交易”,不过"订单"已经在领域中广泛使用,而且把“单”去掉留下“订”也不合适,所以“订单”可以作为类名。
但是,很多带有“单”字的称呼是可以去掉“单”字的,例如“申请单”、“入库单”等,类名保留“申请”、“入库”即可(啥?你看到的各种纸质材料上都有“单”字?后文还会说这个问题)。
8.2.4.6 关于命名所用的语言
这里说的不是编程语言,而是中文、英文、日文……
给核心域元素命名,使用的语言应该首先考虑精确体现核心域内涵和方便开发团队思考和交流核心域知识。该用中文就用中文,该用英文就用英文,该用日文就用日文。
以前经常会考虑转换到编程语言时需要改名的问题。在设计工作流,如果我们使用的编程语言只能用英文命名类、属性、操作等——更严谨的说法应该是编译器广泛支持的字符集比较小,那么还需要一个对编程语言合法的名字。
建模工具例如EA,一般会提供别名(Alias),真实名称用编译器支持的字符集,再加一个别名用于显示。
随着时代的发展,编译器、DBMS等支持的字符集越来越大,上面提到的问题慢慢不再是问题。如果团队成员更熟悉中文,那么在代码中使用中文命名即可,省去蹩脚英语或者模糊的汉语拼音缩写(啥?你说“可以加注释嘛”?)给后续工作带来的麻烦。
其他如在文本编辑器中输标点符号时切换输入法太麻烦、版式不好看等问题,在图形建模时影响很小。
以上所说仅是针对目标系统的核心域元素的命名,不涉及非核心域部分。非核心域部分内容,和计算机和软件领域有关。这方面的“外语”,不管是计算机和软件各个概念,还是编程语言的关键字,要求软件人员熟练掌握是理所应当的。
例如,混元形意太极掌门马老师找我们为他的门派搞一套“修炼辅助系统”。作为软件开发人员,能认真去体会马老师说的“化劲”、“武德”、“真气”、“掌门”之类的概念就已经不易,还要把它们变成英语就更难为人了,估计马老师自己也不知道。
但是,作为软件开发人员,不知道什么叫DTO,什么叫Iterator就不应该了,因此,命名为“武德DTO”之类是没有问题的。
如果目标系统是面向国际的,那怎么办呢?还是前面说的,首先考虑精确体现核心域内涵和方便开发团队思考和交流核心域知识。该用中文就用中文,该用英文就用英文,该用日文就用日文。
8.2.4.7 类命名用单数
类的名称已经是一个抽象概念,既代表属于这个类的所有对象的集合,也指代集合中的任何一个对象。
例如,“人”这个概念代表所有符合“人”的特征的对象的集合,同时我们也可以说“人有姓名、身高”,把“人”当成“任何一个个体”来使用的。这两种用法,分别对应于后文要讲解的泛化和关联关系。
如果用复数表达,例如中文“人们”,英文“people”,第二种用法就很别扭了,实例“某个人们”是什么?
图8-45 类命名用单数
如果“某个人们”另有含义,那么应该有另外一个类。例如,社区团购系统中,“某个顾客们”另有含义“团”,那么应该添加一个类“团”。
有一些常见的开发习惯,如数据库表名用复数,甚至有的框架在类转换表时,直接就在名称后面加上s,理由是表里有很多行。其实"类"、"表"的概念已经隐含了"多个对象"、"多行"的意思,不用再加了。而且,如果英语不熟,还得费心思去想正确的复数形式,何必呢?
8.2.5 审查类和属性
8.2.5.1 属性是否直接描述类
类和属性连在一起说"类的属性",应该能直接说得通,否则类和属性的搭配是不合适的。这个时候应该找到或建立合适的类,把该属性移进去。“属性要直接描述类”这个要求和关系数据库的第三范式“任何非主属性不依赖于其它非主属性”相似。
例如图8-46,“联系人的组织名称”中间隔了个“组织”,"类的属性"不能直接说得通,需要添加一个“组织”类,把“名称”挪过去。
图8-46 属性要能直接描述类
如果确定每个联系人只就职于一个组织,而且系统只关注组织的名称,可以将“名称”合并到“联系人”成为一个属性“组织名称”,如图8-47。不过,如果以上的假设发生变化,这样的做法应变成本很高。
图8-47 特定条件下可以简化
或者可以这样归纳:如果A和B的关联上B的多重性为1,而且B只有一个属性,那么可以把B合并进A,如图8-48。
图8-48 特定条件下合并类
要特别说明的是,习惯于关系数据库建模的建模人员有时会犯这样的错误,在一个类里放上另外一个类的属性作为“外键”。比如针对上面的例子,建模人员会想:“联系人”里放“组织名称”确实不合适,但是放个“组织编码”作为外键总可以吧?其实也不可以。"组织编码"是“组织”的属性,是封装在“组织”中的秘密,“联系人”不应该拥有“组织”的任何属性,它通过关联拥有“组织”对象,通过访问“组织”对象公开的操作间接访问“组织”的属性。
图8-49 不需要“编码”作为“外键”
“联系人”里放“组织编码”不合适,放一个无意义的标识“组织ID”呢?同样也不可以。因为这个“组织ID”是“组织”的标识,前文已经说了,标识属性此时不需要存在,所以“组织ID”在“组织”里不存在,更不要说放到其他类中作为“外键”了。
图8-50 不需要“ID”作为“外键”
在设计工作流,需要把类图映射到关系数据库时,确实需要把"组织"表的主键(可能是"编码"也可能是生成的代理主键)放在"联系人"表中作为外键,但正如上文所说,这同样是另一个领域的知识,而且映射规律和核心域知识无关。
缺少抽象能力的建模人员,经常会直接把手上素材的信息,一一对应地映射为类和属性,导致本属于多个类的信息被合并在一个类中,这是违反本小节要点的一个主要来源。
例如,建模人员对照着一张货物运输托运单,直接把它映射成类,照抄上面的每一栏作为属性,如图8-51。
图8-51 错误:直接把素材照搬成类
建模人员有时还觉得挺符合“类的属性”的。“货物运输托运单的货物名称”,没错啊,手边这张货物运输托运单上确实明晃晃地印有“货物名称”四个大字嘛。
托运单、出库单、销售单等各种单据,以及身份证、工作证、图书卡、设备卡等各种卡片和证件,在信息时代之前就已经存在了。它们相当于某种存储结构,存储一个或多个概念的信息。
现在,既然用软件系统取代了这些单据、卡片和证件,那么要建模的实体类应该是它们所代表的概念,而不是单据、卡片和证件本身。
也就是说,“货物运输托运单的货物名称”应该是“货物运输托运单所记录的货物的名称”。
8.2.5.2 属性是否可以从其他地方推导
如果一个属性可以从其他地方推导出来,那么这个属性就是冗余的,可以删掉。
如图8-52,人的出生日期和年龄属性。年龄可以从出生日期计算得到,应该把年龄删掉。
图8-52 年龄可以从出生日期推导
这个“其他地方”也可以是所关联的类的属性。如图8-53,订单的总金额可以由各个订单项的金额合计得到,那么可以考虑把总金额删掉。
图8-53 总金额可以从各订单项金额合计
同样,在这个地方,经常会有人以“性能”作为遮羞布拒绝思考和废话刷工作量。
如果提出性能问题的这个人知道如何在不考虑性能的情况下得到一个清晰、无冗余的模型,那么他还有资格说说性能问题。
不过,如果他知道如何得到一个清晰、无冗余的模型,说明他具备了一定的建模能力,往往也不会在分析的时候担心性能问题,因为他知道,如果碰到性能问题,可以按照某些套路添加冗余,这些套路和目前所思考的领域知识没有关系,没有必要混杂进来。
在分析工作流不断提出性能问题的人,您可以尝试让他整理一下领域逻辑,也去看看他之前写的代码,大概率会发现其中逻辑的组织是很糟糕的。
这是人的本性中急于回到自己的舒适区的一种表现,不只是在分析工作流如此,其他工作流也会有类似现象。
例如,开发人员没有掌握需求技能,也不愿意学习,即使给他时间去做需求,他也不知道应该怎么做。只好随便找人问问,开个会,走走过场,然后就着急回到编码的舒适区,甚至在讨论需求的时候都已经在不由自主地思考实现小细节。
为什么“现场客户”之类的东西会吸引开发人员,就是迎合人的这些本性,让开发人员可以安然坐在电脑前面,呆在自己的舒适区里。
呆在舒适区并不一定是错的。
如果出于个人爱好和天赋,专精某方面的技能,在某个领域深入挖掘,解决该领域各种难题,这是非常好的选择。不注意自己的爱好和天赋,什么都想学习,很可能什么都浅尝即止,以“学习新知识”来逃避碰到的各种难题。
错的是自欺欺人。
当形势需要人走出舒适区来应对困难的时候,如果说“我只擅长我手上的东西,这个我对付不了,我也没有兴趣和勇气去对付,还是让我解决我擅长的问题吧”,这个可以理解;如果说“这个只需要用我擅长的东西对付就足够了”,那就属于自欺欺人了。
8.2.5.3 属性是否在本领域内可分解
如果属性再分解就得到另一个领域的概念,那么这个属性可以留在类中。如果属性可以继续分解成本领域的概念,那么可以考虑把这个属性独立出去变成另一个类。
如图8-54,"联系人"的"称呼"属性的类型是String,相当于"联系人"关联到String类。String属于基础语义领域,已经不属于"联系人"所在的“人员管理”领域,那么"称呼"可以留在“联系人”中作为属性存在。
图8-54 String可以留在类中
而"组织"还可以像右侧所示分解成“名称”、“办公地址”等,这些概念依然属于人员管理领域,所以可以考虑将“组织”分离为一个类,“联系人”关联到“组织”。
图8-55 组织需要分离出来
注意以上的用词。我们只是说“再分解就得到另一个领域的概念”,并没有暗示这个另一个领域是什么,更没有暗示这个领域比核心域简单。任何领域,只要愿意深入研究,都是复杂的。
就拿“称呼”的类型String来说,String如果用.NET中的String类实现,这个类有157个操作,远远超过人员管理领域某个类所拥有的操作数目。只不过String类如何构造是别人负责操心的事情,不是我们,也就是目标系统的建模人员操心的事情。
在分析工作流,我们只需要判断某个属性再分解就到了另一个领域,或者说类型是另一个领域的类。至于属性的类型具体是哪一个类,UML提供了一些原生类型,如果认为属性的类型刚好是这些类型之一,可以指定,否则可以先不指定,因为分析模型没有绑定到任何一个具体的编程语言和数据存储平台,而不同的编程语言和数据存储平台的类型体系还是有差别的。
在EA中,把类的语言设成none,在属性的类型选择中,就看到UML定义的原生类型,如图8-56。
图8-56 UML定义的原生类型
8.2.5.4 是否有多重性大于1的属性
如果某个属性经过了8.2.5.3 属性是否在本领域内可分解的检验,但该属性多重性大于1,即所谓多值属性。例如,人员有多个手机号。
这时可以:
(1)把“手机”属性留在“人员”类中,多重性设为多,如图8-57。
图8-57 把属性的多重性设为多
注意,只需要说明多重性为多,不要放上一个该属性的数组或List之类。人员有多个手机号,这是一个领域的知识;用编程语言如何实现多值属性,是另一个领域的知识。一旦混杂,又会导致批量刷工作量。
(2)更推荐的做法是,把该属性分离出去,如图8-58。
图8-58 分离多值属性
人员有多个手机号,背后往往是有原因的。虽然现在不关注,很可能以后就会关注。例如,有的手机号是私人用的,有的手机号是办公用的,如果需要关注这些知识,那么就需要从图8-58转成图8-59中的某一个,此时只需要添加关联或者在“手机”类添加一个属性。
图8-59 当需要关注更多的知识时,类图发生变化
当然,这也不是建模“人员有多个手机”的最好做法。后文我们介绍到人员相关的分析模式时,会详细描述如何建模这个领域。
以下做法是不好的:
(1)在“人员”类中放上多个属性“手机1”、“手机2”、“手机3”……,如图8-60。
图8-60 错误:放上多个属性
这样的做法相当于把抽象级别降到了对象级别,或者用关系数据库建模的说法,这是违反第一范式的。如果人员没有那么多手机号,某些属性就会出现空值;如果人员拥有手机号的数量大于预设的个数,就会放不下。
(2)只有一个“手机”属性,属性值里用逗号分隔各个电话号码。
如果这样的做法是好的,那不如更进一步。各个属性也不用分了,就一个字符串。还可以再进一步,类也不用分了,也串在一起……持久存储或网络传输时的序列化不就是这样干的吗?
根源就在于软件是人做的,人脑的容量和运行速度有限,否则就不用理得那么清楚了,直接对计算机下二进制指令不好吗?
8.2.5.5 属性是否对所有对象都有意义
如果通不过8.2.5.1 属性直接描述类的检验,那么类和属性放在一起是不合适的,但这只是必要条件,不是充分条件,即使通过了也未必合适。
如图8-61,人的姓名,人的▲▲(▲▲是男性特有的器官),人的〇〇(〇〇是女性特有的器官)好像都说得通,但如果问:是不是所有对象都应该有这个属性呢?得到的答案就不同了。
是不是有人有姓名——是。
是不是所有人都应该有姓名——是。
是不是有人有▲▲——是。
是不是所有人都应该有▲▲——不是,只有一部分人有。
是不是有人有〇〇——是。
是不是所有人都应该有〇〇——不是,只有一部分人有。
说明“人”发生了分裂,分裂成“男人”、“女人”两个子集(子类)。▲▲是男人的属性,〇〇是女人的属性。
图8-61 分解只属于部分对象的属性到子类
8.2.6 评价DDD话语中的“值对象”
在识别类的时候,有的建模人员受到DDD话语体系的影响,会着急去分辨哪个类是实体(Entity),哪个类是值对象(Value Object),这是没有必要的,而且很容易成为遮掩无能的遮羞布。
8.2.6.1 历史回顾:不可变对象
1986年,Barbara Liskov和John Guttag在其讲述面向对象思想和CLU编程语言的书“Abstraction and Specification in Program Development”中,提到有两种对象:可变的(mutable)和不可变的(immutable),如图8-62。
图8-62 摘自Abstraction and Specification in Program Development, Liskov B. & Guttag. J. , 1986
******
CLU是Barbara Liskov和她的学生在1974-1975年间创造的编程语言,对后来的面向对象编程语言有重要影响。2008年,Barbara Liskov因在数据抽象、分布式计算和容错方面的贡献获得图灵奖。
Abstraction and Specification in Program Development无中译本。2000年,这两位作者又出了一本Program Development in Java: Abstraction, Specification, and Object-Oriented Design,所用的编程语言改成了Java。这本书有中译本,如图8-63。
******
图8-63 摘自《程序开发原理:抽象、规格与面向对象设计》,Liskov B. & Guttag. J. 著,裘健译,英文原版出版于2000年
再列早期一些使用“不可变对象”的文献,如图8-64和8-65。
图8-64 摘自Seamless Object-Oriented Software Architecture, Walden K. , & Nerson J. , 1994(本书无中译本)
******
Seamless Object-Oriented Software Architecture基于Bertrand Meryer的思想,作者是ISE Eiffel 2.2开发环境的主要开发者和BON(Business Object Notation)的发明者。ISE Eiffel是Interactive Software Engineering(由Bertrand Meryer创建)开发的Eiffel语言IDE,最初发布于1986年,现已改名为EiffelStudio,最新版本20.11。BON是类似于UML的建模表示法。
******
图8-65 摘自Non-Interference Properties of a Concurrent Object-Based Language: Proofs Based on an Operational Semantics, Hodges S. & Jones C. , 1995
现在,“不可变对象”依然在广泛使用,如图8-66。除了面向对象的书籍之外,更多的是出现在讲述函数范式的书籍中。
图8-66 摘自Seriously Good Software: Code that Works, Survives, and Wins, Faella M. , 2020
******
Seriously Good Software的中译本起名《你真的会写代码吗》,已于2021年7月出版。此处非广告。我未和出版社联系过,也不欣赏中译本乱改名的行为。提到此书只是随手举例,不代表推荐或不推荐阅读。
******
8.2.6.2 历史回顾:值对象
Martin Fowler和Kendall Scott在“UML Distilled”的第一版使用了“值对象(Value Object)”一词,如图8-67。
图8-67 摘自UML Distilled: Applying the Standard Object Modeling Language, Fowler, M. & Scott, K. , 1997(此版本无中译本)
Martin Fowler在他后续出版的书中继续使用“值对象”,如图8-68和图8-69。
图8-68 摘自《重构:改善既有代码的设计》,Martin Fowler 著,侯捷、熊节 译,英文原版出版于1999年
图8-69 摘自《企业应用架构模式》,Martin Fowler 著,王怀民、周斌 译,英文原版出版于2003年
J2EE话语体系也曾使用“值对象”,但有另外一种含义,相当于数据传输对象(Data Transfer Object),如图8-70。Martin Fowler在《企业应用架构模式》中讲述“值对象”模式时,提到了这一点。
图8-70 摘自《J2EE核心模式》,Alur D. 等 著,牛志奇 译,英文原版出版于2001年
******
《J2EE核心模式》(第2版)已不用“值对象”,改用“传输对象(Transfer Object)”。
******
“值对象”目前主要用在DDD话语体系中。您可以观察近年出版的书籍,里面提到“值对象”的地方,很可能在这个词的周围还会提到“实体”“领域驱动设计”“DDD”等。
也许有人会说“值对象”和“不可变对象”不是一回事。你看,名字都不一样嘛,说明侧重点不同。“不可变对象”可以有标识,Eric Evans甚至还说“值对象”可以改变属性值。
其实,相对于“值对象”的命名,“不可变对象”的命名更本质。我们更在意的是属性值是否可变,而不是有没有标识、如何判断相等。在8.2.6.4会进一步讲述。
8.2.6.3 回顾历史,警惕伪创新
翻出历史来,意思是说“值对象”的概念不是Eric Evans发明的,也不是Eric Evans给起的名字。
这一点并非所有人都了解,如图8-71中的表述。
图8-71 摘自《解构领域驱动设计》,张逸 著,2021
******
顺便再吐槽一下,图8-71中“面向对象设计的基本原则,如信息专家模式”的表达是不严谨的,原则和模式不是一个级别的东西。
以面向对象来说,被归纳的“原则”的数量最多也就两位数,最出名的是所谓的SOLID,而“模式”的数量就多了去了。
GoF(1995)有23个模式;Kent Beck的Smalltalk Best Practice Patterns(1997)有92个模式(就是格式不太规范);POSA(面向模式的软件架构)系列从1996年到2007年出了5本,作者说有114个模式;PLoPD(程序设计模式语言)系列从1995年到2006年也出了5本,其中收录的模式数目查不到,我也没得数,但PLoPD每一本的页数是对应POSA的近两倍;Fowler的《企业应用架构模式》有51个模式(“值对象”就是其中一个)……现在每年依然有新的模式书出版,去除那些变着花样复刻GoF赚流量的垃圾书后,还是有一些书贡献了新模式。
还有,PLoP年年开会,今年是第28届了。
******
如果不了解历史,就有可能会被某些伪创新的宣传所蒙骗。永动机、水变油的伪创新过一段时间就会改头换面出来收智商税,原因就是我们对历史教训的记忆太容易消失了。
如果人们得知一个东西曾经存在过,那么当这个东西再次被拿出来宣传时,人们会对宣传保持较多的理性,“这东西如果真的这么厉害,那之前怎么……”,宣传的人也会收敛,不至于那么夸张。
伪创新会选择换个名字,称自己是“全新的”、“革命性的”,给人一种从未有过的、从天而降的感觉。因为是“全新的”,所以再怎么夸大宣传,人们也还是会给一个机会,毕竟是“新”的,没准人家真的有这么牛呢。
例如,说青霉素可以治愈肝癌,大众肯定不信,要是真的那么多年不早就验证了嘛;如果把青霉素改个名字叫“K9527-α”,说可以治愈肝癌,可能就会有人买了试试。
正如前文(8.2.6.2)所说,伪创新还会有意割裂和已有知识的联系——我是“新”的,不受已有知识的约束。这样,在受到他人批评时,就可以巧妙辩解“你说的鹿和我说的鹿不一样”。
伪创新的宣传中往往会带有“艺术”、“禅”、“道”等字眼,有意无意地朝宗教、艺术、玄学方向引导——这些东西信仰是主要的,道理是次要的。
以上内容并非说“值对象”是伪创新,而是说要警惕过分的宣传——同样适用于UML及其他。
8.2.6.4 本书关于“值对象”的观点
对象本就应该是可变的
“面向对象”就是把某些数据和经常操纵这些数据的行为封装在一起变成类,以此作为系统的基本构造块,如图8-72。
图8-72 类把行为和数据封装在一起
如果说把经常在一起出现的语句集封装成子程序是一级封装,那么把经常在一起出现的数据和行为封装成类可以看作二级封装。
并非随随便便封装就能带来好处。像下面这些经常看到的有规律一一对应的“面向对象”封装:
*每个属性刚好对应一对getter/setter操作(现在的Property语法就更省事了),然后就说封装了,面向对象了。
*把某个或某几个行为凑成一个类,类的名称叫"***er"或“***or”,行为变成类的操作,然后就说封装了,面向对象了,再加上一个泛化结构,更是感觉高大上。Robert C. Martin写的书里面很多地方就是这种er、or类,没有属性,全是操作,然后SRP、OCP什么的喊一通,很多人以为这样就“面向对象”了。
这样的“面向对象”也不是一点用都没有,但还远远不够。
软件的复杂性在于,行为和数据不是一一对应的。某个属性值可能会被多个行为使用和改变,某个行为可能会使用和改变多个属性值。应该用哪些属性值来计算,怎么计算,会修改哪些属性值,怎么修改,这些行为规则封装在类中,可以通过状态机描述。状态表征了对象表现出相同行为规则的属性值组合,把行为和数据连接起来。
如果没有做到这样的封装,或者认为没有必要做到这样的封装,那面向对象的意义就不大了。如果一味强求属性值或状态“不可变”,那完全可以采用另外一种思考范式嘛。
不过,天上不会掉馅饼,即使换了另外一种思考范式,依然要面对上面提到的行为规则复杂性,只不过是换了一种方式表达而已。
关于结构共享(别名错误)
说到对象“不可变”的好处时,往往会提到一个Grady Booch称为“结构共享(Structural Sharing)”的问题,如图8-73和8-74。
8-73 摘自Software Components With Ada: Structures, Tools, and Subsystems, Booch G. , 1987(此书无中译本)
8-74 摘自《面向对象分析与设计(原书第2版)》,Grady Booch 著,冯博琴 等 译,英文原版出版于1994年
Martin Fowler把它称为“别名错误(Aliasing Bug)”,如图8-75。
图8-75 摘自https://www.martinfowler.com/bliki/AliasingBug.html
******
常见的应对方法是把setDate()改成返回一个Date,partyDate.setDate(5)变为:
partyDate=partyDate.setDate(5);
例如,C#中的DateTime值类型就是这样处理。
但是,如果从领域逻辑上认为“A就是B”,那么出现图8-75的结果就是应该的,所以问题的根源在于“A就是B”是不是对的,而不在于A、B能不能变化。
以图8-75为例,说派对日期就是退休日期是不合适的,本来就不应该是同一个实例,只能说两者的日期相同(业务规则可能会变化为“派对日期安排在退休日期7天后”)。不管是用另外的操作来取代“=”还是重载“=”运算符,应该体现这样的逻辑。
图8-75例子中,partyDate和retirementDate的类型都是Date,不是文中暗指的某应用系统的核心域概念。更合适的抽象可能如图8-76,哪一个最合适看具体情况了。
图8-76 更合适的抽象
另,Fowler关于Aliasing Bug最早用的并不是图8-75的例子,而是如图8-77。估计作者后来也是觉得不太合适:什么样的领域逻辑会让人写出导致Martin的受雇日期和Cindy的受雇日期引用同一日期对象的代码?从这一点小细节也可以看出,还是要从领域逻辑的角度来解读。
图8-77 摘自《企业应用架构模式》,Martin Fowler 著,王怀民、周斌 译,英文原版出版于2003年
不用急于去划分“实体”和“值对象”
按照本书前文所说的内容,识别和精化类和属性,再按照本书后文所说的内容建模类的状态和行为,你会发现,只需实事求是描述领域内涵,结果会自然而然显露出来,并不需要套上“实体”和“值对象”的概念。
即使为了附和DDD的“新话”一定要套上“实体”和“值对象”的概念,也不要急匆匆去套上。实际上,你也不能。没有对一个类作充分的建模就武断地针对这个类做出判断,证据是不充分的,只能算胡说八道。
和上册的推导需求一样,没有经过业务建模的思考,张嘴就说“本系统有**功能”,只能算胡说八道。
胡说八道没问题,甚至向其他人宣传自己的这些胡说八道是严密的推导也可以理解,但至少不要自己骗自己,真心以为这就是最佳的做法。
我们看Eric Evans的《领域驱动设计》中是怎么说值对象的,如图8-78。
图8-78 摘自《领域驱动设计:软件核心复杂性应对之道》,Eric Evans 著,陈大峰 等 译,英文原版出版于2003年
你看,“性能”、“性能”。Eric Evans在这个地方以性能为理由来强调“值对象”的重要,其实是不合适的。如果从领域逻辑出发,推导出需要“电话号码”类、“日期范围”类,这可以,以性能为由不合适。
面向对象(其他建模范式也一样)的思考首先是为了让有限的人脑资源能有办法去应对复杂的逻辑,而不是为了性能。我们在前文已经说了,在分析工作流中一旦让“性能”这个东西混进来,很容易导致废话刷工作量,a+b+c变成a×b×c——当然,前文也说过多次,这种不用思考就可以刷很多工作量的结果,可能正是某些开发人员乐意看到的。
关于“值对象”的命名
在DDD话语体系中,“值对象”和“实体”并列,这个命名是不太严谨的。
“值”后面有个“对象”,那“实体”后面怎么不加个“对象”呢?要么都加,“值对象”和“实体对象”,要么都不加,“值”和“实体”。
实际上,“值对象”和“实体”讨论的是类,说“这是一个值对象类”是比较奇怪的,如果一定要加后缀,改成“值类”或“值类型”更合适。
Martin Fowler在博客中(https://www.martinfowler.com/bliki/ValueObject.html)还提到“值对象”和“实体”的命名不符合二分法的问题,更推荐用“引用对象”和“值对象”。另一种划分就是前文所说的“不可变对象”和“可变对象”了。
1. [单选]为什么面向对象分析设计方法比面向过程好?
A) 面向对象更适合人脑去把握系统的复杂性。
B) 面向对象和需求的映射更直接。
C) 面向对象方法更容易掌握。
D) 面向对象更符合计算机的底层。
2. [单选]针对下图,以下描述最正确的是:
A) 这是“打车”领域的分析类图。
B) 这是“迪迪打车”系统的核心域类图。
C) 这是“打车”领域的核心域类图。
D) 这是“迪迪打车”系统的领域类图。
3. [多选]给类命名时,要注意不要在类名的最后加"情况"、"信息"、"记录"、"数据"、"表"等。请问,针对以下哪些系统的核心域建模,类名中可以出现刚才提到的这些文字?
A) 雨量数据信息监测系统
B) DBMS(数据库管理系统)
C) 疫情信息情况数据查询系统
D) 搜索引擎
4. [单选]关于“宝贝”和“商品”,经过讨论和思考,建模人员认为在目前所关注范围内这两个词可以认为相同,“商品”更适合作为分析模型中实体类的名字,那么,以下说法正确的是:
A) 实体类起名“商品”,但如果某类涉众觉得“宝贝”更顺眼,和他交互的界面上依然可以写“宝贝”。
B) 所开发系统的所有成分应一律使用“商品”,以建立“通用语言”。
C) 实体类一律使用“商品”,和人交互的界面上一律使用“宝贝”。
D) 应该以“宝贝”为抓手,将“商品”下沉到底层架构,击穿程序员心智,打出一套敏捷组合拳。
5. [多选]在符合某些条件时,这样建模是可以的,请把这些条件选上。
A) 开发团队决定走敏捷精益的领域驱动架构设计路线。
B) 手机只有一个摄像头。
C) 摄像头只关注一个属性:像素。
D) 用关系数据库来保存手机对象。
6. [单选]以下给类和属性命名,最合理的是:
A
B
C
D
7. [多选]以下说法正确的有:
A 实体-关系图和数据流图也可以描述分析模型。
B 和设计工作流的对象相比较,分析工作流的对象的特点是仅存在于内存中,不保存到硬盘。
C 每个用例映射一个分析边界类。
D 识别分析类时,精力应该重点放在实体类上。
E 识别分析类时,类名称以涉众常用的称呼为准。
F 系统外部有执行者,使用面向对象方法分析,系统内部一定有相应的实体类。
8. [单选]铁路售票处,售票员使用售票系统来售票,在用例进行过程中,系统需要不断向旅客反馈车次、车票和价格信息,系统还需要和银行系统交互。"售票"用例的分析序列图中,会出现_____个边界类,_____个控制类,_____个实体类。
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
9. [单选]电子商务系统有一个类“商品”,“商品”有属性“名称”和“演示动画”。开发人员初步打算在实现时,“名称”的类型设为编程语言的String,“演示动画”的类型设为某个通用3D类库的“Animation”类。以下说法正确的是:
A 因为String比较简单,所以“名称”属性可以留在“商品”类内。
B 因为Animation比较复杂,所以“演示动画”属性应该分离出去变成关联。
C 因为String和Animation都比较复杂,所以两个属性都应该分离出去变成关联。
D 因为String和Animation都不属于核心域概念,所以两个属性都可以留在“商品”类内。
10. [单选]如果要开发一个软件系统来生成奇书《平安经》,以下最正确的分析类图是:
A)
B)
C)
D)
11. [单选]从以下用例规约抽取类,哪些类应该抽取出来?
游客选择航线、航期,
系统反馈该航期的剩余仓位。
游客选择仓位所在层,
系统反馈该层平面图。
游客选择仓位,
系统验证该仓位可以预订,
系统保存仓位预订信息,
系统反馈预订成功。
A) 层
B) 仓位保存
C) 航线
D) 仓位验证
E) 系统
F) 仓位
12. [单选]要实现验钞机的“验钞”功能,恰当的抽象是?
A)
B)
C)
D)
13. [多选]软件开发人员和涉众交流某个问题时,涉众提到一个他工作中的专业术语“差动保护”,开发人员很难理解。对此,以下说法错误的有:
A) 开发人员应该认真去研究相关的领域知识。
B) 说明“差动保护”不是双方能用于沟通的“通用语言(Ubiquitous Language)”,开发人员应该带领相关涉众建立方便开发人员理解和沟通领域知识的“通用语言”。
C) 有的岗位的涉众学历较低、能力较差、知识面较窄,对领域的认识有时还不如开发人员,对这类涉众所说的,如果听不懂,可以先处理其他好理解的问题,然后有空再来处理该涉众的这个问题。
D) 开发人员应该用敏捷+DDD打法,给涉众赋能,连接涉众心智,通过通用语言(Ubiquitous Language)拉齐水位,形成闭环。
14. [单选]如果仅是出于整理领域知识的目的画领域模型,以下不需要画的类关系是:
A) 泛化
B) 依赖
C) 组合
D) 关联
8.3 建模步骤3-2 识别类之间的关系
首先要说明:虽然本书先讲解“识别类和属性”,再讲解“识别类之间的关系”,但实际工作中,先“识别类和属性”再“识别类之间的关系”这个思考顺序只是一个微小的思考周期内的顺序。建模一张类图,需要很多个思考周期。也就是说,识别类和属性→识别类之间的关系→识别类和属性→识别类之间的关系→……是交错进行的。
我们阅读用例规约或其他素材,一边思考一边建模,不管识别出类、属性还是关系,画上去就是,并不需要假装看不见类的关系,非得先把类和属性都识别完了,再来识别类之间的关系。
8.3.1 类之间的关系
类之间的关系有三种:泛化(Generalization)、关联(Association)和依赖(Dependency)。
图8-79 类之间的关系
泛化和关联是类的静态关系。不管系统当前有没有运行到需要用到这些关系的用例,甚至系统已经停止运行,系统都要想办法记住这两个关系,或者说,这两个关系属于系统要维护的“数据”之一。
泛化表示集合关系。两个类形成泛化,意味着超类的对象集合包含子类的对象集合。B、C泛化到A(或者说,B、C继承A),意味着A的对象集合包含B和C的对象集合,如图8-80。
图8-80 泛化表示集合关系
*也可以换一种说法:子类的特征集合包含超类的特征集合。
关联表示个体关系。两个类形成关联,意味着一个类的对象个体可能会和另一个类(也可能是同一个类)的对象个体有关系。A和B、C关联,意味着某个A的个体可能会和某些B和C的个体有关系,如图8-81的左侧;A自己和自己关联,意味着某个A的个体可能会和另外的A个体有关系,如图8-81的右侧。
图8-81 关联表示个体关系
因为关联是个体的关系,所以会有1对多、多对多等多重性。泛化关系是没有多重性的。
集合关系还是个体关系,这是泛化和关联的本质区别,从自然语言的表达来推断有时是不可靠的。
例如,自然语言"人有男有女"说的是泛化关系。"人有男有女"的意思不是一个人的个体里有若干男人个体和若干女人个体,而是说人的对象集合包含了男人的对象集合和女人的对象集合。
自然语言"人有手有脚"说的却是关联关系。"人有手有脚"的意思不是人的对象集合包含了手、脚的对象集合,而是说一个人的个体组装了若干手和脚的个体。
读者可以自行体会一下“人有车有房”和“人有高富帅有屌丝”的区别。
对于比较熟悉的领域,例如刚才的男女、手脚,拍脑袋就可以知道是泛化还是关联,那拍脑袋就可以了。如果进入陌生的领域,有时回溯到集合和个体的本质区别是必要的。
再来看依赖关系:
如果B变化,A也需要变化,那么可以认为A依赖于B。
从这个定义来看,泛化和关联也是依赖关系。泛化是子类依赖于超类,关联的依赖看关联的方向。不过,泛化和关联有另外的表示法,所以一般说的依赖指除了泛化和关联之外的其他依赖,例如调用、创建等。
*显然,只有在软件系统的分析或设计模型里才需要依赖关系,纯粹描述领域知识的领域模型只需要泛化和关联关系。*
这些依赖关系并非时刻都存在,而是在软件系统执行某个用例的某个步骤时才会产生,而且持续的时间非常短——从分析工作流的假设来说,就是趋近于0。因此,要描述依赖关系,仅在类图上描述“A依赖于B”是不够的,还要具体到某个场景。
例如,要描述A会调用B、C的操作,如图8-82在类图上画一根依赖的虚线箭头,不是不可以,但还不够。
图8-82 在类图上表达“A依赖于B、C”还不够
应该在某个用例的某张序列图(或通信图)上描述,在什么场景,进行到哪个步骤时,A需要调用B的什么操作,在什么场景,进行到哪个步骤时,A需要调用C的什么操作,如图8-83。
图8-83 “A会调用B、C”应在序列图上表达
有了序列图,如图8-82的类图上的依赖虚线就没有必要画出来了,类图上画泛化和关联关系即可。
经常看到这样的类图:上面布满了依赖的虚线,却没有泛化和关联,如图8-84。如果这样的类图描述的是不同领域的类之间的协作,那还可以接受,如果类图上都是核心域的概念,那就要警惕了,可能建模人员根本没有去寻找泛化和关联关系。
图8-84 只有依赖关系的类图
也许有的读者会想,图8-84这不挺规整的嘛!问题就在于,这个关系为什么是这样,很可能是没有依据的。建模人员拍脑袋定了这样的依赖关系,然后就假装自己“建模”了。
如图8-85,有ABCD四个类,可以随意编排它们之间的依赖,不管哪一种,都可以写出代码来,编译器也不会报错。但哪一种更合理,要从静态关系来找依据。
图8-85 没有依据,依赖可以随意编排
作者有时候会听到开发人员向我介绍他所做系统的“架构”,“您看,A调用B,B调用C……”,然后就没了。也不讲什么理由,反正我就是这样做了,而且做出来了,也能用,就行了呗!
8.3.2 识别泛化关系
8.3.2.1 识别泛化的思路
(1)直接形成
类图中的两个类可能会直接形成泛化关系,如图8-86所示。严格的做法是针对每两个类,思考“A是B的一种吗?”,再反过来思考“B是A的一种吗?”不过如果真的要这样做,工作量还是挺大的。类图中有n个类,就需要思考2C2 n=n(n-1)次。n=11时,就是100次了!实际工作中,往往是先扫描一遍,大脑迅速过滤出可能值得这样思考的类,针对这些类思考即可。
图8-86 直接形成-两个类之间直接形成泛化关系
实际上,类图上已有的两个类有泛化关系但未识别的情况并不多,因为之前从用例规约识别类和属性时很有可能已经发现了。
(2)自下而上(从特殊到一般)
更多的情况是发现类图上已有的两个或多个类有共同特征,于是抽象出共同的超类,如图8-87所示。
图8-87 自下而上-两个类之上有共同的超类
关联也可以看作类的属性,关联的角色名相当于类的属性名称。如果多个类关联到同一个类而且角色名相同,也可以考虑泛化出共同的超类,如图8-88。
图8-88 共同的关联也可以提炼超类
(3)自上而下(从一般到特殊)
如图8-89所示,这个识别思路就是8.2.5.5 属性是否对所有对象都有意义里的思路,此处就不再重复叙述。
图8-89 自上而下-一个类分裂出子类
8.3.2.2 Liskov替换原则
只是从名称上来判断,并不能成为泛化关系的最终证据。Liskov1988年在“Data abstraction and hierarchy”文章中提出了一个判断的标准,后来被称为Liskov替换原则(LSP):
如果对于每个类型S的对象O1,都有类型T的对象O2,对于所有以T的形式定义的程序P,当O1被O2替换时,P的行为不变,那么S是T的子类型。
很多书和文章中提到Liskov替换原则时,会以矩形和正方形(有时会换成椭圆和圆)的问题为例。
假设把正方形看作矩形的子类,如图8-90。
图8-90 把正方形当作矩形的子类
设置某矩形的A边长为4,再设置B边长为5,按照设想,此时求面积应该得到4×5=20。如果用正方形代替矩形,要么为了保持正方形的约束,最终得到的面积是5×5=25,要么两边自由变化,正方形就不再是正方形了。
根据Liskov替换原则可以判断出图8-90不合适,但Liskov替换原则并没有办法回答其中的原因,我们从领域知识的角度来看看。
如果先不考虑类之间的关系,也不用考虑图8-90定义的行为的规则,只是独立描述矩形和正方形所需的属性,可以得到图8-91。
图8-91 建模矩形和正方形的属性
从图8-91可以得知,正方形的属性比矩形还少一个,把正方形作为矩形的子类是不合适的。
反过来,矩形倒是更像正方形的子类,复用边长,再加一个边长。Bertrand Meyer在“Object-Oriented Software Construction”中就提到,某类库的早期版本,就是让矩形继承正方形。
图8-92 摘自“Object-Oriented Software Construction( Second Edition )”, Meyer B. , 1997
要建立正确的关系,可以减弱超类的定义,由子类定义有几个边长属性,如图8-93。
图8-93 超类不定义有几个边长属性
图8-93中,正方形是矩形的子类,但不是自由矩形的子类。自由矩形、正方形和黄金分割矩形(边长比为黄金分割比0.618····:1)等是互相不重叠的矩形子集(子类)。
也可以把A边长和B边长属性保留在矩形中,正方形、黄金分割矩形如果适用特殊的规则,可以作为矩形的状态。正方形、黄金分割矩形等没有增加新的属性,只是要求A边长和B边长的值符合某个约束,也就是说,正方形、黄金分割矩形是属性值组合中的一个子集的表征,这个就是状态。如图8-94。
图8-94 用状态来表达不同矩形
8.3.2.3 被误作关联的泛化
泛化关系有时会被误认为关联,下面列举一些错例供参考。
图8-95中,“员工有调度员、装卸工、配货员”指的是员工的对象集合包含了调度员、装卸工、配货员的对象集合,不是指一个员工对象由调度员对象、装卸工对象、配货员对象构成。正确的关系是右侧的泛化关系,而不是左侧的关联(此处是组合)关系。
图8-95 泛化被误作关联 例1
很多系统经常需要设置一些参数,有人会把参数建模成图8-96左侧的类图,把超时时间、锁定设置、频带等作为参数的属性。属性其实就是关联(此处是组合)的一种变体,8-96左侧和右侧是等同的。
图8-96 泛化被误作关联 例2
图8-96的意思是一个参数个体由若干个具体参数个体组成,这不符合领域内涵。更符合领域内涵的是“具体参数是参数的一种”或者“参数的集合包含各具体参数的集合”,也就是说,泛化关系更合适。还有一种做法是把具体的参数全部抽象为“名称”和“值”两个属性。如图8-97。
图8-97 泛化被误作关联 例2 更正
如果按图8-96的方式建模,参数类只有一个对象,但这个对象有很多个属性。当需要为系统设置一种新的参数时,就需要修改类结构,增加新的属性。如果按图8-97的方式建模,只需要增加新的参数对象即可,类结构不需要改变。
一些1对0..1的关联,有可能是泛化关系。例如,1台电器可以是1台洗衣机,也可以不是;1台电器可以是1台电视机,也可以不是;1台电器可以是1台空调,也可以不是,有人认为是关联关系,于是画出图8-98。
图8-98 泛化被误作关联 例3
图8-98的意思是一台电器可能由一台洗衣机、一台电视机、一台空调组装而成,这是错误的,应该是电器的集合包含洗衣机、电视机和空调的集合,即泛化关系。如图8-99。
图8-99 泛化被误作关联 例3 更正
8.3.2.4 尽量不要跨领域使用泛化关系
分析工作流的类建模关注的是核心域概念及其关系,但有时候建模人员会不自觉地引入非核心域的内容。例如,若干学员组成小组,建模人员想到了如何实现小组有多个学员的问题,决定用List来实现,于是有图8-100。
图8-100 通过泛化来复用另一个领域
图8-100的问题是把本来应该隐藏在背后的非核心域概念显式引入到核心域类图中。前文已经说过,域之间的映射往往是有规律的,即使实现时由于某种偏好就是要通过泛化来复用,也没有必要逐一画出来。当然,更合理的实现是通过关联来复用List,如图8-101所示。
图8-101 通过关联来复用List
8.3.3 识别关联关系
8.3.3.1 关联的三种形式
如果乐意细分关联关系,还可以把关联分为三种:普通关联、聚合(Aggregation)和组合(Composition)。
用UML图形表示,普通关联是一根直线,聚合的整体一端是空心菱形,组合的整体一端是实心菱形,如图8-102。
图8-102 三种关联的图示
说到这里,我们要整理一下类之间的关系,用类图表示如图8-103。
图8-103 类之间的关系
从图8-103可以看到泛化、关联和依赖在一个抽象级别,普通关联、聚合和组合在一个抽象级别。有些书和文章里会说“泛化和组合”或“继承和组合”,其实是不合适的。
之所以分出聚合/组合这样的特殊关联,考虑的出发点是责任分配。通过建立聚合/组合,使整体把部分封闭起来。在责任分配时,不管外部对象想要发消息给聚合/组合结构里的哪一个对象,都应该先把消息发给整体对象,再由整体对象分解和分配给聚合/组合里的对象,如图8-104所示。
图8-104 聚合/组合关联影响责任分配