墙上挂了根长藤,长藤上面挂铜铃

《长藤挂铜铃》;词:元庸,曲:梅翁(姚敏),唱:逸敏,1959


您在阅读《软件方法》时如果发现错误,欢迎通过微信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展示了不同系统类型的核心域和非核心域概念:

系统类型
核心域概念
非核心域概念(选择之一)
文档处理器
文档、页、行、字……
CStringArray、CFileDialog、MSXML……
电子商务网站  
商品、订单、会员……
</div>、ActionForm、SessionFactory……
操作系统
处理器、内存、页面……
struct、char、int

图8-1 不同系统类型的核心域、非核心域概念

以文档处理器为例,开发Microsoft Word和LibreOffice Writer所使用的编程语言和组件不一样,但文档、页、行、字等核心域概念是一样的。即使回到计算机诞生之前或者去到未来,这些概念也依然存在。

关于“核心域”和“非核心域”,一种常用的通俗说法是"业务"和"技术",但"业务"和"技术"的说法并不严谨。

有的开发人员在潜意识里是这样划分的:

*我懂且我感兴趣的知识→技术;(我懂Java编码,我对Java编码感兴趣,Java编码是技术)

*我懂但不感兴趣的知识→业务;(下单、收银、配送我懂一些,但不感兴趣,这些是业务)

*我不懂但感兴趣的知识→高科技;(我不懂深度学习,但很感兴趣,哇塞,高科技)

*我不懂且不感兴趣的东东→忽悠。(我不懂UML建模,也不感兴趣,妈的,忽悠)

有的开发人员则是这样划分的:

*和计算机无关→业务;

*和计算机有关→技术;

核心域不能以“懂”、“感兴趣”来判断。核心域不一定是非计算机领域,也可以是计算机领域,如图8-1中的操作系统。

另外,还要特地说明的是,本书中的“核心域”和Eric Evans《领域驱动设计》以及后续相关书籍中的“核心域”(Core Domain)意思不同。

本书中的“核心域”指软件系统中不可替换的那部分内容——这个以软件开发人员的知识是可以判断的。

EricEvans《领域驱动设计》知识体系中,把“领域”(相当于本书中的“核心域”)划分为"核心域"、“通用子域”、“支撑子域”等,例如“Delivery”是核心,“Customer”是通用,“Billing”是支撑——这个划分已经超出了软件开发人员的知识,我不认为软件开发人员有能力以及有必要做这样的判断。

一家商场之所以能击败其他对手,原因未必是下单部分有什么不同,倒有可能是在配送环节下了大力气,或者客户服务抓得好。没有经过商业竞争的思考,武断地认为某个子领域是“核心”是不合适的。

8.1.3 域之间的映射和协作

域和域之间的映射以及协作的规律,与域中的个体不直接相关。

例如,我们看一个"人员管理"领域的类图,如图8-2所示。

图8-2 核心域类图

如果将图8-2中的Person类映射为C#实现,可能会得到图8-3的C#代码:

图8-3 类的C#实现(用EnterpriseArchitect映射)

如果将图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 过早混合不同域的知识会增加大脑负担

过早地混合不同域的知识,会加重开发人员大脑的负担,导致开发人员腾不出脑力来思考核心域中更深刻的问题,只好稍微折腾一下如图8-5的“域之间的架构”,心里安慰自己,我有“架构”了!却忘了,其实还没有触碰到最需要大脑去思考的核心域概念和逻辑。而这又很可能会被巧妙地当成遮羞布——不是我不思考,而是要想的事情太多了顾不过来啊!

而这种微妙心态的进一步发展,就会导致开发人员有意无意地混合不同域的知识,把复杂度弄成a×b×c,以此达到通过废话刷工作量——以最少的思考得到最多的“成果”。

我经常听软件组织的架构师向我介绍他们所开发系统的“架构”,口沫横飞,说的基本上都是图8-5的“域之间的架构”。好啊,真棒,我知道了。还有呢?没了?

构思那些“域之间的架构”是某些厂商或者方法学家的工作,我们挑一个适合自己项目的套路用上就行了。有什么问题,可以去请教用这个套路用得好的先行者。

“域内部的架构”,那些核心域概念和复杂逻辑,这是系统最值钱的地方。要是我们没有办法理清楚,别人是帮不到我们的。这才是大脑最该用的地方!

平时我们说“需求变了”,大多数情况是功能需求变化,导致系统所封装的核心域逻辑需要调整,而不是“域之间的架构”需要调整,所以没有必要整天津津乐道“域之间的架构”。

一些以“领域驱动设计”为名的文章,所举例子就1-2个领域类,然后就开始讨论Entity、Service、Repository、DTO、六边形架构……不是说这个知识没用,问题是软件组织缺的是这个嘛?

在一些软件开发技术大会常可以看到这样的场景:某电子商务网站的架构师上台讲了一通,接着某视频网站的架构师上台也讲了一通,咦,两个演讲内容如此相似?原来,他们讲的都是自己系统中“域之间的架构”,而不是核心域内部的机制。究其原因也许并非不为,而是不能——架构师对自己所开发系统的核心域研究太浅。

许多“网红程序员 ”在网上谈论的内容大多是某种语言或框架的新特性,少有探讨他当前所开发系统的复杂领域逻辑,也是同样的原因:并非不为,而是不能。

说了那么多,归纳起来就是一句话:

8.1.4 重视分析工作流

分析,就是从核心域的视角构思系统的内部机理。

在现在的很多软件组织中,分析工作流的技能是非常被忽视的。很多开发人员上手就直接编码,原因并不是软件开发项目的领域逻辑简单到了不需要分析的地步,或者他在大脑里就完成了分析的工作,而是开发人员缺乏分析的技能,只好瞎碰了事,而且为了遮掩自己的无能,还会想各种办法来遮羞。

就像高考的时候,前面几道题比较容易,可以扫一眼就写答案。越往后题目越来越难,学霸会拿出草稿纸,列出已知条件,正推、逆推……理出解题思路,然后再答。

学渣就麻烦了,根本没有学习相关的知识和解题方法,怎么办?

遮羞利器(1):时间。

