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.在你的实际项目中,可以找到泛化的例子吗?

好,今天的课程结束了,有什么问题欢迎在评论区留言,下节课,我们把模型做得更灵活一些,并且看一看,有些情况下是不是可以不使用泛化。

精选留言

  • 阿昕

    2023-02-08 08:06:52

    思考题,学习时间和管理时间虽然也可以进行分类,但是两者已经是原子属性了,没有必要进行抽象,所以不构成泛化关系
    作者回复

    是的

    2023-02-08 12:28:07

  • 子衿

    2023-01-31 09:20:50

    1. 因为学习时间和管理时间,只有共性没有个性部分,所以不必泛化
    作者回复

    这个理由也说得通

    2023-01-31 23:58:48

  • Geek_66158e

    2023-02-23 22:58:38

    问题一:因为从报工时的角度来看,已经不再关心普通工时项下的“个性”了,普通工时项这种“共性”对于报工时来说已经有意义的最小的粒度了。
    作者回复

    这个角度很棒

    2023-03-04 22:21:51

  • 6点无痛早起学习的和尚

    2023-02-15 08:35:55

    思考题 2:想到一个实际上的场景,但是拿捏不定是不是泛化
    父类:账户项
    子类:现金账户、备付金账户、待清算账户等等

    每个账户项都有余额属性:可用余额、冻结余额
    作者回复

    您列出了共性,但没有列出个性。如果有个性(不同的属性,不同的算法等),可以考虑用泛化,否则就不要用泛化了。

    2023-03-16 21:53:06

  • 赵晏龙

    2023-02-14 09:01:07

    老师你的总结是想引发粽子战争吗?

    1. 粽子……不是,学习时间和管理时间在业务需求上没有需要泛化的必要,泛化则变成了过度设计。

    2. 我现在满脑子粽子,等我缓缓
    作者回复

    幸亏没引起豆腐脑战争

    2023-02-15 08:04:52

  • 龙腾

    2023-02-02 12:43:05

    自然语言层面的子类,说男人和女人是人的子类,所以我们可以说“男人是人”。本文所说 工时项的子类有项目和子项目,但是我们显然不能说“项目是工时项”,而只能说“工时项是项目的一个属性,或者说项目包含工时项”。可以看出自然语言中的类型是实体之间的关系,而非属性之间的关系。而本文所说的“类”、“统称”其实是实体的公有属性:是属性和多个实体的关系,因此我觉得这种关系不应该用自然语言的“类”、“统称”来表述。
  • aoe

    2023-02-01 21:37:00

    思考题
    1. 「学习时间」和「管理时间」所有属性都相同,添加一个新属性「类型」就可以进行区分
    2. 封装第三方登录(微信、手机号、QQ);封装第三方支付(支付宝、微信、银联);封装不同等级用户提现时的手续费计算逻辑

    其他想法
    1.「工时记录」中需要新加一个属性「类别」,这样方便以后按类别查找记录(由泛化引发的一个坑)
    2. 今天刚好读到《DDD》第 9 章,趁热打铁,又多理解了一点
    3. 感觉「泛化」就像代码中的「接口」。写代码时「面向抽象编程」,建模时「面向抽象建模」。当一个程序员,不会「抽象」还真吃力啊!
    4. 「直接“借用”系统中已经存在的机制,在短期内虽然达到了目的,但长期来看会导致概念混乱,这种做法是很多开发团队常见的错误」这是我们项目中的常态,开发一时爽,后来很多人走了……
    5. 明白了 Predicate(谓词/谓语) 在编程中的意思:谓词是指计算结果为「真」或「假」的函数,并且可以使用操作符(如 AND 和 OR)把它们连接起来以表达更复杂的规则。
    —— 《领域驱动设计 软件核心复杂性应对之道》9.2.3 模式: SPECIFICATION
    钟老师还提到了一本书:《离散数学》,里面的一阶谓词理论,就是说这个
    作者回复

    没毛病,继续努力!

    2023-03-16 22:49:27

  • Michael

    2023-01-31 09:03:26

    期待老师讲泛化的数据库设计和代码实现,有些时候我在做项目的时候也觉得应该有泛化,但是只体现在模型设计上,但是不懂怎么体现在数据库设计和代码实现上,导致很多想法就此打住。
    作者回复

    泛化实现已经更新了,可以去看啦!

    2023-02-11 10:59:49

  • 小5

    2023-01-31 07:52:47

    回答问题1:因为两者有共性但是没有业务上关注的差异性
    作者回复

    是这么个意思

    2023-03-16 23:04:00

  • Sam_Deep_Thinking

    2024-11-21 09:04:25

    看了这篇文章后,对抽象的概念的理解,更加深刻了,感谢作者,写的真的非常细心和用心,这个DDD的实战专栏,堪称是极客时间里的【经典专栏】。

    有一定技术积累和工作经验的人,看这个专栏会非常的爽,偶尔还会有任督二脉被打通的感觉。再次感谢作者。
    作者回复

    感谢认可,共同进步 :)

    2024-11-25 10:45:37

  • 刘毅

    2023-12-21 13:33:38

    钟老师您好,结合您前面讲的第5节中组织与组织类别的抽象过程以及java面向对象思想,我的理解如下,不知道是否正确。

    值对象(个性)->实体(共性)与子实体(个性)->父实体(共性)两种机制都能够同时表现出不同实体间的共性和个性,两者主要的区别是个性部分在当前业务领域内是否复杂。
    若个性部分在当前业务领域内比较复杂(具有自己的标识、属性或方法等),则可用子实体实现;若个性部分在当前业务领域中比较简单(如只是简单划分几个枚举值),则可用值对象实现。
    作者回复

    是的

    2024-02-17 16:15:58

  • Geek1560

    2023-10-26 20:45:14

    工时项作为项目和子项目的泛化有点怪怪的,仅仅因为它们有个很小的相同点是工时就做这种抽象是不是不太合适?项目和子项目的大部分信息应该都是相同的,而不仅仅是工时
    作者回复

    项目和子项目作为工时项的共性,对于登记工时而言,是重要的。是否“很小”取决于主观的权衡。项目和子项目抽出更多的共性也是可以的,关键看用在哪里。有用就抽,否则没必要仅仅因为相似而抽。

    2023-11-04 11:57:16

  • Geek_4e4ec2

    2023-03-30 09:39:03

    有个问题,将工时项移到工时管理后,项目实体岂不是要继承或实现工时项,但它们是在两个上下文里,相当于一个上下文里直接引用了另一个上下文中的领域对象,这样也没问题?
    作者回复

    到这节课为止,还没有分上下文(只分类模块),所以不成问题。在第三个迭代分上下文以后(29课),会把这个问题解决掉。

    2023-03-30 15:27:15

  • Y024

    2023-02-17 08:39:29

    “学习时间”和“管理时间”,我们是作为一个灵活工时项目来处理。
    作者回复

    可以这么理解。两者之间除了名字不同,没有其他个性了。

    2023-03-15 09:23:17

  • 树袋熊🐨

    2023-02-04 20:45:00

    项目和子项目是不是可以改为:项目、子项目、普通项目,子项目和普通项目是项目的泛化?在该章节的项目和子项目应该有很多共性存在,应该进行一次抽象处理。
    作者回复

    可以的

    2023-02-06 08:06:46

  • 支离书

    2023-02-03 18:37:06

    文章中项目和子项目抽象出来一个“工时项”,是因为有工时记录的需求,如果再加个给项目和子项目拆分任务但学习时间管理时间不需要拆分任务的需求,那就不能复用“工时项”这个抽象了吧?一是名称不合适,二是普通工时项不需要拆分任务了。这种情况再给项目子项目单独抽象出一个“任务项”的抽象吗?

    这个问题群里有同学也问到了,看老师回复说详见泛化第三节课。坐等更新。
  • Jxin

    2023-02-03 09:05:23

    课后题,个人理解
    1.本来就是泛化关系,只是在模型上要不要体现出来而已。 哪怕这几个类型的结构一模一样,也可能会标识泛化关系。是否标识的判断依据是这几类模型有没有自己私有的业务流程,如果有,那么展示泛化关系,在具体业务流程用子类的模型来构建类关系图。
    两天基本思路,怎么画更不需要解释就怎么画,能降低认知复杂度就降低。
    作者回复

    这么理解也可以。除了不同的业务流程以外,不同的属性,不同的算法都会导致泛化。

    2023-03-16 22:40:52

  • tt

    2023-01-31 21:46:40

    是不是因为学习时间和管理时间就是普通工时项,他们之间没有不同,只是名字、使用场景不同,可以用给普通工时项增加一个“类型”的枚举属性,就可以完全编程不同的实例了。
    作者回复

    基本是这个意思,后面的课也会提到。

    2023-01-31 23:50:27

  • escray

    2023-01-31 07:54:08

    终于追上了进度。

    泛化 generalization 确实看上去很高阶,可以转化为自然语言中的分类、是一个(is-a)、共性三种说法。

    空心三角(箭头)加实线表示”泛化“ generalization,继承关系,指向基类
    空心箭头加虚线表示”实现“ Realization,类对接口的实现,指向接口
    空心菱形表示”聚合“ aggregation, has-a,两个对象一般不平等
    实心菱形表示”组合“ Composition, contains-a
    虚线箭头表示”依赖“ Dependency,使用,偶然、临时、弱关系
    实现箭头表示”关联“ Association,两个对象平等,强依赖,长期、平等

    来自 https://www.cnblogs.com/duanxz/archive/2012/06/13/2547801.html

    抽取工时管理模块,以及将普通工时项、工时项、工时记录迁移到工时管理模块的过程,确实很精彩,将隐式概念显示化,这个可能才是领域建模方法的精粹。

    思考题:

    1. 学习时间和管理时间也可以单独拿出来,但是在目前业务上下文中,没有必要
    2. 在之前的项目中,有用到”泛化“的东西,但是并没有抽象出来。比如对于文字材料、视频、音频都可以看做素材,但是并没有泛化出”素材“的概念。
    作者回复

    在概念层面的领域模型,一般不用“空心箭头加虚线”,在实现层面可以用。
    只要是实线,不论有没有箭头,都表示“关联”
    其他理解都很到位。
    思考题回答也不错。


    2023-03-16 23:08:34