22 泛化建模(上):领域知识更抽象怎么办?
你好,我是钟敬。
上节课,我们学习了“限定”技术。利用这个技术可以丰富模型的语义并简化关联。今天,我们要挑战领域建模中的另一个话题——泛化。
泛化是领域建模技能由初级水平迈向中、高级水平的门槛。也就是说,如果不懂泛化,那么你的领域建模水平就始终停留在中级以下。初步掌握了泛化,那么你的水平可能就能达到中级。把泛化玩得出神入化,那么大概就能达到高级水平了。
领域建模层面的泛化,大体上相当于面向对象设计中的继承和多态。如果你学习过“设计模式”的话,还记得刚开始的“痛苦”吗?其实,整本《设计模式》,无非是教你怎么灵活运用多态罢了。而在领域建模层面,与《设计模式》对应的就是《分析模式》。整本《分析模式》也无非是教你怎么成为使用泛化的高手。
我们现在要讲的内容,尽管达不到“分析模式”的高度,但是可以为你将来的学习打下基础,帮你在实际工作中解决一些相对复杂的问题了。
我安排了5节课的时间,带你分别由泛化的建模、泛化的数据库设计以及泛化的代码实现由浅入深地认识和理解泛化。
这节课会围绕着“在子项目上报工时”这个需求来展开讨论,演示泛化的建模方法。还是老办法,我作项目经理,你作架构师。
项目和子项目的“泛化”
我先来回顾一下需求:我们把业务由“零零后公司”扩展到了“九零后公司”,而“九零后公司”会把项目进一步分为子项目,然后在子项目上报工时。
先看看目前的模型是什么样(下图省去了和这节课无关的部分)。
根据需求,你第一步,先在模型里增加 子项目。
上图说明,一个 项目 可以没有 子项目,也可以有多个 子项目。不过,现在这张图里还没有说明可以在 子项目 上报 工时。
于是,你又在 工时记录 和 子项目 之间增加了一个一对多的关联。
增加了这个关联以后,确实可以说明在子项目上能登记工时了。但是我发现,在项目和子项目一端,两个多重性都是“1..1”,这似乎有问题呀?
你想了一下,发现确实有问题。就拿 项目 和 工时记录 的关联来说,“1..1”的后一个 1 没有问题,但前一个 1 说明了,一条 工时记录 必然关联一个 项目,这就不对了。因为,如果有的 工时记录 是报到 子项目 上的,那么就不会对应一个 项目 了。所以应该改为“0..1”才对。同样的道理,子项目一端也应该改为“0..1”。
于是,你把图改成下面这样。
现在的模型确实解决了刚才的问题。但是你很快又发现一个新问题。现在的模型意味着,一条 工时记录 可以既不关联一个 项目,也不关联一个 子项目;另一方面,一条 工时记录 可以既关联一个 项目,也关联一个 子项目。这显然是不对的。
正确的逻辑应该是: 一条工时记录要么关联一个项目,要么关联一个子项目,但不能同时关联两者,也不能两者都不关联。运用目前我们已经掌握的建模技术,只能用注释的形式增加一个约束。
于是,你把模型改成下面这样。
一般来说,如果能够用UML里自带的建模符号,比如说类、关联等等,那么应该优先使用建模符号,而利用注释和约束,其实是一种“补救”措施。那么,在UML里有没有一个符号,可以帮助我们表达这种约束呢?其实是有的,这就是我们今天想介绍的主角——“泛化”。
在正式引入这个符号之前,我先从另一个角度捋一下这个逻辑: 项目 和 子项目 都是可以报工时的“东西”。而一条 工时记录,必须要关联到一个这种“东西”上。
你问我:“那么这个‘东西’应该叫什么呢?”我说:“就叫‘工时项’吧。”有了这个名词以后,我们可以把这个逻辑再说一遍: 项目和子项目统称为“工时项”,一个工时项可以关联0到多条工时记录,一条工时记录必须关联且仅关联一个工时项。
根据这个表述,我接过键盘,通过一个新的符号,把模型改成了下面的样子。
图里面的空心三角,就表示前面说的“统称”的关系,由于 工时项 的含义比起 项目 和 子项目 要更“广泛”,所以这种关系在UML里叫做“泛化”(generalization)。这时候, 工时项 称为 项目 和 子项目 的“父类”,而 项目 和 子项目 称为 工时项 的“子类”。
也就是说,如果A类和B类可以统称为C类的话,C类和A、B两个类就具有泛化关系,其中C是父类,A和B是子类。泛化关系用一个空心箭头表示,由子类指向父类。
除了“统称”以外,泛化关系转换成自然语言,还可以有另外三种说法。
第一种说法是, 工时项 分成两类,一类是 项目,另一类是 子项目。也就是说,泛化表示的是一种“分类”关系。例如生物可以分成动物和植物,动物又可以分成哺乳动物和爬行动物等等。
第二种说法是,一个 项目 是一个 工时项,一个 子项目 也是一个 工时项。也就是所谓“是一个”(is-a)的关系。
第三种说法是, 项目 和 子项目 具有“共性”,也就是都能报工时。我们把这个共性的概念提取出来,称为 工时项。另一方面, 项目 和 子项目 又具有“个性”,也就是两者有差别,比如说,要为项目分配项目经理,而子项目则不需要。
“统称”“分类”“是一个”以及“共性/个性”这四种说法,虽然从表面上看不同,背后的含义却是完全一样的。在领域模型里,不论哪种说法,都可以用泛化来表达。总的来说, 泛化是一种强大的抽象机制,能够同时表现出不同对象间的共性和个性。
引入了 工时项 以后,你还注意到了模型图里其他几个变化。
首先,原来 工时记录 和 项目 还有 子项目 之间的两条关联不见了,取而代之的是, 工时记录 和 工时项 之间的一条一对多关联。这是因为, 父类的属性、规则、关联等等要素,都可以被子类继承。所以, 工时项 和 工时记录 之间有一对多的关联,也就意味着 工时项 的子类, 项目 和 子项目,都和 工时记录 有这种关联,所以就不需要重复了,重复画的话反而是错的。
其次,工时项一端的多重性变回了“1..1”,这也就表达了前面说的 一条工时记录必须关联且仅关联一个工时项 这一业务规则。
最后,我们刚刚为 工时记录 添加的那条约束被删除了,这是因为修改过的模型本身已经表达了这个约束。
普通工时项
通过引入泛化,我们解决了在子项目上报工时的需求。现在,我接着说说来自另一家企业“八零后公司”的需求。他们既可以按项目,也可以按子项目报工时,这些我们已经基本解决了。
但是,他们还有一些活动和具体项目无关,比如说:员工在项目不紧张的时候,可以利用空余时间学习;或者,管理者常常花大量时间在管理和沟通上,其中有一些时间很难归入某一个具体的项目。而这家公司希望也能为这些不属于任何项目的活动报工时。
你的第一个想法是:借用 项目 的概念。也就是说,建几个特殊的项目,就叫“学习时间”“管理时间”等等。然后大家就可以像普通项目那样报工时了。
但我对这个思路提出了质疑:“ 学习时间 和我们一般人理解的 项目,是两个不同的概念。如果我们借用了项目来表达学习时间,虽然在短期内解决了问题,但如果开了这个口子,那么混淆的概念就会越来越多,系统就会越来越难以理解和维护。”
事实上,直接“借用”系统中已经存在的机制,在短期内虽然达到了目的,但长期来看会导致概念混乱,这种做法是很多开发团队常见的错误。而错误的根源,就在于我们没有掌握一种优雅的方法,来处理不同概念的共性和个性。 学习时间 和 项目,既然都可以报工时,那么两者必然存在着共性。现在你应该已经想到了,这个共性就是前面已经抽象出来的 工时项。
于是你又问了我一个问题:“学习时间和管理时间,以及其他不算作项目的时间,有没有一个统一的称呼呢?”我说:“就叫 普通工时项 吧。”基于这个理解,你画出了下面的模型图。
你为 工时项 增加了一个 普通工时项 的子类。同时,你把 工时记录 的约束做了相应的修改,变成了“ 对于项目和子项目,员工只能在被分配的项目上报工时”以及“ 对于项目和子项目,只有在项目有效期内才能报工时”。
这时,我又问了你一个问题:“ 普通工时项 和项目管理已经没有关系了,还把它放在项目管理模块里,这合适吗?”你也感觉不太合适了。由于 普通工时项 是用于工时管理的,所以你把 普通工时项 移到了工时管理模块。
紧跟着我又问了一个问题:“ 工时项 还应该留在项目管理模块吗?”你思考了一下,说:“ 工时项 应该是服务于工时管理的,而不是项目管理的关注点,所以, 工时项 也应该移入工时管理。”我们对此达成了共识,于是你把模型改成下面的样子。
隐式概念的显式化
现在,今天的建模任务就基本完成了。我们再来回味一个有趣的问题。你可能已经注意到了,像“工时项”“普通工时项”“项目工时粒度”这些概念,在原来的业务术语里是没有的。在我们的抽象过程中才被发掘出来。
这其实是领域建模中的一种常见的现象。一些概念本来就隐含在业务逻辑内部,但没有经过抽象的话,往往不会感觉到,更不会有人来命名。
而一旦采用领域建模方法对领域知识进行抽象,这些概念就会暴露出来。这时候,开发人员和业务人员就要一起对这个抽象达成共识,并且给新发现的概念起一个恰当的名字,并把这个新词加入统一语言。这样,我们对领域知识的理解就又深化了一层。这就是《DDD》原书第 9 章中提到过的“隐式概念的显式化”。
总结
好,这节课的主要内容就讲到这,我们来总结一下。
今天我们讲了领域建模中一种比较高级的抽象机制——泛化。
泛化表示一种分类机制,能够对领域对象的共性和个性进行抽象。泛化用一个空心箭头表示。箭头由子类指向的是父类。子类会继承父类的属性、关联和逻辑。
假定C是父类,A和B是它的子类,那么对应到自然语言,可以有四种说法。
第一种,A和B统称为C,例如,甜粽子和咸粽子统称为粽子。
第二种,C可以分成A和B两类,例如,粽子可以分成甜粽子和咸粽子两类。
第三种,一个A是一个C,一个B也是一个C,例如,一个甜粽子是一个粽子,一个咸粽子也是一个粽子。
第四种,A和B具有共性,表示共性的概念称为C,例如,甜粽子和咸粽子具有共性,表示共性的概念称为粽子。
这四种说法表面上不同,实际上表达了完全相同的含义,都可以用同样的泛化关系来表示。
在建模的抽象过程中,我们还经常遇到隐式概念的显式化,从而引入新的词汇,丰富了统一语言,也深化了对领域知识的理解。
思考题
下面我给你留了两道思考题:
1.为什么 学习时间 和 管理时间 可以统称为 普通工时项,但没有构成泛化关系呢?
2.在你的实际项目中,可以找到泛化的例子吗?
好,今天的课程结束了,有什么问题欢迎在评论区留言,下节课,我们把模型做得更灵活一些,并且看一看,有些情况下是不是可以不使用泛化。