例如,抱怨考试时间太紧张,来不及思考,只好胡乱写个答案,甚至故意提前交卷,力图给人造成一种“如果时间允许,我是能做对的”的印象——真相是,给再多的时间也不会。

对应到软件开发,就是以“时间紧”、“敏捷”为借口掩盖自己没有能力剖析复杂逻辑的事实。

遮羞利器(2):空间。

例如,考试时故意选择不好写的笔和劣质的草稿纸,力图给人造成一种“如果纸和笔再好一点,我是能做对的”的印象——真相是,不会就是不会,给他再好的纸笔也不会。

对应到软件开发,就是借助“口头交流”、“白板”等容量小的介质,掩盖内容的苍白。白板就这么大,所以客观上你总不好意思让我用白板剖析复杂的逻辑吧?

伽罗瓦在决斗前一天晚上仓促写下自己的数学思想,不停哀叹“我没有时间了”。唉,早干嘛去了,不过伽罗瓦是真懂。

图8-7 伽罗瓦决斗前一天的手书

费马在书的空白处写下“费马猜想”,还写“我确信我发现一种美妙的证法,可惜这里的空白处太小,写不下”,估计费马是忽悠。

此处提到此二人纯属作者关于“时间”、“空间”不够的随意联想,无其他含义。

遮羞利器(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.5 分析相关历史的简单回顾

1958年,John W.Young Jr.和Henry K. Kent发表“Abstractformulation of data processing problems”,第一次提出在独立于实现的抽象级别上定义系统的规范。

图8-8 来自 “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-9 来自 Structured analysis and system specification(DeMarco T,1979)的截图

图8-10 来自 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-11 来自PC Magazine 1990年1月30日刊的截图(被圈住的内容说明了工具的数量)

1980年代后期,面向对象的思想开始用于分析和设计。然后,UML统一了表示法。这部分历史已经在本书第1章“UML简史”部分讲述,此处不再赘述。

图8-12 来自 Object Oriented Analysis, 2nd Edition(Coad P,Yourdon E, 1990)的截图

图8-13 来自 Object lifecycles. Modeling the world in states(Shlaer S, Mellor SJ, 1992)的截图

8.1.6 互联网和敏捷的影响

互联网浪潮以及敏捷运动的冲击打断了分析的传承。

互联网浪潮到来之前,软件系统的竞争焦点是功能。

我1997年毕业,先到高校当了一年老师,然后才去软件公司做程序员。第一个参与开发的系统是酒店管理系统。这样的系统用的人不多,服务器一台,每个部门放上一台客户端电脑就差不多了,但功能很多,入住退房、收银、客房,餐饮、娱乐、财务、电话计费、各种报表等等,能不能把领域逻辑理清楚非常关键。

互联网的兴起带来了这样一种系统:这种系统功能很简单,开发这种系统时需要思考的领域逻辑很少,但是这样的系统可以通过互联网让非常多的人使用,问题的关键变成了“如何在大用户量下保持性能”。

典型的例子是1996年出现的hotmail,推出一年多时间就有1200万的用户。hotmail是一个基于web的电子邮件系统,这样的系统,开发出来并没有太大难度,竞争的关键在于有没有背景、有没有钱买基础设施,有没有钱做推广……。

可能有人会说“邮件系统也有逻辑啊!”当然,这同样是一个领域,也有逻辑,但是其中的绝大多数逻辑已经被前人探索得很清楚,甚至有实际的可用组件提供,并不需要web电子邮件系统的开发人员从头思考。

很多开发人员就进入了类似的“互联网公司”,开发或维护类似的系统。因为不需要剖析复杂的领域逻辑,开发人员有没有掌握分析的技能已经无所谓,于是,很多打着“敏捷”旗号的“方法”就在这类公司大行其道,导致软件开发人员的分析能力普遍退步。

经常有人和我说,潘老师,敏捷这一套做工厂管理系统之类的可能不太行,但不得不承认,做互联网很管用噢!

当然管用了!

有个巫医发明了一种治疗方法。他坦言,我这个方法对付癌症可能不太行,但对付感冒很管用噢!你不信,找个感冒患者来!

感冒患者找来了,医生让患者躺在一张绘有八卦图案的方桌上,然后绕着患者绕了八八六十四圈**(看到没,他也是有一套方法的!)**,然后对患者说,回去该吃吃该喝喝,五天之内就好了!

果然,患者好了。

医生四处宣传他的治疗方法,由于此方法简单易学,迅速收获了大批粉丝。

图8-14 电影《破坏之王》截图

给软件开发人员一段文字描述,让他提炼和表达其中的领域概念和关系(通过ER图、类图……甚至口述表达都可以)。基于我在训练班上的体会,能在这个测试中给出合格结果的开发人员占全体开发人员的比例,如果在2000年占百分之x的话,二十年之后的2020年,这个比例是否能占到千分之x都值得怀疑。

随着互联网的成熟,大部分组织都变成了“互联网组织”。以往以“互联网公司”著称的巨头们变成了行业领袖,宣称“我是做互联网的”已经不足以包装自己,必须要对领域深入挖掘了。

但是,开发人员“敏捷”惯了,怎么办呢?还能回得去吗?

图8-15 分析技能下降之后,还能回得去吗?

8.1.7 伪创新

于是,就出现了各种伪创新。

有的人(国外国内都有)没有掌握相应技能,也不愿意认真学习已有的知识,凭着一些朦胧的“领悟”,就“发明”了一些“新”方法,这就是伪创新。

软件开发的一些伪创新前些年打的是“敏捷”的旗号,最近几年打的是“领域驱动设计”的旗号。仔细观察,背后推动的人很多是重叠的。

伪创新,例一:

图8-16摘自2017年出版的某本名字中带有“Domain-Driven Design”的书,看起来有点像图8-9,对吧?但是图8-16的内容和绘制于1979年的图8-9比起来,水分要多得多。

图8-16 来自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-16的表示规律可以看出来,方框是workflow,进入箭头是command,出去箭头是event。既然如此,每个标签文字后面其实可以不用加“workflow”、“command”、“event”等字样,但是,不加怎么显得我工作量大呢?

考虑到这一点,信息浓度估计20%吧。或者反过来说,一条信息刷到5倍。

图8-16还有一个问题,混合了非核心域的知识,会造成之前说的a×b×c,具体在什么地方,留给读者观察。

伪创新,例二:

图8-17摘自2019年出版的另外一本名字中带有“Domain-Driven Design”的书。展示的就是打着“领域驱动设计”旗号的伪创新之一:事件风暴(EventStorming)。

图8-17 来自2019年出版的某本名字中带有“Domain-Driven Design”的书的截图

我把图8-17里提到的概念提炼出来,画了1个类和4个小人,如图8-18。数一数,包括类名称在内,图8-18一共有16个概念。

8-18 我提炼图8-17的概念画的图

有了图8-18,可以准备开车……不,准备刷工作量了!

(1)创建对象,销毁对象,刷2个蓝色纸片,就是图8-17中的Createan ad和Remove ad(怎么前面没有an了,刷得不整齐,重刷!)。

这个步骤刷出2个蓝色纸片。

(2)多重性为1的属性(从图8-17看应该是title、text和sell price),每个刷1个蓝色纸片,就是图8-17中的Change the ad title(怎么不和后面两个一致都用Update,刷得不整齐,重刷!)、Update the ad text和Update ad sell price(怎么前面没有the了,刷得不整齐,重刷!)。

这个步骤刷出3×1=3个蓝色纸片。

另外,本来这些都是Ad的属性,直接称title、text和sell price即可,不必再加前缀,但不加怎么能刷出工作量呢?一定要加!

(3)多重性为多的属性(从图8-17看应该是picture和category),每个刷2个蓝色纸片,Add***和Remove***。

这个步骤刷出2×2=4个蓝色纸片。

另外,本来这些都是Ad的属性,图8-17中写Add***和Remove***即可,不用加to Ad、from Ad,但不加怎么能刷出工作量呢?一定要加!

(4)每个操作刷1个蓝色纸片。

这个步骤刷出6×1=6个蓝色纸片。

咦?这些改变状态的操作看着怎么和属性没有什么关系呢?是属性漏了,还是操作错了?估计作者也不太了解操作、状态和属性的关系吧。

(5)把(1)-(4)步产出的所有的蓝色纸片变换词序,每个旁边加一个橙色纸片。

这个步骤结束后,得到(2+3+4+6)×2=30个方框形状的纸片,或者说,15对。

(6)把图8-16中的4个小人和每对方框任意组合,得到大约15个黄色小人。

最终,我们从往墙上贴了30+15=45张小纸片,数量和内容正好和图8-17中的纸片相同。

信息浓度=16/45×100%=36%。再考虑到那些刷上去的Ad,估计差不多33%的浓度。或者反过来说,一条信息刷到3倍。

要注意,这仅仅是其中一个类Ad,其他类照此办理,今年的工作量可算是有交代了。

是不是创始人英明神武,只不过其他人把经念歪了?感兴趣者可以去自行看“事件风暴”的作者Alberto Brandolini的书,看看书里面讲了什么。

*调查:您看过的以“领域驱动设计”为名的文章,有类似的刷工作量的情况吗?怎么刷的?请在本文评论留言。

这些伪创新在思想上都有共同的错误:一一对应,内外不分。

世界之所以复杂,或者说,系统之所以复杂,就是因为很多关系不是一一对应的。

组织的一个流程可能由多个系统(包括人肉系统和非人系统)协作完成,一个系统可以参与组织的多个流程;系统的一个用例(如果读者没有掌握上册讲解的用例的知识,就当作是功能吧)可能由系统的多个类协作完成,一个类可以参与系统的多个用例。类的一个操作可能会影响多个属性,一个属性可能会被多个操作影响……

正是因为如此,才需要软件开发人员大脑来找出最佳的映射方案,这才是人的脑力需要花费的地方。

而这种思考是有一定门槛的,不是所有人都能胜任。

如果一个人不能胜任,而又不愿意花时间去学习,当有一种“一一对应刷工作量”的伪创新出现在他面前时,自然而然就会产生一种虚幻的“受用”感觉,欢快地投入伪创新的怀抱——

“哥也是有方法的人了!”

最近几年的微服务浪潮中,类似“以用例为依据分割系统”甚至“以业务流程为依据分割系统”等明显内外不分的言论为什么能流传开,就是因为足够简单,方便一一对应刷工作量。

***********

初中数学里要学习全等三角形、相似三角形、SSS、SAS……,到了高中以后学了正弦定理、余弦定理等解三角形的知识……就不会再回去用初中的方法解题了。

但是,不是所有人都能学会高中的知识,比如说张三。

张三可能会这样解释:

“我这个人能力比较弱,只能掌握全等三角形、相似三角形的方法。”

这样的说法没有问题。

张三还可能会这样解释:

“这个题目比较简单,用全等三角形、相似三角形的方法做足够了,而且这样更方便广大人民群众理解。”

这样的说法也可以。不过,竞争对手不是傻子,市场中哪里有什么"简单题目"!能带来利润的题目都很复杂。

但是,张三如果这样说:

“全等三角形、相似三角形的知识比高中三角函数的知识更深刻。”

这就是自欺欺人了。

更要警惕的是,有一个李四,也许和张三一样没有掌握高中方法,也许掌握了高中方法但是为了忽悠张三们,偷偷把"全等三角形"改名为"叠合三角形",然后和张三宣传:

“我发明了"叠合三角形"新方法,比高中的三角函数有用,三角函数过时了。”

这就是可恶了。

***********

回到前面举的伪创新例子。如果说熟练掌握类图、状态机图等建模技能,并发现了其中的缺点,站在前人的肩膀上创新,这个要举双手双脚支持。

在不了解已有知识的情况下,拍脑袋搞出伪创新,甚至向大众宣传伪创新,也是个人自由。

但是,宣传伪创新时,像上面李四那样胡说“我这个方法比***好”,就不对了。

事实上,一旦付出努力,咬咬牙掌握了更严谨和更高效的方法,是羞于再回头去使用那些打着“敏捷”或“领域驱动设计”旗号的伪创新的。

8.1.8 本书使用的分析方法

分析模型描述系统要封装的核心域知识。

至于用什么建模概念来思考和描述核心域知识,可以有很多种选择。例如,“人”用不同的建模概念描述,可以说它是一个“类”,也可以说它是一个“类型”、一个“实体”。

本书使用面向对象的建模概念来描述分析模型,从三个视角来描述:

分析类模型:描述系统中各个类以及类之间的关系。

分析状态机模型:描述某个类的各个行为的逻辑。

分析交互模型:描述某些类在实现某个用例时的协作。

面向对象的分析模型的表达形式,也可以有多种选择:语音、文本、图形等。

有的人觉得当前许多编程语言的表达能力已经很强,认为用文本已经足够,抗拒用图形来表达领域知识。

但是,和只有自上而下顺序的文本相比,能够朝四个方向扩展的平面图形(如果有三维模型就更好了)更容易让建模人员看出领域概念之间的联系。例如,图8-19和8-20的内容,如果没有图形的帮助,直接用文本一行一行地构造分析模型,人脑的负担非常重。

图8-19 餐饮领域的类图

图8-20 来自 Practical UML Statecharts in C/C++(Samek M, 2008)的截图,计算器的状态机图

说到这里,又不可避免地要提醒,故意选择文本的形式来表达领域知识,有可能也是一种遮羞利器。图8-19和8-20的内容如果用文本表达,可能会得到很多页文本——这就有了理由:因为工作量太大了,所以很多地方无法做深入的思考,可以原谅!

本书使用类图、状态机图和序列图三种UML图形来表达面向对象的分析模型。UML类图表达分析类模型,UML状态机图表达分析状态机模型,UML序列图表达分析交互模型。

图8-21 本书的分析方法所使用的UML图形

需要说明的是,虽然我们用的是面向对象的分析方法,也就是说,用面向对象的概念来剖析核心域知识,但不意味着你的系统一定要用特定的“面向对象”编程语言、存储方式或物理分布形式来实现。

也许你使用的编程语言是面向过程语言,例如C;也许你使用的编程语言是函数式语言,例如F#;也许你使用的存储系统是关系数据库系统,例如SQL Server;也许你使用的存储系统是非关系数据库系统,例如MongoDB;也许你的系统运行在同一台机器上,也许是分布在很多台机器上……

不管你的系统的实现方式和运行形态如何,从分析过渡到设计时,变化的只是分析到设计的映射套路。如果设计所使用的非核心域比较“面向对象”,那么映射套路会比较直观一些,否则,就需要一定的转换。但无论如何,如前文所说,这个套路和具体的核心域知识没有关系,我们并不需要针对每一个核心域概念逐一花费脑力去思考它。

我们之所以选择在分析工作流使用面向对象的分析方法,是因为从思考深度和表示的严谨程度来看,面向对象的分析方法以及UML表示法目前仍然是剖析和整理领域逻辑的最佳选择。

本书在设计工作流的内容,会展示分析模型和各种实现方式的映射套路。事实上,在实现本书的案例时,部分实现会使用函数式编程语言。

扫码或访问

http://www.umlchina.com/book/quiz8_1_1.html

完成在线测试,做到全对以获得答案。

1. [单选]掌握MVC、MVP、MVVM、六边形、洋葱型……等模式或架构,并不能解决分析的问题,原因是:
A) 它们描述的是域之间的协作。
B) 它们没有得到广泛使用。
C) 它们没有体现面向对象的思想。
D) 它们不够敏捷。
2. [单选]以下(1)-(4)所展示内容的共同点是:
(1)

