最近有小伙伴在Thoughtworks DDD社区中提起了有关哲学的话题,这在我这个哲学民科(下文简称“哲民”)的心中激起了阵阵涟漪。
据小道消息,Eric Evans认为DDD不是一种方法学,而是一种软件开发的思想和哲学。言下之意,“方法学”把DDD给说小了。好吧,那咱就顺着艾老师的意思,看看DDD和哲学能碰出什么火花来。
由于出自哲民之手,本文穿凿附会有余,科学严谨不足,仅供大家茶余饭后消愁解闷。欢迎提出宝贵意见,如有任何不同观点,以您的为准 。
好吧,让我们从头开始。
引子:从本体论到认识论
最早,无论中外的哲学家,首先思考的问题是世界的本原(希腊文:ἀρχή)是什么。德谟克利特认为是“原子”,老子认为是“道”,基督教认为是上帝,唯物主义认为是“物质”等等。研究这些问题的学问,后世称为“本体论”(Ontology)。
在争论世界本原的过程中,人们不禁会问这样的问题:我们怎么知道我们认为的“本原”就是正确的?推而广之,我们怎么知道我们所知道的东西就是正确的?还可以进一步追问:世界是可知的吗?人类认知的边界是什么?知识是如何获得的?知识是先天的还是后天的?等等。研究这些问题的哲学分支就是“认识论”(Epistemology)。认识论是关于知识的学问,是对“思考”进行思考。
对“思考”的思考,引发了逻辑学的创立。最早系统地研究逻辑学的是亚里士多德老师。为了研究逻辑,首先就要搞清“概念”,为了搞清概念,亚老师讨论了“范畴论”(见《范畴篇》)。
“模型驱动设计”与亚里士多德的“范畴论”
为避免混淆,先说明一下,“范畴论”有两种不同的含义:一种是数学上的(Category theory),一种是哲学上的(Categoris)。本文说的是后者。
什么是“范畴”呢?我尝试给出一个通俗的(也可能是庸俗的)解释。亚老师的逻辑学是从对语言的研究展开的。老先生尝试对人们语言中的所有概念进行归类。比如把人归为动物,动物归为生物等等,一级一级往上归。最后归到不能再归了,发现归成了十大类(Categoris)。Categories本来可以翻译为“类”。不过当年中国的翻译家借用《尚书》中“洪范九畴”的说法,译成了“范畴”。这当然显得更有学问喽。
亚里士多德的十个范畴包括:
- 实体(ουσία, Substance):例如人、桌子
- 数量(ποσόν, Quantity):例如10公斤、5公里
- 性质(ποιόν, Qualification or quality):例如红的、热的
- 关系(προς τι, Relative):例如甲比乙大、甲认识乙
- 场所(που, Where or place):例如办公室、学校
- 时间(πότε, When or time):例如5点半
- 姿势(κείσθαι, Being-in-a-position):例如站着、坐着
- 状态(έχειν, Having or state):例如穿着衣服、拿着武器
- 主动(ποιείν, Doing or action):例如打开电视
- 被动(πάσχειν, Being affected or affection):例如电视被打开
(上面的括号中注了希腊文和英文,维基百科抄的啦。后面的例子有些是亚老师给的,有些是我加的)
DDD领域建模中的概念可以和亚老师的十范畴建立起对应关系,如下表:
范畴 | 领域建模 | 说明 |
---|---|---|
实体 | 实体 | 范畴论和DDD所说的实体的英文单词不同,不过意义相近 |
数量 | 定量的属性 | 属性的类型通常是值对象 |
性质 | 定性的属性 | 属性的类型通常是值对象 |
关系 | 返回与其他对象比较结果的方法;或表达与其他对象的关联关系 | 表达“关联”时,可看做类型为其他实体的属性 |
场所 | 表场所的属性 | 属性的类型通常是值对象 |
时间 | 表时间的属性 | 属性的类型通常是值对象 |
姿势 | 表状态的属性 | 属性的类型通常是值对象 |
状态 | 表状态的属性 | 属性的类型通常是值对象 |
主动 | 调用其他对象的方法 | 使其他对象的属性改变 |
被动 | 本对象的方法被其他对象调用 | 使自己的属性改变 |
对上表还有几点说明:
- DDD强调了领域对象可以分为“实体”和“值对象”两类。值对象常用于表达实体的属性。这一区别的哲学意义,后面还会讨论。
- 亚老师还把实体分成了第一实体和第二实体两类。第二实体其实就是今天说的类,第一实体就是类的实例。
- 对象一般都是“被动”的。也就是绝大部分对象不会主动做任何事情。只有在自己的方法被别的对象调用的时候,才会“被动”地执行。其实主动的对象也有,术语就叫做“主动对象”,不过很少用。
从现在的观点来看,亚老师的十范畴来源于经验,本身并不严密。上表中对应关系的细节也可再行斟酌。关键是总体来看,这种对应关系到底仅仅是牵强附会,还是有必然的内在联系呢?答案是后者。
DDD的核心是建立领域模型。艾老师的原书(以下简称《DDD》)将以领域模型为核心的开发方法称为“模型驱动设计”(《DDD》第4至第7章)。
领域模型是现实世界的抽象。建立领域模型的过程就是认识现实世界中的概念(领域概念或者说业务概念),进而将认识的结果用一定的方式可视化的过程。从哲学的角度,就是认识论的问题。由于是对领域概念的认识,因此Martin Fowler在《分析模式》中将领域模型称为“概念模型”。对概念的归类,形成了“范畴”。
面向对象设计和编程的典型特征是:封装、继承和多态。上升到“方法学”层面后,“继承”(inheritance)扩展为“泛化”(generalization)。继承是泛化的特例。
当人们认识一个实体的时候(例如一匹马、一个人),只会关心实体的外在表现,也就是属性和行为,而不会关心实体内部的结构。也就是说我们一般不会为了认识一个新朋友,而把这位仁兄切开看看他内脏的构造。因此实体的内部信息是隐藏起来的,这就是“封装”。
在特定场景下,客户可以分成两类:个人客户和团体客户。分类关系就是“泛化”。其中“客户”是父类型(Super Type),“个人客户”和“团体客户”是子类型(Sub Type)。不同子类型的同名行为(例如更改联系方式)可能具有不同的逻辑和数据,这就是“多态”。这里按照《分析模式》的习惯,在分析(概念模型)层面使用术语“类型(Type)”;在设计层面使用术语“类(Class)”。类是类型的实现。
类型可以看做实例的集合。关联(association)是实例之间的关系;而泛化是集合之间的关系。父类型是全集,子类型是子集。子集又可以有交集,从而形成《分析模式》中所说的“多重分类”。分析层面的多重分类无法直接用主流编程语言的“继承”来实现,而要在设计上做些特殊处理,这是后话了。
分类或者说范畴(Categories)是人理解概念的基本方法,因而也是面向对象方法学的基本方法。DDD正是建立在面向对象方法学之上的。
总之,开发软件时,先基于领域知识建立领域模型,再根据领域模型编写代码和设计数据库。这种“模型驱动设计”有其认识论的根源,符合人的认知规律。
我们真的是在对“客观世界”建模吗?
上文说到为了开发软件,我们首先要对客观世界进行建模。
对于我们这些从小受到辩证唯物主义教育的小伙伴来说,这没什么问题。但有些哲学家可未必同意。
首先,佛教哲学认为根本就不存在“客观世界”,一切都是空幻不实的。印度教也认为我们不过是生活在梵天的一场梦中。
好吧,就算存在客观世界,我们能够认识它吗?
柏拉图认为不能。在他的世界观中,真实的世界是“理念”的世界,我们所能认识的世界只是理念世界的摹本,而真实的世界是我们无法认识的。类似的,康德将独立于人的意识而存在的事物称为“物自体”,虽然存在,但无法认识。
而唯物主义者认为能。认识的方法是通过“实践 – 理论 - 再实践”的无止境的循环。关于这一点,对中国人来说最亲民的阐述应该在毛老师的《实践论》中吧。
电影《黑客帝国》对上述问题进行了形象的解释。如果人真的生活在“母体”中,根本无法判断我们所在的世界是否真实。我们怎么知道我们现在不是生活在“母体”中呢?我觉得甚嚣尘上的“元宇宙”是黑客帝国的低配版,所以这东西总让人感到一丝不安。
好在这些哲学思辨并不会对程序员和领域专家们的生活造成太大影响。我们只需理解《分析模式》中所说的,我们实际上不是对客观世界本身建模,而是对我们所认识到的东西,也就是说客观世界在我们心灵中的映像建模(大意,没查书)。至于我们所认识到的东西和客观世界(如果有的话)的关系就留给哲学家们讨论吧。
不过上述论断对建模还是有一个细微但重要的影响。例如对一个事件的记录,常常要记录两个时间:一个是录入计算机的“登记时间”;另一个是实际的“发生时间”。登记时间是不变的,而发生时间可能改变,这就是因为从哲学上说,我们并不知道当前所认识的“发生时间”是否就是客观上真实的时间。因此总有机会由于对这一时间的新的认识而改变。很多系统就是由于没有将这两个时间区分清楚,从而造成一些微妙的问题,例如统计数据的时间口径不一致,造成数据“对不上”。
值对象与第二信号系统
在DDD中有几个模式总是不太好讲,其中之一是“值对象(Value Objects)”。
值对象的例子倒是好举。数值(整数、实数等)、数量(包含数值与单位两个属性,例如长度)、姓名、字符串、日期、地址、实体的状态、实体在某时刻的快照等,常常都建模成值对象。
捎带说一句,由于历史的原因,有些人会将值对象(VO)与数据传输对象(DTO)相混淆。两者其实是完全不同的概念。
除了举例说明的方式,从理论上说,值对象和实体的确切区别到底在哪呢?按照《DDD》第5.3节的说法,从概念层面,大约有以下几点:
- 值对象在概念上不具有标识符(identity),而实体有;
- 实体靠标识符来判断同一性,值对象靠值本身判断同一性;
- 值对象常用于描述实体的性质(characteristics);
- 值对象是不可变的,实体可变。
就这?我不知道大家读到《DDD》中这些论述的时候是什么感觉,反正我觉得是“你不说我还明白,你越说我越糊涂”。为什么呀?为什么实体和值对象有这些区别呀?直到我从哲学层面进行思考。
实体是我们能够感受到的客观存在的外部事物。比如说一个苹果,人看到,是一个苹果。狗看到,也是一个苹果,虽然狗不知道这个东西叫“苹果”。从这个角度来说,对实体(至少是自然界中的实体)的直觉认识能力,是人和动物共有的。
值对象则不然。我们从5个苹果、5只山羊、5棵树中,抽象出“5”这个概念,用于描述数量。“5”是整数,整数是值对象的类型,“5”是值对象的实例。
我们发明了字母,并约定用字符串“apple”来指称“苹果”这个概念。字符串是值对象的类型,“apple”是这一类型的实例。
从唯物主义的认识论来说,值对象是人类为了认识和描述事物的属性,在头脑中经过抽象思维创造出来的概念。这些概念在自然界中是不存在的。只有智慧生物才有这种抽象能力。狗的头脑中不会有“5”、“apple”的概念。
无独有偶,诺贝尔奖得主巴浦洛夫提出了“第一信号系统”与“第二信号系统”学说。该学说认为,大脑皮质最基本的活动是信号活动。根据条件刺激的不同区分为两大类:一类是现实的具体的刺激,如声、光、电、味等刺激,称为第一信号;另一类是抽象的刺激,即语言文字,称为第二信号。对第一信号发生反应的皮质机能系统,叫第一信号系统,是动物和人共有的。对第二信号发生反应的皮质机能系统,叫第二信号系统,是人类所特有的,是在婴儿个体发育过程中逐渐形成的。通过第二信号系统的活动,能够对现实进行概括,出现了抽象思维,并形成概念、进行推理,不断扩大认识能力。人对世界的认识是通过第一信号系统和第二信号系统的共同作用实现的。
联系到DDD,大体上可以说,对“实体”的认识主要通过第一信号系统,而对值对象的认识则通过第二信号系统。这是科学对哲学的佐证。
值对象的本质决定了它在概念上具有不变性和唯一性。
关于不变性,举例来说,“5”这个值对象如果“变”成了“6”,那就已经是另一个值对象了,“5”本身是不会改变的。再详细点说:一个小朋友5岁。“小朋友”是实体,“年龄”是这个实体的一个属性。属性的类型是整数,属性的值是“5”。“整数”是值对象的类型,“5”是该类型的一个实例。如果小朋友的年龄变成了6岁。那么“6”就是另外一个值对象了。这里发生变化的是“年龄”这个属性(从而实体也发生了变化),而不是“5”这个值对象。这就是值对象在概念上的不变性。这里的不变并不是由于任何外在的约束,而是值对象的本质决定的。“值对象的变化”本身就是一个没有意义的概念。
《DDD》一书中提到,有些情况下值对象也是会改变的。这种说法其实是混淆了值对象的概念层面(或者说分析层面)和实现层面(或者说设计层面)。正确的说法应该是,在概念层面,值对象总是不变的。但在实现层面,有时考虑到性能的原因,可以将对象的内部实现为可变的,但对象的外在表现(接口)仍然是不变的。
另一方面,《分析模式》中指出,值对象在概念上总是唯一的。也就是说,“5”这个概念只有一个,全世界没有两个“5”。准确的说”两个5”这种说法同样是没有任何意义的。用咱们中国的俗话来说:一笔写不出两个“王”字。作为姓氏的“王”也是值对象。
虽然在概念上,值对象是唯一的,但在实现上,则可以采用唯一或不唯一的方式。
上面提到的实体例子都是自然界中存在的事物。但是企业应用中却常常要处理另外一类实体:组织、账户、合同等等。这些实体在自然界中是不存在的,而是人类社会生活中产生的,有些是有形的,有些是无形的。他们比自然界中的实体要抽象,又比值对象具体。参考《人类简史》中“想象的现实”这一说法,我们可以称这类实体为“想象的实体”。按照《人类简史》的猜测,正是由于智人(也就是我们)具有这种想象的能力,才使我们战胜了其他同样具有智慧的人种(例如尼安德特人),繁衍至今。
最后还有一个实践中的心得。虽然值对象确实是领域概念,但在研发人员和业务人员共同建模的过程中,我们却很少和业务人员提值对象,这一般也不会影响建模的效果。而如果强调值对象,反而会将业务人员搞糊涂。反之,我们会问业务人员,“这个信息会不会变化”、“需不需要为这个信息保存快照”等等,从而间接地识别值对象。
有些东西人们天天在使用,但如果要抽象到理论上去谈,一时又不容易理解。而即使不抽象到理论,也不会影响日常使用。比如说人们天天都会说话,说的话基本上都符合语法,但研究语法学的确实少数人。值对象也是同样的道理。
关于限界上下文、统一语言、模型演进等的一些心得,且听下回分解。
文/Thoughtworks 钟敬
原文链接:DDD的哲学:实体和值对象 - Thoughtworks洞见