前言
领域驱动设计(Domain Drive Design)简称DDD,是2004年Eric Evans提出的一套软件开发的方法论,随着微服务的兴起,这个概念又再次推到大众眼前,热度不减。
笔者有幸从17年接触领域驱动设计的理念,在实践的过程中受益良多。对于DDD的概念每个人都会有自己的解释,如果非要用自己的话给DDD下一个定义,我的理解是:DDD是一套指导我们需求分析,服务拆解和实际编码落地的一整套完整地方法论,它提倡软件模型和业务模型相关联,用代码模拟客观现实世界,以此做到客观世界发生变化的同时,代码可以更好地随之演变。
笔者是Java开发出身,所以在文章中举例子使用的是java的语法和编程方式,请见谅,同时文章观点若有偏差,还请指正。
0:灵魂拷问
1:软件开发之前是否在分析完需求后第一件事就是新建数据库表?
2:为什么咱们PO对象(数据库持久化对象)需要写那么多的get/set方法?
3:是否有一些时刻觉得service的代码越写越多,非常混乱,往往改一个需求会牵一发而动全身?
4:是否有想过,java还有很多语言都号称是面向对象的语言,但是我们好像依然是在面向过程编程?
5:我们平时谈的微服务是什么,仅仅只是一个个拆分后的系统实例么
6:我们做微服务的时候,如何拆分是否有成型的方法论
如果您有以上疑问,请继续往后,同时文章的末尾会对以上问题做解答。
1:领域驱动设计是什么
领域驱动设计(DomainDriven Design,DDD)是EricEvans2004年提出的从系统分析到软件建模的一套方法论。它要解决什么问题呢?就是将业务概念和业务规则转换成软件系统中概念和规则,从而降低或隐藏业务复杂性,使系统具有更好的扩展性,以应对复杂多变的现实业务问题。
是不是听起来很诱人?并且你也会感觉到理所当然,因为我们就是需要这样的一套体系。
现在我们在落地领域驱动设计的时候,我们经常会花很多的时间去讨论微服务,限界上下文怎么划分,对象怎么建模,代码怎么去分层和调用。
最初我觉得特别浪费时间,但是过往的经验又会告诉我们,很多细节上花更多的时间是值得的。
2:为什么用DDD
为什么用DDD这个问题,我先放两张图这里,按下不表,之后会再给大家分析一下这张图可能大家印象会更加深刻一点。
领域驱动设计并不是万金油,他不能解决所有的问题,但是我们的代码得有设计,而最重要的也是设计本身,如果你现阶段还是靠经验去设计你的代码,何不跟我们一起了解一下领域驱动设计。
其实在thoughtworks,盒马,美团点评,还有京东的7fresh都有做过DDD的实践,盒马的张群辉就说过,其实阿里不在乎你用的是什么设计,可以是DDD也可以是别的,但是你得有设计,而DDD就提供了这样一套方法论。
其实说简单一点,领域驱动设计就是面向对象的进阶。
3:重塑我们的思维模式
面向对象大家都再熟悉不过了,因为JAVA就是面向对象的语言。面向对象最基本的封装,继承,多态还有遵循着内聚的原则,这些在领域驱动都是成立的,虽然JAVA是面向对象的语言,但是在实际开发的过程中,我们都是在面向过程去开发,写代码的方式非常简单从controller到service然后一直访问到数据库。当我们提到领域驱动的时候,一般会拿它和数据驱动做一个对比。回忆一下我们日常的开发,我们都会先设计表,围绕着这个表再去做业务逻辑的开发。
3.1:忘掉你的数据库
现在我们做一个假设,假设你的机器内存无限大,永不断电,永不宕机,我们还需要数据库么?答案是不需要,数据库其实只是一个外部设备,他是我们在技术不够发达的时候对持久化的一种妥协。我们设计模型的时候,过多的把“数据库”这个不相干的因素带进来了这其实是不恰当的做法,我们的模型需要的是持久化无关设计。对象模型对应领域模型。接下来我们做一个直接的对比。
3.2:构造具有业务含义的领域对象
可能上图的对比不太形象,我们可以结合代码看一下,假设有一个需求,有一个狗,叫布鲁,需要摇尾巴十次,如果只有get和set方法可能写出来的就是左边的代码,如果真正做到面向对象编程,那么理论上代码应该是右边的样子。
在起初学代码的时候,我们new出来的狗和猫都是有属性有动作(方法)的,可是在WEB开发的过程中,我们写出来的代码渐渐地只有get和set方法,让对象仅仅成为了数据的载体,而不是一个具有行为和属性活灵活现的对象,渐渐地我们仿佛遗失了什么。
再回头看左边的代码,想象一下,它出现再我们的service层里的时候,一直没有尾巴,没有名字的狗被生了出来,然后凭空有生了只尾巴装上去,取了个名字set进去,然后被service层这个上帝之手将尾巴拨来拨去10次。是不是画面就很惊悚的,我们的狗狗应当生而完整,也应当拥有对自己尾巴的控制权,这才是真正符合客观世界规律的代码,才是真正的面向对象编程。
3.2:贫血模型和充血模型
传统的数据库对象内部只有单纯的get/set方法,这样的模型在领域驱动里面成为贫血模型。get,set方法没法反映出业务的逻辑。而充血模式下的对象才是行为饱满的领域对象,栩栩如生,温婉动人。
如上图对于一个用户对象举例,左边是贫血模型构造的对象,右边是充血模型构造的对象,我们可以简单对比一下优劣。
其实右图这个对象依然不完善,因为对于user来说,它的密码应该是不对外公开的,即使是密文的,也不应该泄漏出去,所以password的get方法也是不可以对外部暴露的,关于密码的修改和校验都应该是这个对象内部自己完成的。
贫血模型就像一个被service这个木偶绳摆弄的傀儡,是不健康的,贫血症会引发失忆症,在service里调用了几十个set方法以后,我们自己往往都会忘了这一坨代码是干什么的,但是在行为饱满的领域对象中,每一个方法名明确地表明了这个方法的意图,并且严格保证了对象的完整性和正确性。
不论是面向对象,还是职责驱动设计,领域驱动设计在这里都是一致的,希望对象是自治的,其实领域驱动并不是银弹,并且一百个人写出的领域驱动代码真的都是有一百种样子,实践的过程中也会有很多问题,还是那句话,不管是什么设计,但是我们得有设计。
3.3:限界上下文拆分
是不是我们有了行为饱满的对象,一切就万事大吉了呢?不是的,请看下图构建的订单对象。
我们可以看到这个充血模型设计的订单对象也是有缺陷的,它包含了太多的语义,面向对象的设计有一个原则,叫做迪米特法则,又叫最小知识原则。图中这个对象就成了上帝对象,godobject。如果有一个新的需求过来,需要在物流里支持自提,那么我们还得去改订单,如果加上了一个分销代理的错需求,我们还是在订单上面修修补补。当这种对象在我们系统里变多了以后,整个系统就会变成了一个大泥球,往往在新增需求以后,牵一发动全身,该都不敢改,同时测试面也会很广,很难覆盖到所有的场景。
那么我们应该怎么办?拆!呢么怎么拆才合理呢?你说的不算,我说的也不算,我们需要协作完成,完成这个的方式就是领域驱动设计,在战术战略设计的环节提到的【事件风暴】。事件风暴是一种和领域专家,FE,UI,QA一起协作的一套需求分析和拆解的方法论,简单而言是通过梳理用户路径对各个环节的统一语言以及事件进行整理分析,提炼限界上下文从而做到微服务的拆分。感兴趣的同学可以查阅相关的文章,后续咱们也会有分享。
3.4:统一语言
接下来就是领域驱动设计中最重要的一个概念,叫统一语言(UBIQUITOUS LANGUAGE),所有语言的流畅沟通需要我们统一语言和语境,代码和现实世界之间亦如此。这样我们需要做到业务模型和软件模型相关联,同时相互提炼新的知识做补充,以此达到模型完善和拥抱变化。
3.5:六边形架构(游泳圈架构)
写了太久的MVC三层架构,这种直截了当的做法的确很有利于后续的迭代速度,但是这种模式并不能给我们彻底带来不稳定的隔离,数据库DAO层的代码充斥在service方法里,不明确的边界带来各种问题。DDD的战术设计里提到了一种六边形架构,也符合整洁架构里提出的【游泳圈架构】或者是【洋葱架构】,这是一张我奉为圭臬的架构图,后续会对整体的架构做探讨还有架构的演进做一些分析。
4:DDD学习门槛
如果你看到了这里,其实就已经对DDD有了较大的兴趣,本篇文章只是带你们简单了解了DDD的一些更加符合客观现实的一些方向,还有很多内容是没有提到的。学习DDD需要克服内心上的对很多专业术语的描述,不可否认,DDD提出了很多我们可能不太了解的概念,这些概念往往让人望而却步。光战术设计(编码落地)阶段随手罗列就会有一堆概念名词。
但是那又怎样呢,如果心里认为它是对的,为什么不去实践呢?高山仰止,景行行止,虽不能至,心向往之。
5:回到开始的问题
这张图想表达的意思是,微服务,DDD还有现如今很火的中台战略其实之间是有着非常相似的关系,他们都需要将业务逻辑沉淀,内聚,将变化隔离到外面。
右边的图是一个随着系统复杂度的提升,在代码编写难度上带来提升的一个曲线图,可以看到系统复杂度高的情况下,使用领域建模带来的复杂程度提升比较稳定,或许我们的项目是在一定的难度之下,但是又何妨我们去对自己提出更高的要求呢?
其实看到这里,相信开始提出的几个问题也就有答案了,至于为什么我们的代码里充斥着get和set方法,大概是在原始的ORM框架里,必须通过get和set方法做数据的赋值和提取,亦或是开源的框架里大多是简单API的操作,于是大家也就照猫画虎挪用过来,具体原因也就不得而知了。
6:结语
这一次没有展开提到DDD的一些概念和问题,只是简单从我们日常开发的角度和DDD做了一下对比,之后也会写相关的文档去和大家一起探讨和学习领域驱动设计,希望大家可以喜欢。