(2)

(3)

(4)
产品愿景:为了满足内外部人员,他们的在线订餐、自动订餐统计和外部人员管理的需求,建设这个在线订餐系统,它是一个在线订餐平台,可以自动订餐统计。它可以同时支持内外网订餐,同时管理内外部人员订餐和定期订餐分析。
A) 都是UML模型
B) 都有废话刷工作量的嫌疑
C) 都涉及到电子商务领域
D) 都不够敏捷
3. [单选]第一款CASE(计算机辅助软件工程)工具是:
A) Rose
B) ERwin
C) FlowChart/360
D) DesignAid
4. [单选]“领域驱动设计”体系中有“限界上下文”的概念(类似于UML的组件)。有的人提出以功能(或用例)为依据划分上下文,有的人甚至提出以业务流程为依据划分上下文。这些做法的主要问题是:
A) 没有区分核心域和非核心域
B) 内外不分
C) 没有使用UML的标准符号
D) 不够敏捷
5. [多选]以下说法正确的有:
A) 分析模型的内容就是用核心域术语表达的内容。
B) 分析模型概要地描述核心域知识,设计模型将核心域知识细化。
C) 面向对象的分析模型不妨碍使用面向过程的设计。
D) 口头表达也可以表达分析模型。
6. [单选]有一篇文章,作者在白板上画了一个类图,然后开始掰着指头数这个类图缺什么,"没考虑到持久化","没考虑到对象的创建"……然后得出结论:画这个类图不如直接编码。根据本节的知识,以下正确的说法是:
A) 作者不了解核心域和非核心域分离的重要。
B) 别急,这个图会越来越细,逐渐添加作者认为缺少的那些东西。
C) Talk is cheap. Show me the code.
D) 敏捷是建模的精髓,加上这些就不敏捷了。
7. [多选]有开发人员说“现在开发一个应用真容易,咔咔咔几下,绝大部分工作框架都帮你弄好了,只要添加一些业务代码就可以了”,从本节的知识点评价这段话,以下说法正确的有:
A) 这段话的背后是一种无利的思维。
B) 这个说法太片面,有的业务是比较难的。
C) 非核心域的复用竞争对手也容易获得。
D) 这正是敏捷开发的优势。
8. [多选]以下描述的系统中,适合用面向对象的分析方法做领域建模的有:
A) 电磁轨道炮武器系统
B) 互联网拼单购物系统
C) PC单机角色扮演游戏
D) 电梯控制系统

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

