去年就打算总结一下,结果新换的工作特别忙,就迟迟没有认真动手。主要内容是很多初学DDD甚至于学习很长时间的同学没有弄明白DDD是什么,适合什么情况。这世界上没有银弹,抛开了适合的场景孤立的去研究DDD,在学习过程中还可以,但是应用到实际项目时就会遇到各种坑,到头来各种妥协,我看到很多同学遇到这种情况,最后怪DDD,说DDD不实用云云。另有一些都没细了解过就抨击反对的,事实上,要想否定个东西,你总要了解了才有发言权。
一、误区
综合起来主要有一下几种误区:
1.1、DDD更高级,可以替代三层,为了未来项目的规范与扩展,初始的小项目直接采用DDD的方式想要一步到位(当然这也不能说错,但是需要一定的妥协,因为DDD并不是为了这种场景而产生的,甚至特别小的项目更适合诸如书上提过的smartUI)。一个项目未来的发展是不能估计的,为了未来不可预测的变化在现在花费更多的成本是没有必要的,哪怕你能确定以后一定这样,但细节很难保证,与其到时候改还不如不做。另外,在一定规模的项目中,DDD和三层没有什么区别,这个后面会细说。也有人反驳说不信大设计解决不了小系统,解决当然都能解决,解决的是否优雅就不好说了,就如分布式可以用于解决大并发,但并发不大的小系统用来并不合适,因为会有不需要的网络等开销。DDD本身其实就是面向对象的一种延展,当你系统体量不足够大的时候,即使使用DDD很多地方也是需要向工期向复杂度妥协的,其实表现出来的一定是常见的面向对象设计,只要严格按照面向对象的方式推进演变,当复杂度到一定程度的时候,就可以发现转变为DDD的设计思路是自然而然的,很舒适的过程,我设计过的一个架构就是一步步由很简陋的三层最后演变为现在的经典DDD方式的架构,分别通过几个随笔在博客简单记录过,有兴趣的大家可以看看。
1.2、DDD是一种实现方法,这样的看法一般是两种情况:一是没有从事设计工作的人;二是没看过书通过网上一些实现例子来了解DDD的。事实上,DDD是用来指导业务规划设计,架构设计的分析过程,方法论(所谓方法论是说告诉你想看蓝天就不要有雾霪但不会告诉你要减排),而不是确实的模式方法,不是用来套用的,更多的是间接影响代码实现。这种误区经常体现在一开始谈DDD就提数据库,或者DDD是Code first,我并不反对实现的时候先做进行数据库方面的工作,也不反对DB first,因为这是实现过程,和业务逻辑的规划没有任何关系,考虑的数据库的时候应该是在DDD设计完成之后。另外,个人的一点偏见,或许我不了解,但仅就我接触过的来看,数据库架构师这种职位完全没有存在意义,DBA协助开发人员根据业务设计数据库没有问题,但这种设计更多的体现DB方面的技术能力,但是把精力都放在规划业务,引导程序员这完全是努力错方向了。学过UML应该会注意到一点,在UML设计中,活动图或序列图通常会比类图更重要一些,因为类图只能体现静态关系,无法体现动态的业务办理和业务规则,类图只能将业务对象的细节表现出来,充其量也就是活动图中业务对象的细节填充,数据库的架构最多也就只能表现出类图的效果,然后这片面的东西最终却会干扰业务动态过程的开发。我见过的数据库架构师一般是说程序员没有设计好,开发没有收到控制导致各种问题之类的说法,这种时候其实更需要的是软件业务层面的架构师,他不只会规划业务对象,也会规划业务规则和动态过程。
1.3、使用了聚合这些基本方式就是DDD了,其实聚合在DDD产生之前就有,这些只是DDD的组成基本元素,比如学习汉字前需要先学横竖撇捺这些笔划。一般情况下我们学一样技术,主要原因是它能解决我们的某些问题,但多数DDD的初学者只是因为不明觉厉就开始学,完全没有学习DDD所需的基础,而DDD书上说的最直接的就是这些基础,同时多数人看书会只看个开头就觉得明白了,而不往后认真看,导致认为学会了笔划就学会了汉字。
1.4、不看DDD原书,想直接通过IDDD实现学会DDD。大家都知道DDD最经典的两本书《DDD》和《IDDD》,最近发现多数同学完全没有看DDD直接就去看实现,觉得学会写DDD的项目就学会了DDD。其实无论多理想的实现都会和设计的时候有一定偏差,各种妥协,以代码学习理论,妄图坐所有井以观整个天,用代码实现去反推学习理论,之前曾经看过很多自称过来人说面向对象如何不好,其中多数都是以为写了几个实体类就算是面向对象了,多数都是从代码来理解理论造成的,如面向对象,DDD这类方法论类似于哲学,指导解决一类问题的一种思路,而不是固定的一种实现,玄而不绝谓之哲学。举个例子,宪法是基本法,是制定所有普通法律的依据,但是妄图读了所有普通法律然后倒推出宪法基本就是吃力不讨好,DDD和代码实现的区别怎么也比宪法和普通法的区别要大。再说DDD原书上的理论是根本,而IDDD上提到的理论性的东西至少我是持保留态度。
1.5、仓储是用来与数据库交互的,特意要说明的是,虽然在实现的时候确实持久化数据库基本都在仓储里,但在设计的时候仓储的意义只有一个就是重建聚合,DDD的最后一个D是Design,特别强调这个是因为哪怕再微小的一点偏差在最后实现的时候就会被放大很多倍。另外一点在实现过程中,仓储的调用是应该在应用层的,经典DDD的基本分层中,应用层和领域层的区别是领域层定义封装业务过程,应用层调用领域层的定义来执行,而重建聚合是对领域层聚合的使用,所以应该在应用层。
二、DDD的产生
2.1、什么是DDD
学习一样东西的时候,首先要弄清它是什么,以是什么为基础点开始学习才不会偏离。大家愿意看到这,当然是对这方面有兴趣的,自然都是知道DDD的,也就是用领域驱动设计。那什么是领域呢,经常看玄幻小说的童鞋应该会比较熟悉这个词,有一类技能经常以“领域”为名,这类技能有一个共同点,技能的作用范围内,所有的规则(例如重力,阻力等)是由技能的发出者决定的。DDD的领域是指某种业务的领域,也就是说,这个领域内决定了这种业务的规则,区别是“领域技能”的重力可能规定为地球的10倍,而银行转账的业务领域中传出的钱一定与目标账户转入的钱是相等的。理想的状况,领域中包含了业务中相对固定不变的东西,也就是每次业务实例的执行中都会包含的办理该业务的依据、规则,比如发工资,公司账户少的钱一定等于手续费之类加上你的工资,但是发工资的日子却不是业务固定下来的,所以检查发工资日期的功能就不该在领域层中。不过现实中可能会有些特别情况,比如之前做过政府的项目,在进行过程中,国家对相关政策会进行调整,所以在设计领域层时,也会将本应在一起但由于稳定层度不同的业务规则分隔开来,虽然总体来看并不十分顺畅,不过妥协也是项目进程中不可缺少的部分。至于对这些规则的应用,也就是经典DDD中应用层负责的事了,每一次业务的办理,由应用层去实例化领域中的定义和规则来办理业务。
知道了什么是领域,接下来就是领域怎么用,使用领域是用作推动设计的,以领域为基本点来推动的是软件设计,也就是怎么来规划软件,注意是如何规划而不是如何实现(有哪些业务对象,而不是有哪些类)。明确了领域驱动的是设计,就不会出现设计之初先设计数据库这种情况,可能在常年使用表驱动的开发方式的童鞋眼中,这无关紧要。但是,偏离会越来越严重,一旦开始采用表驱动设计,就会丢掉许多重要元素,而这些元素有很大可能对对象的职责及属性有影响。在UML图中,序列图能反映出有哪些对象和对象之间的交互,在我看来某种程度上来说类图只是对序列图细节的说明,而表驱动就相当于以类图为核心做设计,似乎有些舍本逐末,无法反映业务对象的状态流转的规则,纯粹由业务对象关系组成的架构是不完整的,而领域既包含静态的关系,也包含动态的变化规则,注意这里不是变化,而是变化的规则。之所以说是变化的规则,意思是领域虽然说明了怎么变化,但并不包含领域的应用也就是具体的变化。还是用玄幻小说的技能来距离,比如进入了某人的“领域”,进入者的运动速度会减半,这说明了这个某人的“领域”有一个使进入者速度减半的特性,但是只有在某人释放了他的“领域技能”,并且有人进入时才会发生这个具体的速度减半功能,也就是说某人定义了他的“领域”与速度减半的规则,但只有在应用层调用这个领域并且符合有人进入的条件的时候,这个速度减半的变化才会发生。
由于很多同学反映分不清领域层和应用层,关于这点特别说明一下,一种方式能不能解决自己的问题,自己应该是最清楚的,当前情况如果是领域层服务或应用层服务分不清的时候,一般来说是因为还没有到必须分清楚的情况,至于未来需要分清楚,当然是遇到了问题才需要解决问题。应用层和领域层可以粗略的认为相当于普通三层的业务层,而之所以分开则是,当业务非常复杂后,业务规则本身相对稳定,但是对业务规则的应用变化却会频繁,将变化频繁程度不同的部分分开也是产生这种变化的一个重要原因。
2.2、DDD的作用
DDD产生的作用,原书的副标题写的很清楚,软件核心复杂性应对之道。关于什么是复杂,我写过一篇博客《应用系统开发思想的变迁》。传统面向对象的三层大家都了解,对DDD来说,在同一上下文也就是一个独立系统内DDD的实现与充血三层的区别,在我看来也就是当充血三层中的业务对象庞大,以至于在分析设计时对大脑思考造成一定的负担,DDD提出了一些解决这种负担的方法的理论。也就是说,DDD与三层一脉相承,由三层发展而来。DDD最主要的目标是在十分复杂的环境下,帮设计人员理清业务逻辑。按照一个确定的规则规划出业务代码的组织形式,让复杂的系统围绕着核心价值展开,有清晰的主题和脉络。
而解决复杂的原理:分虚拟层,我们从开始学习开发开始就一直不断的从各种层面上学习一种解决复杂度的方法---分而治之。最早,当时的程序没有数据的概念,全部都写在代码里类似现在的常量,但是常量太多了,程序不好写了,于是分出来产生了配置文件,再之后爆炸式增长的数据被分出来于是有了数据库,对象也是一样,聚合也是一样。
大家都或许都听说过,正常人的工作记忆量为7正负偏差2,对软件设计来说就是我们同一时段内互补干扰的思考的不同业务规则或对象的上限是5~9。而实际中我感觉设计时通常有超过两种需要考虑的问题时就会对思路有一定的扰乱,为了让业务的分析和用软件体现出清晰的业务更流畅轻松,将复杂以至于难以分析的业务分解为各个小领域,直至每个领域内部可以在符合需求的级别轻松以OO的方式分析,最后可以将复杂的业务转变为可以简单的采用传统OO的方法实现,这就是DDD所要达到的目标。这也是我所说的开头的第一个误区为什么是误区,所以在普通的OO方法足以应付时虽然套用DDD的分层方法,对大多数程序员来说只是从三层变成了四层而已。
DDD的重点也就是在这个解决复杂的原理下,如何清晰的分解出领域,对此给出分解的依据和方式方法。对领域中大量的聚合划分核心子领域,通用子领域,以及相生的界限上下文等等的划分方法、模式、理论也就是DDD的真正产生价值。
三、DDD战略设计基本原则
之前说了适用场景和解决复杂度的问题,下面大致说一下DDD是如何划分各种虚拟层的。顺便说一句,还有一句名言,任何性能问题也都可以通过减少虚拟层来解决,所以说不复杂就不要多分不能带来明显好处的层。
3.1、上下文
最先划分的一般是界限上下文,界限上下分所划分的是系统的边界,也有人说上下问标示着一个问题域。这个边界之内一定可以独立成为一个系统,可以独立完成围绕着一个核心的业务,当关注点是这个系统的时候,这个边界内一定有一个核心子领域(Core Domain)。上下文的边界之内,通用语言是一致的。而跨上下文的通用语言通常是不能保证一致的。也就是说不同上下文之间的业务关联非常弱,或许可以相互配合完成某种业务,但各自是独立的,各自所关注的核心业务应该是没有直接联系,不会受到互相影响的,保证这种独立性的就是界限上下文了。保证上下文内部概念的一致性,持续集成是一种很好的方式。这里说到了核心子领域,相对的也有通用子领域,这是根据业务重要程度以及紧密程度来划分的。而当前上下文的核心子域,有时也会成为其他上下文的通用子域,比如微信支付有可能成为我的微信商城的支撑支撑,辅助解决商城购买的支付问题。上下文这一层次的一种很好的实现,类似于现在很多互联网公司的架构,公司内有无数独立运营的小系统,每个系统都是独立的模型,独立的领域,独立的收支方式,同时又提供API,对内可以有自己系统的核心子域,同时又可以成为其他子系统的支撑子域。每个小系统只要保证自己系统不出问题,整个公司就可以平稳的运作,即使某些系统出现问题,也可以在一定程度上保证其余业务,至少是业务的核心尽可能的稳定。而对于整个公司的整体架构,一个清晰上下文图是很重要的。
保证清晰的上下文图,需要明确各个上下文的接触点、名字、边界,和之间的关系。关于上下文的关系以及如何应对这些关系,DDD提出了一些实践模式以及演进可以借鉴:共享内核模式,客户/供应商团队模式,Conformist(跟随者)模式,防护层模式,独立自主模式,开放主机模式,公共语言模式。例如:共享内核模式是说适用于不同团队中有部分完全相同的公用部分,可以避免这部分重复,但是共享部分的修改需要与公用的团队进行沟通,同时进行集成测试时,各个团队都要进行测试。大家应该很熟悉这类情况,一般一家公司的项目都会有公用的类库,有可能是核心业务的,也有可能是工具类。这里就不细说了,回头用空,配合一些项目再来细说,大概知道就好真遇到特别契合的情况再细了解也不迟。特别提一点,客户/供应商模式中的客户是指客户的了解业务的技术团队,因为客户业务团队的领导想要的东西不一定符合业务,有时甚至与真正的业务人员的业务工作是相冲突的。
所谓模式,就是在特定限制条件下的优秀实践。对于如何应用模式的态度,我觉得在你所面对的特定情况下,自然而然的发现不使用某个模式,你没有更好的实现方法时才去用;而不是你时刻想着这个地方可以用什么模式实现,这是典型的拿解决方案套问题,非常不可取,无论是设计还是开发过程中,都时刻要考虑的一件事,是你要达到目的需要付出什么代价,性能和分层,时间和空间,CAP都在说明这件事,模式也不例外。
3.2、领域
上下文这一层次划分确定后,就要进入上下文的内部。领域的划分,概念太泛泛,打个比方,某种程度上来说领域的实现其实就是对业务对象的序列图的封装。领域的划分的主要目的是挖掘出当前上下文所关注的核心问题。因为即使使用了隔离层,在适用于DDD的大型项目中,领域的复杂性依然难以管理,这就需要继续剥洋葱。DDD里把这叫做精炼,精炼出最有价值的部分,区别于同类软件的价值部分,即当前上下文的核心子领域。
这一层次,DDD同样有一些实践模式可以借鉴:核心领域、通用子领域、领域前景说明、突出核心、内聚机制、隔离的核心、抽象核心。这些模式更多的需要团队根据当前情况配合使用。
至于子域内部的划分,很大程度上是以现实业务概念做参考,当一个概念在领域层面的展现很大很复杂,或者说需要展现的粒度更细时,就将其划分为很多更小的概念,这些概念通常会表现为聚合。至于聚合的设计,也有一些比如断言、概念轮廓之类的模式,不过更多的是面向对象的分析设计以及业务的理解了,如果面向对象掌握的好问题基本不大。这里基本就是充血实体一类的东西,虽然多出了聚合根这样的概念,不过差别不大,聚合根是为了保证聚合内的一致性而作为聚合对外的同一接口。如果面向对象没有理解好,建议还是循序渐进比较好,比如学物理的时候,先学的是伽利略变换然后才是适用更大范围的洛伦兹变换,否则学习效率会很不理想。
3.3、 大比例结构
大比例结构是从另外一个维度规划系统,尽量使程序员对整个系统的总体目标和组成有一定的认识。这一点上看上去很像上下文图,但上下文本身是拆分系统,很容易将某个子系统的程序员限定在自己的子系统内部,而无法为整个系统全局提供支持。在子领域层次有同样情况,因为系统的复杂性,导致专注于核心子域的程序员很可能对某些通用子域完全不了解而造成协调上的困难。大比例结构就是用来解决这个问题的,它提供了共享的整体视图概念,各个部分在整体中的角色,可以帮助相关人在一定程度上了解各个部分在整体中所处的位置。
大比例结构,大家或多或少都实际接触过,只是没有明确这个名字,多数可能会混杂在给新同事介绍公司整体业务、企业文化等的过程中,或许在设计文档中也会用或多或少的体现。有序演化模式,系统隐喻模式,职责层模式,知识级别模式,可插入式组件框架模式这些模式可以用来辅助形成大比例结构。最常用的可能是使用职责层,我在几个项目中使用过职责层和可插入式组件框架模式,博客里写过一点,不过由于是公司项目,又加上当时的理解有些微妙的偏差写的比较晦涩,其实现在理解也是随着学习实践的加深而不断加深的,不是也会发现之前的理解上的偏差。
另外,大比例结构也会需要一些规则限定。比如职责层中通信必须是单向的等等。需要注意的是:约束可以是设计容易理解,推动开发工作和更好的协调,但同时也会限制开发的灵活性,所以每条结构规则都应该使开发变得更容易。
四、总结
上下文,领域划分和大比例结构是DDD战略设计的三个基本原则。这三个原则相辅相成,可以搭配使用。至于具体设计如何做,经常会也会遇到这样的问题:DDD应该怎么做设计。答案当然是具体问题具体分析,虽然多数不大的项目都是用同样的熟悉的方法套用解决,但是当系统复杂到一定层度时,再用解决方案去套问题,很容易在一些细节出现无法预料或者难以追查的问题。虽然不能对设计一概而论,不过制定战略设计还是有些参考意见:决策必须传达到整个团队、决策过程中必须收集和反馈意见、计划必须允许演变、架构团队不必把所有最好的人员都吸收进来、战略设计需要遵守简约和谦逊的原则、对象的职责要专一而开发人员应该是多面手。另外需要注意,不要使用傻瓜式框架,因为有时技术框架会影响妨碍领域模型的表达和演变。
以上是DDD简略的介绍,很多精奥之处就需要各位童鞋看书实践揣摩了。DDD本身也是提倡不断演进的,演进的方法同上一句。IDDD倒不急,概念不弄清,很容易偏离,事倍功半,先弄清楚怎么分,为什么分是项目实现的第一步,但这第一步确实DDD真正的核心。实际上,领域驱动设计也就是如何推动设计的进行,一旦设计做出了或者大体成型了,DDD在这个项目中的实践也就打好基础可以平稳推进了,至于之后的迭代,在有XP意味的项目中不论是需求、模型还是什么的都是贯彻始终的。DDD是一系列分析方法,在遇到具体情况的时候完全可以借鉴其中一部分,最终目的还是解决问题,至于是不是DDD,强迫症到一定要套用,就舍本逐末了。
原文链接:https://www.f2er.com/javaschema/284589.html