节选自《软件方法(下)》。
8.2.8 评价DDD话语中的“值对象”
在识别类的时候,有的建模人员受到DDD话语体系的影响,会着急去分辨哪个类是实体(Entity),哪个类是值对象(Value Object),这是没有必要的,而且很容易成为遮掩无能的遮羞布。
8.2.8.1 历史回顾:不可变对象
1986年,Barbara Liskov和JohnGuttag在其讲述面向对象思想和CLU编程语言的书“Abstractionand 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 inProgram Development无中译本。2000年,这两位作者又出了一本Program Development in Java: Abstraction, Specification, andObject-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 SoftwareArchitecture基于Bertrand Meryer的思想,作者是ISE Eiffel 2.2开发环境的主要开发者和BON(Business ObjectNotation)的发明者。ISE Eiffel是InteractiveSoftware 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.8.2 历史回顾:值对象
Martin Fowler和Kendall Scott在“UMLDistilled”的第一版使用了“值对象(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 TransferObject),如图8-70。Martin Fowler在《企业应用架构模式》中讲述“值对象”模式时,提到了这一点。
图8-70 摘自《J2EE核心模式》,Alur D. 等 著,牛志奇 译,英文原版出版于2001年
******
《J2EE核心模式》(第2版)已不用“值对象”,改用“传输对象(Transfer Object)”。
******
“值对象”目前主要用在DDD话语体系中。您可以观察近年出版的书籍,里面提到“值对象”的地方,很可能在这个词的周围还会提到“实体”“领域驱动设计”“DDD”等。
也许有人会说“值对象”和“不可变对象”不是一回事。你看,名字都不一样嘛,说明侧重点不同。“不可变对象”可以有标识,Eric Evans甚至还说“值对象”可以改变属性值。
其实,相对于“值对象”的命名,“不可变对象”的命名更本质。我们更在意的是属性值是否可变,而不是有没有标识、如何判断相等。在8.2.8.4会进一步讲述。
8.2.8.3 回顾历史,警惕伪创新
翻出历史来,意思是说**“值对象”的概念不是Eric Evans发明的,也不是Eric Evans给起的名字。**
这一点并非所有人都了解,如图8-71中的表述。
图8-71 摘自《解构领域驱动设计》,张逸著,2021
******
顺便再吐槽一下,图8-71中“面向对象设计的基本原则,如信息专家模式”的表达是不严谨的,原则和模式不是一个级别的东西。
以面向对象来说,被归纳的“原则”的数量最多也就两位数,最出名的是所谓的SOLID,而“模式”的数量就多了去了。
GoF(1995)有23个模式;Kent Beck的SmalltalkBest 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.8.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 WithAda: 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)还提到“值对象”和“实体”的命名不符合二分法的问题,更推荐用“引用对象”和“值对象”。另一种划分就是前文所说的“不可变对象”和“可变对象”了。