8.2.1 三种分析类

当使用面向对象的方法来分析系统时,我们假设系统由"对象"这样一种东西构成,对象封装了数据和行为。

在分析工作流,我们认为系统中的对象在一个虚的"对象空间"中运行。这个空间不是内存,也不是硬盘,只是人脑中的一个逻辑空间,将它想象成宇宙空间也未尝不可。在"对象空间"中,速度不是问题,对象的创建和对象之间的通信都非常快。

图8-22 虚的"对象空间"

以上内容可以用来判断你思考的问题是分析问题还是设计问题。

我们可以针对分析模型里的元素,一个一个问“如果性能不成问题,速度无穷快,这个东西还有必要存在吗”,如果答案为否,那么从分析模型中把它删掉。

分析模型受到设计的污染,很容易导致批量的废话刷工作量,导致没有时间思考应该思考的问题(当然,这也可能是某些人乐意的)。

注意上文提到的"假设"二字。面向对象就是一个假设,如果不认可“系统由对象构成”,也可以分析系统的核心域逻辑,只不过用的方法不叫“面向对象方法”。

面向对象的思考方式比目前的其他思考方式要好一点,原因不是计算机喜欢面向对象或者面向对象更接近于计算机的底层(计算机更"喜欢"人类用机器语言编码,一千万行指令写在一起依次执行),而是面向对象的思考方式更能帮助人脑去剖析复杂问题。

