
第1章 领域驱动设计基础
1.1 领域驱动设计的起源与发展
2004年,Eric Evans完成了《Domain-Driven Design Tackling Complexity in the Heart of Software》一书,提出了一套针对业务领域建模的方法论和思想——领域驱动设计,简称DDD。DDD可以说是一种艺术性的技术,是一种复杂软件如何快速应对各种变化的解决之道。
本章主要针对领域驱动设计的起源、发展、特点、难点,以及应用场景进行概述,让读者先从外部视角打量“领域驱动设计”,然后再逐渐深入其内部。这种从事物内外两个视角分析事物的方式是一套基本的逻辑分析和设计方法,领域驱动设计方法论总体上也是根据这种思路进行设计的。
1.1.1 程序员为难之处
在分工日益精细的今天,程序员或软件工程师被看作软件的构建者。同为构建,建筑工程在施工队进场开始构造之前,各种工程图样和材料都已经非常精确地准备到位,但是,在软件中没有足够时间收集所有的需求,即使收集了,需求也不是从创建软件角度去描述的。
需求表达往往带有个人知识“偏见”和逻辑漏洞。需求文档很可能是一位管理人员写的,因为他拥有丰富的专业领域知识,然而成为管理人员后,在长期和管理阶层的交互沟通中,他可能已经逐渐改变了原有的专业视角;很多产品经理是从程序员转行而来的,但是在创意与严谨逻辑的不断碰撞中,他们往往选择了创意,以表明其能胜任产品经理,当这些创意传达给程序员予以落实时,其中极可能存在巨大的逻辑漏洞。
没有足够时间收集需求,但快速行动的重要性已经扎根于程序员心中。没有足够的信息来构建软件,但是项目却有期限,因此程序员只能开始进行创造性的假设。程序员学会填补产品经理留下的空白,只是为了保持项目不断推进。当然,产品经理或客户会经常改变主意,这意味着程序员对留白处的创造性假设经常夭折,这让程序员很为难。
笔者曾经参与过一个大型项目,需求文档非常详细,而且团队从事这个行业已经有十多年,准备时间相当充裕,代码调试都已经准备就绪,只等客户一声令下,就进场实施项目。但是,问题就出在进场实施中。每个人都以为准备得非常充分,有些项目小组已经开始为下一个项目做准备了,但是客户发现这个项目根本无法运行。这里的运行不是程序无法运行,而是项目与客户的要求还差得很远。项目代码还处于Demo阶段,很多数据需要从其他系统获取或衔接,而这些接口只能在现场才有机会真正使用。尽管之前已经将接口有关代码准备得很充分了,但是真正与其他系统衔接起来时,问题非常多——项目组原来将业务的上下文场景设想得太简单了。
缺乏项目现场的业务上下文背景,需求文档只能抽象地表达一种通用业务要求,这种通用性其实忽视了很多特殊上下文,从而让软件系统变得像演示Demo系统一样简单。而程序员自身对领域知识的理解也是有限的,即使是开发过老版本的老员工,他们的理解也是片面的。有人说:程序员其实不是在编写代码,而是在摸索业务领域知识。
项目实际需求与程序员的理解之间存在客观的落差,但是程序员负责最终完成项目,如果这种落差没有被注意或发现,就只能由程序员自己创造。这种创造是在没有指导或指示的情况下进行的,当完成的项目放在客户面前时,客户才发现这不是自己想要的,双方很容易陷入尴尬甚至争执之中。
关键问题是:软件系统很难做到像建筑行业一样,程序员只要根据图样一步步实施就能完成项目。没有人可以完整设计出一个大型项目的图样,这是瀑布软件工程方法的致命问题。客户(尤其产品经理)虽然无法提供逻辑严密的软件实施路径,但是目标、要求还是可以描述得相当清楚的,而且也可以在实施过程中通过与程序员互动,帮助程序员快速理解该领域的本质,而程序员的严谨逻辑也能帮助客户或产品经理不断修补设计漏洞。
很多情况下,程序员之所以在项目开始迟迟不编码,是因为他们希望某人告诉他们该怎么做,但是这种情况一般不会发生,这时他们就开始发挥自己的创造力,但他们这种创造有时会使得项目偏离正确方向。如何发挥程序员的创造力,同时又能保持项目的方向不被程序员带偏呢?
解决方法就是让程序员尽早参与创意过程,与业务专家、客户、产品经理一起参加头脑风暴会议,不断缩小双方的思考或理解偏差。有逻辑性的正确设计会节省大量代码,因为用编码实践来试错的代价是昂贵的。编码涉及大量技术细节,细到一个字段的字节数都需要考虑,一旦发现代码实现的功能完全不是客户要求的,就只能全部推倒重来。成千上万行代码被删除了,好像它们没有存在过,它们存在过的意义就是让程序员明白:此路不通。项目组付出了大量加班时间,却可能被浪费在一个思想的试错上,或者只是让自己的领域理解更进了一步而已。
孔子说:“学而不思则罔,思而不学则殆”,只是通过编码实践来学习业务知识,却不从思想层面去思考这些业务知识,就会陷入无头苍蝇般的忙碌中,并夸大每个项目或产品的特殊性,在不相信“银弹”的路上走上极端。
那么,程序员该如何将业务知识的学习和思考结合起来?如何通过有逻辑性的思考来提升自己的业务知识水平,从而编写出更专业的业务软件?
DDD思想和方法论的诞生,可以说初步解决了程序员的这种困惑。DDD建模思想不同于以往的面向对象分析设计思想那样,建模和代码之间还是存在落差,无法平滑衔接。它将分析和设计完美结合起来,通过引入上下文的特殊性,将项目的真正业务背景和集成复杂性引入设计建模阶段,虽然增加了设计的复杂性,但也提高了设计的实用性。不过,可能因为DDD引入了太多行话,导致其本身很难被传授。
1.1.2 技术负债与软件质量
DDD这个术语来自Eric Evans的著作,首先值得关注的问题是:为什么会有对DDD的需求?为什么DDD会逐渐风生水起?其根本原因需要放在一个更大的上下文(或背景)中去寻找,这个上下文就是软件质量。
程序员每天的工作大部分是增加新功能,当完成需求中的所有功能后会长舒一口气,总算可以交付了。但“噩梦”可能才刚刚开始,为了赶工期加班加点编写出的代码质量如何?
谈到代码质量就会涉及另外一个软件质量领域的重要词语:技术负债。所谓技术负债,按照Martin Fowler的定义就是增加新功能所需的额外成本。
这句话怎么理解呢?先来打个比方:有一个新的音响,准备接到计算机上,但是电源插座上虽然有空余的插座孔,却无法插入新的音响插头,因此,要把电源插座上原来的插头拔出来,重新理一遍,以便让插头之间空隙大一些来插入新的插头。当把原来的插头拔出来时,发现很多线缆紧紧缠绕在了一起,需要把这些线缆分离以后才能将原来的插头移动到新的插座孔。
换一种思路:如果平时增加新线缆时,顺便把以前的线缆整理一下,彼此分离,井井有条,是不是就更容易替换或移动了呢?
也就是说,在平时增加新线缆(对应新代码)的同时,已经引入了复杂性,这个复杂性的成本就是一种债务,需要以后偿还——不会不还,只是时候未到而已。
技术负债就像技术前进途中的累赘一样,会像滚雪球那样越滚越大,不断拖延增加新功能的步伐,最终可能无法再为系统添加新功能。因此,技术负债的存在是导致软件质量下降的重要原因。软件质量下降以后,系统难以维护和修复,就会导致项目失败或者必须重写代码。
软件质量不同于其他商品质量。用户购买到一个商品以后,往往在使用过程中会直接发现该商品的质量问题,而软件质量不是直接被软件系统的使用者所感知的,也就是说,客户如果同时使用两种质量不同的软件系统,他们将无法发现两者的区别,即无法直接发现软件的质量问题。不过他们会发现,随着时间推移,自己的产品交付过程越来越长了。
软件质量问题不是直接面向用户,而是面向软件的开发团队。因为软件质量差,新程序员很难快速上手,无法形成生产力;修改别人的代码时可能一直不得要领;Bug丛生,修复一个可能使得系统崩溃,一个都不修复系统反而能正常运行;修复Bug时牵一动百,修改一处却引起其他地方的连锁故障反应……这些都是软件质量低下的外在表现。软件质量高的系统则很少发生这些情况,这样新功能就能一直高效加入,旧功能也不断地通过重构得到增强。
正是由于软件质量不是最终用户所能感知的,导致行业内对软件质量没有过多重视——客户都没有提出改进要求,那么一切为客户服务的软件公司自然就没有动力去提升软件质量。而且行业内对软件质量存在一个认识误区:便宜确实没好货,但是质量高必然导致成本上升,而客户又不会察觉质量好坏,那么产品如何卖出好价格呢?
然而,软件并不是质量越高,成本就会越高。这好像违反常识,背后其实也与技术负债有关。如果将技术负债看成一种前进中的累赘,累赘遍布于代码各处,那么提高软件质量就是通过良好设计或重构来减轻这种累赘,从而能轻装上阵,新增功能就更加快捷,交付效率也会大大提升。
降低技术负债意味着软件质量提高,软件质量越高,修改拓展起来就越方便。
那么如何降低技术负债呢?这里存在一个适度问题。首先,代码越多,复杂性越高,技术负债肯定越高,那么就需要惜墨如金。有时为了写出正确可运行的简洁代码,可能要删除数十倍的代码,但也不是代码越少越好。有的代码只是考虑功能的实现,没有考虑到功能的对接或扩展,那么当需要对功能实现扩展时,就发现难以下手,甚至需要采取黑客破解的方式强行入侵修改,这些都是原来代码过于简单僵化的表现。
适度是在过度和不足中探索平衡的结果。代码适度的一个衡量标准是单一职责原则,即每个函数或类只能有一个职责,它就是为了这个职责而存在的,而不是为了多快好省,将多种功能放在一个类或函数中。
面向对象编程领域还有另外一个原则:D RY(不要重复自己)原则。对这个原则的共同理解是代码不应该重复,如果两段代码表示的是同一个职责,那么合并它们。但是,这种抽象合并会导致共享内核或共享库,最终造成代码各处对共享库或内核的依赖,这就很自然地引入了不必要的、偶然的复杂性——一旦共享库发生修改,牵一动百的事情就可能发生。这也是使用框架或库包的局限所在,框架和库包确实提高了生产效率,但是也限制了项目代码,因为代码会依赖于它。
很多时候,重复的代码可能会带来相当大的优势,重复能拖延决策,这是软件开发的黄金。这样,延迟到适当时机后从多个专业化角度重构,这比从单个方法层面进行抽象的重构要容易数倍。预先应用DRY,将导致构建领域中不存在的抽象,虽然这是体现程序员创造性的地方,但是笔者不推荐无中生有的创造,因为当时你的视角可能存在偏见。将一些函数合并在一起的原因常常是,这些函数虽然位于在不同的类中,但是功能看起来是相同的,但随着时间推移,当你的注意力从函数功能本身转移到它所在的上下文时,又会发现它们还是有些不同的。
只有当复杂性变得难以管理或业务模型有明确要求时,才应该对抽象进行重构,提前执行此类操作只会损害代码并引入大量意外的复杂性。
降低技术负债的另一个办法是引入架构元素,例如编码前通过对业务模型的头脑风暴,缩小领域专家和程序员之间的业务水平落差;编码完成后增加单元测试和集成测试,尽可能将测试、发布和运维自动化,实现DevOps哲学化管理。改善质量不是一个人的责任,而是每个人的责任。精益是丰田应用于它们如何在整个组织中制造汽车的理念,短短几年内丰田的快速发展证明了这种哲学方法的有效性。
当然值得注意的是,不要被打着提高软件质量的旗帜欺骗,在增加了复杂性的同时也阻碍了软件质量的提高。编程是人类思维的延展,保证程序员足够的休息时间和愉快的心情,才是提高软件质量的重要因素。没有良好的精力和敏捷的思维,头脑风暴会议只能成为一场瞌睡大会。
1.1.3 ER数据建模与面向对象建模
将用户的需求转化为软件代码的过程是软件的分析设计过程,这个过程一般有两种方式:ER数据建模法和面向对象建模法。
ER数据建模法是在接收到需求以后直接开始数据表ER模型的设计、实体表和表关系的设计。ER模型往往依赖于数据库技术,甚至与后者非常紧密地耦合在一起,虽然带来了效率的提升,但是高效率不代表高质量,而软件高质量却能带来高效率。
建模过程是一种翻译再表达的过程,其唯一要点就是翻译不走样,如果翻译过程过多引入其他干扰因素和知识,那么无疑会增加翻译的难度和复制的精确性。ER数据建模引入了数据库表技术,这种库表技术虽然因为SQL标准的普及而变得门槛较低,但是这也对存储过程或触发器等复杂技术的引入敞开了大门,进而要求开发者掌握每种数据库特有的“方言”,这样才能应用自如。这些都偏离了建模忠于输入需求的目标。如今,由于NoSQL等非SQL数据库日渐流行,包括全文搜索、Redis缓存数据库等各种数据库技术百花齐放,SQL不再是所有数据库遵循的标准用语,这些都为ER数据建模带来了新的挑战。
ER数据建模虽然也有一套分析设计方法论,但是由于过于注重数据库技术而忽视了业务上下文。CRUD是“增删查改”的简称,如果使用CRUD用语替代业务用语,例如使用“创建订单”替代“下单”,使用“创建帖子”替代“发帖”,使用“创建发票”替代“开票”等,虽然也容易让人明白,但是“下单”等用语才是真正的业务术语、业务行话,是这个行业内每个人都知晓的。作为软件系统不是去遵循这些用语习惯,而是进行转换改造,按照自己的理解生造出一些词,是不是会潜移默化地将业务需求引导到不同方向呢?
当使用CRUD这样的通用词替代业务术语以后,最大的遗憾是丢失了术语背后的上下文——在冬天说“穿得越少越好”与在夏天说“穿得越少越好”是不一样的。失去业务上下文以后,设计的逻辑性将很难去追溯和质疑。
例如,用ER数据建模工具为电商系统实现了一个订单表的CRUD功能,但是却不知道为什么电商系统会有一个订单表。也就是说,怎么会从电商系统中发现订单这个实体表,这个实体表从何而来?这些问题可能背后没有一套统一的逻辑方法来支撑,即使存在也因为这种数据抽象技术的表达而丢失了,或者变得复杂了。由于没有统一可演进的分析设计方法论,当接到另外一个全新的项目时,如果没有任何业务经验,则可能无从下手。
面向对象的分析与设计方法由此催生而蓬勃发展起来。顾名思义,面向对象的意思就是基于对象,直接面对的是对象而不是数据表、不是ER数据模型。那对象是什么呢?
首先从日常分析思维开始。对事物进行分析时经常先对它们进行分类,分类后根据其类别特征取一个名称。当然,这个名称不能是“类型1”“类型2”……这和CRUD一样失去了业务上下文,还是无法清楚地表达事物的含义。其实生活中这样含糊的定义有很多,例如糖尿病有1型和2型,只有医学专业的人或病人自己才能明白它们两者的区别;又例如垃圾分类有干垃圾、湿垃圾、可回收垃圾和不可回收垃圾4种,这4种类别名称也是有问题的,在逻辑上并不能形成排斥补充的关系,至少不能一目了然。干与湿是一套标准,而可回收和不可回收又是一套标准,这两种标准如何放在一起呢?这两种标准是互斥的还是相容的?可见其逻辑漏洞还是很大的。产品经理或客户也常会设计出这样的矛盾体,如果没有进一步建模分析,程序员就很难用代码真正实现,即使强行推动,等到面临具体矛盾时,也会发现前面的代码白写了。
因此,逻辑一致性是分析思维的基础,至少不能自相矛盾,但是当需求复杂性提高时,这种矛盾经常不可避免,必须排除需求中自相矛盾的概念,才能更加完美地进行分门别类。
概念的分类前提是概念本身定义的严谨性。有了业务概念的严谨性和逻辑一致性以后,就可以使用明确的对象名称来表达它们了。但是有人认为,可以使用对象表达的概念,也可以使用数据结构表达。
那么,对象与数据结构有什么区别?
数据表是一种常见的数据结构,数据表中的数据是需要SQL动作去操作的,也就是说,数据结构中的数据是被外部的某些行为或函数操作的;虽然对象或类中封装的属性其实也是数据,但对象或类有行为方法,这些行为可以保护被封装的属性数据,外界需要改变对象中的属性数据时,必须通过公开的行为方法才能实现。因此,对象和数据结构两者的区别之一就在于对数据的操作是主动还是被动——对象是主动操作数据,而数据结构的数据是被动操作,这一区别使得两种方式下的分析设计思路和编程范式完全不同。
当使用这两个模型表达业务领域中的业务概念时,强调主动操作数据的类或对象更适合表达业务概念,因为业务领域中的业务策略或业务规则都需要动态操作去保证,它们的逻辑性需要主动操作数据来完成,如果只是一条条静止的规则数据,肯定无法保证业务规则的逻辑完整性和一致性。例如,if else常用于执行业务规则判断和流程转向,如果没有这样的判断执行,再好的业务规则也无法应用到现实中。
从设计角度看,业务领域中的业务对象是定义业务行为的一种结构;数据表模式是定义业务数据的结构。它们一个注重业务的行为,另一个注重业务的数据,着重点不同,导致设计要求不同,也正是出于不同的设计要求才有对象和数据结构这两种不同的实现方式。通常,数据表结构一旦形成,就不会因为一个特定的应用而进行调整,它必须服务于整个企业,因此,这种结构是许多不同应用之间平衡的选择;而使用“对象或类”这种结构可以针对具体应用进行设计,将业务行为放入“对象或类”中,这样更能精确反映领域概念,保证业务规则的真正逻辑一致地实现,这是面向对象分析和设计(OOAD)的主要优点之一。
但是,传统的面向对象分析和设计也有问题,如分析和设计之间落差很大,甚至是分裂的。分析阶段的成果经常不能顺利导入设计阶段,设计阶段引入太多细节而歪曲了分析的宗旨。分析和设计分裂的根本原因是它们导向的目标不同,或者说面向的目标不同:分析人员的目标是从需求领域中收集基本概念,是面向需求的,而设计人员则不同,他们负责用代码实现这些概念,因此必须指明能在项目中使用编程工具构建的组件,这些组件必须能够在目标环境(比如Java)中有效执行,并能够正确解决应用程序出现的问题。条条大路通罗马,分析人员负责指出罗马的方向,而设计人员负责找出通往罗马的某条道路,但是技术细节有时会让这个过程中产生绕路和不必要的复杂性,甚至走错方向,南辕北辙。
以上讨论了传统的ER数据建模的局限,以及传统的OOA和OOD之间的割裂现状,正是在这样的一个背景下,人们期待一种新的分析设计方法问世,它应比ER数据建模更加面向业务,能够弥补OOA和OOD之间的天然裂缝,于是,DDD应运而生了。
1.1.4 DDD的诞生和发展
自从Eric Evans的首本DDD书籍出版以后,涌现出很多书籍和文章扩展了其中的观点,人们创建了各种新的方法来应用这些原则,各种在线课程与会议遍布欧洲、亚洲和北美等世界各地,Evans本人也认为,DDD社区需要大家共同发展。
传统上,DDD社区只由程序员和架构师组成,软件分析人员是其使用者,因为建模一直是分析的基础部分,但现在测试人员和产品设计人员正在发现DDD的价值。
DDD还在发展之中,过去十多年中主要经历了三个阶段:首先是Eric Evans的理论原则创建和普及阶段;然后是引入领域事件、事件溯源阶段;最后是微服务架构的提出阶段。由于DDD提出的有界上下文已经将业务的边界划分清楚,所以微服务的实现就顺理成章了。当然,微服务架构的普及和发展也迅速促进了DDD的普及和发展。
同时,在人们不断丰富DDD的实现技术以后,突然回首才发现,DDD中的战略模式需要更多的关注,因此,事件风暴等有关组织管理等方面的新事物开始出现。通过事件风暴会议发现领域中的事件,对领域的上下文进行切分,发现其中的聚合,这套方法变得越来越流行。之所以会这样,是因为大家发现寻找领域或上下文边界才是DDD中最难,也是最需要创造力的地方。边界或有界上下文是DDD专门用于解决复杂性的有力武器,是DDD的核心内容。
DDD是专门解决复杂性的方法论,前面的描述中已经对复杂性有所提及。首先,当需求规模比较大时,需求内部之间可能会发生矛盾,有些矛盾隐藏得非常深,可能只能通过代码实践才会被发现,但是这种代价非常高。当然,使用DDD建模并不是为了将整个系统预先设计出来,而只是让一些有丰富领域知识和逻辑思考能力的人通过头脑风暴发现系统的复杂核心所在。
复杂性无法消除,问题空间的复杂性是天然存在的,一个大型系统肯定比小型系统复杂得多。那么人们对这种客观存在的复杂性就无能为力了吗?当然不是,人们理解复杂性有自己的一套分析方法,如分门别类,逐步、有层次地分解。DDD关注的重点就是如何将复杂的问题空间通过逻辑分析解析出来,如同解析方程一样,从原来的混乱无序变得有条理、有层次,这样经过梳理的复杂性才是DDD需要的结果。由于分析结果变得有层次,相互隔离、松耦合,就能分派不同的团队专门处理各个问题的子域或有界上下文,分而治之。
DDD在一定程度上帮助解决了程序员的主要困惑。当一个项目或产品启动时,着急的产品经理或项目经理经常发现程序员还在慢悠悠地工作,代码都没有编写一行。程序员迟迟不肯动手编程的根本原因在于:他们并没有得到更具有指导意义的设计,具有逻辑思考习惯的程序员根本不知道从哪里入手、怎么入手。建几个数据库,然后使用CRUD解决吗?但问题空间可能非常复杂,需要哪些数据表都不是很清楚,对这些数据表是否能够完成用户需要的功能心里也没底,更重要的是如此复杂的需求中有没有深坑?有没有逻辑矛盾?有没有带有个人职业背景偏见的观点?产品经理如此脑洞大开,其创意是否能够实施?
DDD中的事件风暴(Event Storming)建模倡导将脑袋大开的产品经理和严谨求证的程序员召集在一起进行头脑风暴。大胆设想和小心求证在这里得到了融合,通过事件风暴会议,程序员能够更加理解业务领域知识,产品经理会发现自身的逻辑矛盾之处,通过反复迭代,创意开始变成可实施的产品。关键是,每个人对复杂性的认识逐渐一致,复杂性被专门隔离出来,可以先从容易的地方下手,逐步逼近复杂性。随着时间推移,每个参与者都会朝着目标更进一步,复杂性被肢解,由不同的团队专门负责不同的有界上下文或微服务,同时在敏捷实施过程中,不断调整人员。领域的边界划分不断演绎,只要发现复杂性凝聚的地方,就划定为有界上下文,割裂它与其他系统的关系,并派出精兵强将专门对付。
DDD是复杂问题的解决之道。DDD解决复杂性的方法并没有带来额外的复杂性,但是很多初学者还是觉得DDD复杂,难以掌握,行话很多。这些复杂性其实是因为初学者本身没有培养其中的逻辑分析思维。软件设计的思路难道就只是建数据表或实现CRUD的SQL编程,然后再放入一个微服务中?有没有想过CRUD本身已经脱离了业务场景,已经是一种抽象思考?这种抽象是否有必要,是否因为抽象而漏掉了重要细节?为学日益,为道日损,CRUD是不是一种真正的“道”呢?有没有真正的“大道”只是自己没有意识到呢?
DDD解决问题的方式继承了多年的面向对象分析设计方法学,同时吸收了函数式编程的优点。面向对象和函数编程其实并不是矛盾不可共存的,面向对象更符合人类的思考习惯,可以帮助人类实现分门别类的大方向分析,划分边界,封装复杂性等,而函数编程更符合数学思维,适合计算机模型本身,在将人类分类的结果交由计算机运行过程中能发挥重大作用,同时也能避免人类自身没有计算机精确、没有计算机逻辑严密的缺陷。副作用通常是人类容易犯的错误,做一件事本来是为了某个目的,结果却有了另外一个不好的结果,而函数式编程则可以避免这种副作用,从而规避副作用带来的技术债务,提升软件质量。
DDD实施中最大的副作用是可变状态的管理。DDD聚合根代表一个有界上下文的复杂核心概念,其复杂性来自聚合根实体的状态是经常可变的,例如订单的状态从支付变为发货,这种变化决定了整个系统的成败——如果一个订单没有支付,但是商家发货了,这样是不合理的。DDD虽然通过边界划分和状态封装固定了复杂的可变状态,但是如果不结合函数式编程,这种可变状态产生的全局影响是很难消除的。如果一个新手程序员不小心改变了订单状态,这个订单就变得无从追查,损失的是客户利益,而采取事件溯源等方式,直接记录发生的领域事件,并不改变状态,所有需要订单当前状态的有界上下文自己通过遍历这些领域事件来合成当前状态,这样的状态既是实时的又是准确的,也是可以追溯的。
DDD解决了传统面向对象分析和设计的割裂状态。面向对象分析的结果会被设计细节干扰,导致严重偏离分析方向,DDD是一种“一竿子插到底”的方法,分析的结果必须经过设计细节验证。事件风暴倡导的是首先提取领域中发生的事件,从非常细节的动作入手来分析需求,而传统的面向对象分析方法则是主语名词法,首先寻找领域中有哪些实体名词,这种分析方法其实受到了ER数据模型的影响。主语名词法的严重问题是,如果人们无法发现主语名词,就只能替代以想象中的“上帝”了。
事件风暴从动词事件入手,虽然很琐碎,但是这些事件正是日后需要实现的功能激发的。事件离需求功能更接近,对领域事件进行分门别类,可以发现有界上下文和聚合。使用领域事件替代可变状态,可以实现有界上下文之间重要的状态传递,领域事件是就是DDD一竿子插到底的那根“竿子”。
当然,事件风暴是在传统DDD基础上演变发展出来的,传统DDD也可以通过UML顺序图使用动词分析法,这点在Evan的书中已经得到体现。无论事件风暴还是UML,都是人类思想的表现形式,不必拘泥于表现形式,关键是要掌握DDD分析方法的核心:从细节动词入手发现有界上下文和聚合,以逻辑一致性为边界划分依据,对动作实现分门别类地划分。
人以群分,物以类聚,这里的“物”应该是动词事物,而不是名词。人其实也是一个活动的动词,但是人们习惯于名词主语思维,因此会给人贴上标签,标签是一个名词,代表一种状态,很显然这是粗暴简单的分门别类。人以群分的真正意思不是用标签划分。微信群是一种“人以群分”的典型代表,一个人可以参与不同微信群,参与这些微信群正是人以群分的体现。注意这里的用词:“参与微信群”,这是一个动词短语,如果去掉动词“参与”,这些微信群正是人以群分的体现,这其实就是用微信群作为标签对人进行分类,这也不符合真正原义。从这里可以看出,DDD非常强调语言表达,可以说,DDD是一种关于自然语言符号分析的方法学,它倡导的统一语言、无所不在的语言,正是从语言分析入手去发现上下文语境的。同一个领域模型在不同上下文语境中是不同的,因此需要分别建模,例如客户这个模型在客户管理上下文和订单上下文中是不同的,客户管理上下文中客户无疑是主语,是主导者,而在订单上下文中,客户只是订单这个主语的附加定语或组成信息而已。
总之,DDD继承了传统面向对象分析设计方法,结合了函数式编程风格,同时照顾了ER数据模型的设计,可以说是过去这些主流方法学的集大成者,它通过划分边界、纲举目张、分而治之等分析方法直面分解复杂问题,而不是隐藏回避复杂问题。
DDD遵从单一职责、少即是多、大道至简等设计原则,吸收了各种新的分析方法和思想,不断努力降低技术债务,提升软件质量,提高软件的可维护性和可拓展性。
笔者对此深有体会。本人一直对论坛系统开发有浓厚兴趣,曾经用过Perl、PHP、JSP和Java编写各种论坛系统,同时也负责这些论坛系统的运维,但是自从使用DDD重写论坛系统以后,才发现维护拓展一个系统竟如此简单,以前不愿意更改论坛代码,担心这样会中断论坛的运行,影响论坛的人气,现在几乎很少碰到这种情况;同时可以将论坛系统演变发展成为博客系统或类似微博的社区系统,这些都得益于对论坛系统的DDD建模和实现。在基于DDD的论坛系统中,模块之间松耦合,特别是核心部分得到隔离,核心部分又通过聚合凝聚在一起,很多核心功能通过聚合功能优化即可,摆脱了对数据库的依赖,数据库表字段的增加和修改几乎完全避免了,也不会对原始数据进行各种转换。其中所有的设计都是通过Java代码实现的,轻量且容易改变,如果通过ER数据模型实现,对旧数据的转换就让人生畏,关键有时还需要转换回去,几万行数据中任何一行数据有问题,转换就会失败,即使转换成功,在新系统中不能正常读写时,又会涉及旧业务逻辑和新业务逻辑的比较。使用Java在内存中计算排序或做各种处理,包括业务流程的变化都不影响数据表结构,通过新增数据表结构来实现新业务数据的保存,不去修改原有结构,通过Java的聚合模型将新旧数据表结构凝聚在一起。论坛系统的DDD实现方法会在本书后面章节的知识点讲解中作为实例具体介绍。