但这并不意味着面向对象的思考方式比其他的思考方式更容易掌握(参见前文提到的全等三角形和三角函数类比),而且随着你掌握了更强有力的思考工具,更复杂的问题就会扑面而来。这些问题之前已经存在,只是之前你没有能力来发现和对付它们——就像“古人很少死于癌症”一样。

在接受“面向对象”假设的前提下,我们接下来就要做第一步思考:系统由什么样的对象构成。

在这一步思考中,我们通过抽象思维把具有共同特征的对象集合归纳为"类",对象看作类的实例。

归类是人类认知的一种基本技能,其哲学讨论可以追溯到柏拉图的理型论(Theory of Forms)。

依照Ivar Jacoson在“Object-Oriented Software Engineering: AUse Case Driven Approach”(Jacobson 1992)中的思想,在分析工作流我们进一步假设系统中存在三种类:边界类(Boundary Class)、控制类(Control Class)和实体类(Entity Class)。

三种分析类只是一种逻辑上的思考方式,如果你乐意,可以换成另外的思考方式。在设计工作流,三种分析类可以映射到任何实现架构,包括但不限于MVC、MVP、MVVM、六边形、洋葱型……甚至不做任何分割的“架构”。

在模型中,我们通过不同的构造型(Stereotype)来表达三种分析类,如图8-23。

图8-23 三种分析类的构造型

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

图8-24展示了三种分析类的责任、和用例的关系以及命名。

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

图8-24 分析类的责任、和用例的关系以及命名。

注意,图8-24中,"每个有接口的外系统映射一个边界类"里的"外系统"不仅仅包括系统执行者,还包括仅接受系统输出信息的外系统。

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

图8-25 外系统映射边界类,用例映射控制类

图8-25中,“时间”映射一个“时间接口”,“软件开发人员”映射一个“软件开发人员接口”,“助理”映射一个“助理接口”。还可以看到,“发送给开课通知”映射一个控制类“发送给开课通知控制”。

分析工作流的边界类不暗示任何实现方案。在总责任相等的前提下,它和实现的映射是多样的,可以用图形界面实现,也可以用非图形界面(包括文本、声音……)实现。

即使使用图形界面实现,也不能简单认为一个边界类对应一个窗体。一个边界类的责任可以拆解到多个窗体上,一个窗体也可以和多个外系统交互。如何组织这些责任,应该从外系统的角度来考虑,而不是从用例或实体类的角度来考虑。

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

图8-26 边界类责任的组织

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

如果某个外系统和系统的交互很多,对应边界类的责任可能会有很多。另一种做法是按"外系统+用例"的组合映射边界类,这样可以减少一个边界类上的操作个数。不过,这样的做法已经暗示“按用例来划分边界”,所以还是建议尽量保持一个外系统一个边界类,如果操作很多,可以将从外系统角度观察可能要分在一组的操作移到一起,EA等工具可以随意定制属性和操作的上下显示顺序。

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

1. 助理选择公开课,请求创建通知任务

2. 系统验证所选公开课适合创建通知任务

“助理”是执行者,映射一个边界类“助理接口”是可以的,但如果映射一个“助理”类,如图8-27,那就错了。

图8-27外系统不映射实体类

系统是否需要一个“助理”类,要看系统是否需要维护助理的信息。如果需要,会在某个用例规约的某个地方体现,例如,可能会有一个步骤:

7. 系统保存通知任务

绑定一个字段列表:

7. 通知任务=4+创建时间+创建人

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

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

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

4 乘客提供身份标识

5 系统验证身份标识合法

6 系统记录乘客信息和入厢时间

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

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

图8-28 “乘客接口”及其常见的实现

控制类是可选的,如果在分配责任时发现控制类只起到传递的作用,没有起到分解和分配的作用,那么就可以把控制类去掉。

图8-29展示了三种分析类之间的协作。

图8-29 三种分析类在系统中的协作

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

实体类可以按照它们之间的耦合程度组成若干聚合,类似于公司的部门。聚合之间还可以再组成更大的聚合,类似于大部门中有小部门。也有可能有的类既不聚合其他类,也不被其他类聚合,类似于自成一个部门。

控制类对象发送消息时,先发给聚合的整体对象,再由整体对象分配(可能还有分解)给聚合内的其他对象,如果聚合内的对象还聚合了更小的对象,还可以继续分配。最后,由边界类对象反馈信息,完成一个交互回合。

边界类与执行者、控制类与用例的映射关系很明显,所以识别边界类和控制类不需要太多思考。思考的主要工作量应该花在识别实体类上。一个用例需要哪些实体类协作实现、如何协作,一个实体类会参与哪些用例的实现,这是一个多对多的映射,需要由分析员的大脑决定哪种映射最好。

有的分析方法学如ICONIX提倡一种Robustness Diagram,认为可以通过它来帮助寻找类。开发人员一用确实感觉很舒服,噼里啪啦就发现好多类,有一种"我已经取得了不小成绩"的错觉,不过要是仔细看看,就知道"发现"的多是边界类、控制类。这些类用不着刻意去发现,只要按照图8-24的套路映射即可。最难的工作——寻找实体类以及它们之间的协作,Robustness Diagram却是寥寥带过。所以,本书不推荐开发人员额外花时间画RobustnessDiagram。

建模人员应该把精力放在识别实体类上。边界类、控制类直接按照上面的套路映射即可,甚至可以推迟到画分析序列图时再加上去。

8.2.2 在建模实体类之前-区分一些概念

对于一些用语,本书按以下定义使用。

领域模型:描述某个领域中的概念及概念之间关系的模型。

分析模型:从核心域视角描述的信息系统的模型。

核心域模型:等同于分析模型。

模型可以用各种表示法来表示,相应的图可以叫“领域类图”、“领域ER图”、“分析类图”、“分析状态机图”……。

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

画类图的工作,并非只是在软件开发的分析工作流中才会做。第7章“需求启发”中就提到,我们在研究资料的时候,可以通过画类图来整理领域的概念。整理领域概念时,有时还可以加上状态机图(但不会使用序列图,自行思考一下为什么)。即使不是为了开发信息系统,也可以通过这些手段来整理领域知识,帮助我们更快掌握。

软件开发也是一个领域,在阅读软件开发资料时,同样可以通过画类图、状态机图等来整理领域概念。

以阅读《软件方法》为例。在阅读时,看到如图8-30的内容,即《软件方法》第2版第2章第35页。我们用类图整理概念,可能会先得到图8-31。

图8-30 《软件方法》第2版第2章第35页

图8-31 整理领域概念得到初步的类图

如果建模技能掌握到位,可以把图8-31稍微改进一下,得到图8-32。

图8-32 改进后的类图

图8-32表达了《软件方法》这一部分内容的领域知识,或者说软件建模领域的一小部分领域知识,可以称为软件建模领域的领域模型。不管有没有打算用某个信息系统来封装这些领域知识,这些知识都是摆在那里的。

如果打算用某个信息系统(例如后文将提到的“发糕”智能建模工具)来封装图8-32模型的部分或全部知识,可能会得到图8-33。

图8-33 某软件系统的分析模型

可以看出,图8-33是图8-32的一部分。该软件系统(即“发糕”智能建模工具)不打算封装哪个组织是哪个组织的客户,哪个人员是哪个组织的开发人员……等知识。图8-33可以看作该软件系统的分析模型(核心域模型)

注意我们的用词——软件建模领域的领域模型该软件系统的分析模型(核心域模型)

图8-32、图8-33都可以称为软件建模领域的领域模型,因为它们确实描述了软件建模领域的某些概念及概念之间的关系。

图8-32不是“发糕”智能建模工具的分析模型,因为“发糕”不打算封装图8-32中的所有知识。

图8-33是用iOS上的“备忘录”系统画的,但图8-33不是“备忘录”系统的分析模型。“备忘录”系统的核心域概念应该是“笔”、“颜色”、“图片”等。

我们可以注意到,图8-31至图8-33都比较粗略,例如类没有属性,关联没有多重性等,但不妨碍对它们的定性。

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

8.2.3 如何提炼实体类和属性

上一节所举的识别类的例子,是以《软件方法》为素材来识别的,相当于在需求启发时通过类图来整理领域概念。

如果当前处在分析工作流,那么提炼类的素材应该是系统的需求规约,如果用本书的方法表达,就是系统用例规约。

(什么?没有需求规约能分析吗?当然不能。你可以随便找个素材来提炼类,但你提炼的类到底是不是目标系统将来需要维护的概念,是没法判断的,因为需求都没有弄清楚嘛。当然,如果你脑子里对于系统该干什么不该干什么清清楚楚,只不过没写出来,那也算是有需求规约了——不过你得确定你真的懂,而不是拿这个当遮羞布。)

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

图8-34 从用例规约识别类和属性

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

如果关系数据库建模技能掌握得好,得到的数据模型符合1NF、2NF和3NF,那么用关系数据库建模的思考方式得到的类图极有可能也是合格的。反过来,也有理由怀疑,自诩关系数据库建模能力强的人,如果类建模做得乱七八糟,估计也有吹牛的成分。

当然,我们画类图的目的不仅是为了得到关系数据库,面向对象和关系数据库(或任何的具体存储方式)也没有必然的绑定关系。任何系统都可以用面向对象的方式来构造,不管它用什么方式来持久存储对象。

例如,我们坐电梯上楼时,在电梯里按了按钮5。电梯到了5层,会把门打开。电梯肯定记住了某些东西才能这么做。可以认为它记住了一个整数5,代码会这样写:

int 目标楼层=5;

但是,这样的做法,背后的类型是int,这是基础设施领域的概念,不是电梯调度领域的概念,说明我们的抽象级别依然处在基础设施域,没有到达核心域。图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 Guideto 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 andpublic policy. But wait, we said, have you read the literature on differentialpricing, bundling, signaling, licensing, lock-in, or network economics?

我们一直在听人说,我们生活在“新经济”中。言下之意是需要一门“新经济学”,一套指导商业策略和公共政策的新原则。但我们说,等一下,你读过关于差别定价、捆绑、信号、许可、锁定或网络经济学的文献吗?

8.2.4.3 不要把“上下文”当作懒惰的遮羞布

“领域驱动设计”乱象中,和本节内容相关的一个现象是:把“限界上下文”当作遮羞布,闭门造车炮制“通用语言”。

例如,针对下面这段描述:

同样规格的联想ThinkPad X1 Carbon2021笔记本电脑,在淘宝的A店铺卖9999元,B店铺卖9888元,某单位从B店铺买了20台,贴上单位的条码编号,分配给员工使用。

有的人“哇,我发现(没错,他们不喜欢研读,喜欢“发现”)商品在不同的上下文有不同的含义!”,活生生地把领域建模搞成玄学。

其实,好好思考一下,或者看看前人的归纳,就知道这里面涉及到不同的概念,在模型中如实描述即可,如图8-37,不需要故弄玄虚。

图8-37 如实描述领域概念

在不同上下文中使用同一称呼,而且有不同含义,这种情况当然是存在的,但应该在调查和思考之后确认。要提防因为没有能力或者懒得去剖析背后的区别,就随便乱说——我的上下文中,鹿就是马的意思,马就是鹿的意思,我的上下文里的商品和你的上下文里的商品不同……

8.2.4.4 核心域透镜

在为了软件开发而建模时,建模人员可能会用自己熟悉的非核心域术语体系来代替不那么熟悉的核心域术语体系,还引以为豪。例如,面对一段集装箱领域装箱规则的描述,建模人员立即在大脑中把它转换成自己熟悉的概念:栈、链表、树……而且认为这是“透过现象看本质”,甚至宣称“我就是程序,程序就是我”!

Fred Brooks在《人月神话》中引用了James Coggins的一段话:

The problem is that programmers in O-Ohave been experimenting in incestuous applications and aiming low inabstraction, instead of high. For example, they have been building classes suchas linked-list or set instead of classes such as user-interface or radiationbeam 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等各种方式实现。它应该不带任何领域知识,因为一旦带有领域知识,就意味着一旦领域知识发生变化,它也要跟着变化。

在类的对象集合内唯一的、带有领域知识的“编码”属性,如订单的订单号,人员的身份证号等,则可以像其他属性一样,出现在领域模型中,需要的时候它的值可以变化。

在设计工作流时,直接把某个“编码”属性作为标识,也不是不可以,但是就要面临上面提到的风险。

本书作者的身份证号,最开始是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-44,类图上的字从最开始图8-42的28字增加到135字,但没有增加任何有价值的信息。

图8-44 废话刷工作量的结果

********************

“信息”、“数据”等也不是不可以作为类的名称。如果系统关注的焦点是"信息处理",处理的信息是什么内容无所谓,“信息”、"数据"也可以作为类的名称,但这个时候就不再有"人员"了,它们不在一个抽象级别。类图可能如图8-45。

图8-45 "信息"作为一个类的名称

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

但是,很多带有“单”字的称呼是可以去掉“单”字的,例如“申请单”、“入库单”等,类名保留“申请”、“入库”即可(啊?你说什么?你看到的各种纸质材料上都有“单”字?)。

8.2.4.6 关于命名所用的语言

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

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

以前经常会考虑转换到编程语言时需要改名的问题。在设计工作流,如果我们使用的编程语言只能用英文命名类、属性、操作等——更严谨的说法应该是编译器广泛支持的字符集比较小,那么还需要一个对编程语言合法的名字。建模工具例如EA,一般会提供别名(Alias),真实名称用编译器支持的字符集,再加一个别名用于显示。

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

其他如在文本编辑器中输标点符号时切换输入法太麻烦、版式不好看等问题,在画图建模中时影响很小。

**以上所说仅是针对目标系统的核心域元素的命名,不涉及非核心域部分。**非核心域部分内容,和计算机和软件领域有关。这方面的“外语”,不管是计算机和软件各个概念,还是编程语言的关键字,要求软件人员熟练掌握是理所应当的。

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

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

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

英文命名用单数。

如图8-46。"顾客"的实例是一名顾客,"顾客们"的实例是什么?一个集合?

图8-46 英文名用单数

设计工作流中的数据库表名也应该用单数。有一些开发人员习惯在最后加s,甚至有一些框架在类转换表时,直接就在名称后面加上s,理由是表里有很多行。

这个问题和上面提到的类名称后面加个"类"字是一样的,都是一种冗余。“类”、“表"的概念已经隐含了"多个对象”、“多行"的意思。说"我是一个类,我的名字叫顾客"就够了,不需要说"我是一个类,我的名字叫顾客类"或者"我是一个表,我的名字叫顾客们”。

而且,复数未必是加s,还得费心思去想正确的复数形式,何必呢?

8.2.5 审查类和属性

8.2.5.1 属性直接描述类

类和属性连在一起说"类的属性",应该能直接说得通,否则类和属性的搭配是不合适的。这个时候应该找到或建立合适的类,把该属性移进去。“属性要直接描述类”这个要求和关系数据库的第三范式“任何非主属性不依赖于其它非主属性”相似。

例如图8-47,“联系人的组织名称”中间隔了个“组织”,不能直接说通,需要添加一个“组织”类,把“名称”挪过去。

图8-47 属性要能直接描述类

在缺少抽象的“照猫画虎”式建模中,这种错误比较常见。例如,一张工作居住证上确实有该人员聘用单位名称,于是,建模人员对照工作居住证,一项一项把它搬到类图上的类中。

如果确定每个联系人只就职于一个组织,而且系统只关注组织的名称,可以将“名称”合并到“联系人”成为一个属性“组织名称”,如图8-48。不过,如果以上的假设发生变化,这样的做法应变成本很高。

图8-48 特定条件下可以简化

或者可以这样归纳:如果A和B的关联上B的多重性为1,而且B只有一个属性,那么可以把B合并进A,如图8-49。

图8-49 特定条件下合并类

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

图8-50 不需要“编码”作为“外键”

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

图8-51 不需要“ID”作为“外键”

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

缺少抽象能力的建模人员,经常会直接把手上素材的信息,一一对应地映射为类和属性,就会导致违反本小节所说的要点**。**

例如,建模人员对照着一张土地登记卡,直接把它映射成类,照抄土地登记卡的每一栏作为属性,如图8-52。

图8-52 错误:直接把报表照搬成类

土地登记卡只是一个报表,或者说一个视图,把更为本质的多个实体类的一些属性组织在一起。碰到这种情况,建模人员可以用本条检验规则来深入思考背后更本质的概念。

8.2.5.2 不存在本领域内可分解的属性

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

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

图8-53 String可以留在类中

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

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

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

图8-54 组织需要分离出来

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

在EA中,把类的语言设成none,在属性的类型选择中,就看到UML定义的原生类型,如图8-55。

图8-55 UML定义的原生类型

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

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

这时可以:

(1)把“手机”属性留在“人员”类中,多重性设为多,如图8-56。

图8-56 把属性的多重性设为多

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

(2)更推荐的做法是,把该属性分离出去,如图8-57。

图8-57 分离多值属性

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

图8-58 当需要关注更多的知识时,类图发生变化

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

以下做法是不好的:

(1)在“人员”类中放上多个属性“手机1”、“手机2”、“手机3”……,如图8-59。

图8-59 错误:放上多个属性

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

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

如果这样的做法是好的,那不如更进一步。各个属性也不用分了,就一个字符串。还可以再进一步,类也不用分了,也串在一起……持久存储或网络传输时的序列化不就是这样干的吗?

根源就在于软件是人做的,人脑的容量和运行速度有限,否则就不用理得那么清楚了,直接对计算机下二进制指令不好吗?

8.2.5.4 属性对所有对象都有意义

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

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

是不是有人有姓名——是。

是不是所有人都应该有姓名——是。

是不是有人有▲▲——是。

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

是不是有人有〇〇——是。

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

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

图8-60 分解只属于部分对象的属性到子类

扫码或访问http://www.umlchina.com/book/quiz8_1_2.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)


如果想早点了解后续内容,也可以带您的项目来参加这个训练,由我在课上剖析项目:

软件方法(下)分析和设计第8章连载[20210816更新]分析 之 分析类图——知识篇相关推荐

  1. 软件方法(下)分析和设计第8章连载[20210723更新]

    墙上挂了根长藤,长藤上面挂铜铃 <长藤挂铜铃>:词:元庸,曲:梅翁(姚敏),唱:逸敏,1959 您在阅读<软件方法>时如果发现错误,欢迎通过微信umlchina2告知.如果作者 ...

  2. 软件方法(下)分析和设计第8章连载[20210518更新]

    墙上挂了根长藤,长藤上面挂铜铃 <长藤挂铜铃>:词:元庸,曲:梅翁(姚敏),唱:逸敏,1959 您在阅读<软件方法>时如果发现错误,欢迎通过微信umlchina2告知.如果作者 ...

  3. [20210429更新]软件方法(下)分析和设计 第8章 连载

    墙上挂了根长藤,长藤上面挂铜铃 <长藤挂铜铃>:词:元庸,曲:梅翁(姚敏),唱:逸敏,1959 您在阅读<软件方法>时如果发现错误,欢迎通过微信umlchina2告知.如果作者 ...

  4. 软件方法(下)分析和设计第8章分析 之 分析类图——知识篇Part02(202204更新)

    DDD领域驱动设计批评-文集-点击查看>> <软件方法>强化自测题集-点击查看>>**** 8.1.8 伪创新 有的人(国内国外都有)没有掌握相应技能,也不愿意认真 ...

  5. 软件方法(下)分析和设计第8章分析 之 分析类图——知识篇Part01(202204更新)

    *为避免单篇文章篇幅过长,分成多篇以合集发布* _墙上挂了根长藤,长藤上面挂铜铃 _ <长藤挂铜铃>:词:元庸,曲:梅翁(姚敏),唱:逸敏,1959 可到此处下载<软件方法>( ...

  6. 软件方法(下)第8章分析之分析类图—知识篇Part09-审查类和属性1

    可到此处下载<软件方法>(下)目前公开的最新pdf版本: http://www.umlchina.com/book/softmeth2.pdf 8.2.5 审查类和属性 8.2.5.1 属 ...

  7. 软件方法(下)分析和设计第9章分析 之 分析类图——案例篇(20211228更新)

    软件方法(下)分析和设计第8章分析 之 分析类图--知识篇(20211227更新) 鸳鸯扣,宜结不宜解 <身似摇红烛影>,词:唐涤生,曲:王粤生,唱:红线女,1954 可到此处下载本文档最 ...

  8. 走向.NET架构设计—第五章—业务层模式,原则,实践(后篇)

    走向.NET架构设计-第五章-业务层模式,原则,实践(后篇) 前言:在上一篇文章中,讲述了一些设计模式的使用,本篇首先接着介绍还没有讲完的一些设计模式,然后再讲述一些架构模式中的超类模式,作为本篇的结 ...

  9. 系统和系统实例-软件方法(下)第9章分析类图案例篇Part07

    DDD领域驱动设计批评文集>> <软件方法>强化自测题集>> <软件方法>各章合集>> 9.2.2.2 系统 广义上,从整个宇宙,到上一小节 ...

最新文章

  1. 浅谈嵌套命名实体识别(Nested NER)
  2. HDU 1317 XYZZY(floyd+bellman_ford判环)
  3. 【强化学习】Actor Critic原理
  4. 高级cmd攻击命令_一步一步学习DVWA渗透测试(Command Injection命令行注入)-第七次课...
  5. Servelt中的ServletContext对象
  6. MySQL 5.6和MySQL 5.7的区别
  7. docker的源配置
  8. Python学习系列----第六章 数据结构
  9. C-Free 5.0注册码分享
  10. 会议论文集出版地总结_codestorm_新浪博客
  11. android中的对话框,Android中的对话框
  12. java程序员平均工资多少,详细说明
  13. Google地图十年 从流量平平到用户十亿的背后
  14. 手机布局rem的使用(rem)
  15. 王者荣耀与英雄联盟:如何解决玩家骂人的问题?
  16. 亿图脑图MindMaster思维导图及亿图图示会员-骨折啦
  17. 云麦体脂秤华为体脂秤_如果是一个真正的体脂秤就好了:华为智能体脂秤
  18. 数据库的故障及其恢复策略
  19. 2022年C等级考试九月二级真题E:反反复复
  20. 文秘专业计算机基础试卷,秘书题库:文秘类专业基础理论课试卷上2

热门文章

  1. npm包--rimraf
  2. 报错Takes no arguements解决办法
  3. 结束任务管理器电脑黑屏怎么办
  4. numpy array 升维
  5. 请问苹果x是如何建文件夹_苹果x怎么创建文件夹相关阅读-苹果x怎么创建文件夹文章阅读-123文学网...
  6. 3. 自定义Java编译时注解处理器
  7. 开源物业管理系统的对比
  8. ​数字经济指数合集:各省、城市数字经济指数面板数据
  9. 解决Macbook Pro蓝牙不可用问题
  10. ps保存图片logo背景透明