复杂软件解决之道[5]灵魂拷问,我理解的DDD是DDD吗?

分布式朝闻道

共 4585字,需浏览 10分钟

 ·

2021-04-12 16:29

DDD领域驱动设计今年真的是大火了,这是好事,表明我们的软件开发领域是一直在向前发展的。

但是很多的文章或者培训,都是在说DDD如何如何优秀,简单的举一些例子就行了,有些甚至是完全错误的。

因此笔者决定将自己的实践心得以及与他人讨论的关于DDD的问题整理为一篇文章,通过问题驱动的方式,将领域驱动设计中的一些注意点进行总结,希望能够对读者有所帮助。

主要的问题如下:

  • 到底什么才是统⼀语⾔,它有那么重要么?
  • 领域驱动设计仅仅需要开发者参与吗?
  • 领域驱动设计的那些个概念到底是在说什么?实体和值对象有啥区别?
  • DDD四层架构的好处有哪些?
  • 限界上下文如何划分比较好呢?有没有工具推荐?
  • 聚合粒度如何控制呢?一个聚合根就够了,为什么还要细分各种子域?
  • ACL(防腐层)应该怎么用?它的作用和优势有哪些?它在DDD分层中处于哪个位置?

到底什么才是统⼀语⾔,它有那么重要么?

统⼀语⾔,并不是某种具体的标记。⽽是针对不同的业务需求场景,由领域专家、开发⼈员、需求提出⽅、客户(有时候没有该⻆⾊)共同参与讨论,提炼出的领域知识的产出物。

这套语⾔,可以理解为是不同⻆⾊间同步信息的媒介,⼤家都认同它的定义和含义,只要是基于这套共 识机制进⾏讨论,那么就能较为顺畅的推进需求迭代。

实际上,统⼀语⾔在DDD中不是单独存在的,⽽是⼀个中间产物,得到它的过程伴随着领域建模的事件⻛暴过程。

其他建模⽅式有:名词动词法、四⾊建模法、职责驱动法、事件驱动法等,这些场景属于领域建模的「战略设计部分」,因此我们不在这⾥做过多的解释。

以事件风暴建模过程为例,举⼀个统⼀语⾔的例⼦:

绿⾊代表 实体

蓝⾊代表 ⾏为

橙⾊代表 事件


图中,我们对账户信息进行了事件风暴,围绕着账户的行为进行了定义,主要有:

  • 创建账户
  • 查询账户信息
  • 账户充值
  • 账户扣款
  • 账户退款
  • 账户冻结等行为

这些行为本身又会触发一些事件的发送,主要有以下事件:

  • 账户已创建
  • 账户已充值
  • 账户已扣款
  • 账户已退款
  • 账户已冻结

领域驱动设计仅仅需要开发者参与吗?

当然不是。领域驱动设计建模过程需要多⽅参与,最后的落地主要是研发参与。

在建模过程中,往往需要以下⻆⾊共同作⽤:

  • 领域专家(有些时候,领域专家是产品经理,运营兼职的,甚⾄可能直接由RD同学扮演领域专家)
  • 开发⼈员
  • 需求提出⽅(如:运营 产品)
  • 客户(有些场景下 产品/运营扮演⻆⾊)

总的来说,领域驱动设计是⼀个团队⾏为,并⾮开发者单独参与。

领域驱动设计的那些个概念到底是在说什么?

领域驱动设计的概念主要涉及到这些,我们可以全⾯了解,有选择性的使⽤。实际在开发中常⽤的也就那么⼏个。

这⾥单独说⼀下「实体」「值对象」吧:

  • 实体有主键ID,值对象没有;
  • 值对象本质上就是⼀个数据集合,⽐如说:收货地址、家庭住址就可以⽤值对象表达,只对属性进⾏归类,没有唯⼀ID;
  • ⼈员信息,⽤实体表达,原因在于:我们可以对⼀个实体对象进⾏多次修改,修改后的数据和原来的数据可能会⼤不相同。但是,由于它们拥有相同的 ID,它们依然是同⼀个实体。

DDD四层架构的好处有哪些?

其实DDD四层架构的本质还是分层架构,而软件分层架构本质上还是 「通过层来隔离不同的关注点」 (变化相似的地方),以此来解决不同需求变化的问题,使得这种变化可以被控制在一个层里。

因此我认为最大的好处还是分离关注点,屏蔽因下层变更导致的上层变更,使得层间松耦合。

限界上下文如何划分比较好呢?有没有工具推荐?

重点还是要基于战略设计中的 「事件风暴」,即event storming,这个过程可以多做几次。尽量不要一个人闭门造车,要带着产品,领域专家一起把这个过程做好,做透。

因为从本质上说,事件风暴这个事情没有好或者不好,但是要保证限界上下文的划分是产研和领域专家的共识。

因为每个人对一个领域的认识没有对错,只有深度和角度的不同,甚至于你在不同的时间,团队,对同一个业务,限界上下文的划分也不完全一致。所以说,衡量的标准是,团队协作过程中不同角色间是否达成了共识。

详细展开说,首先还是要建立统一语言,达成共识,及时调整,多做沟通。然后多去推演,发现不合理的地方随时修改。

退一步讲,即便因为条件所限,没有进行标准的领域风暴流程,设计者自己也可以模拟这个过程,把领域,值对象,事件,都拆分出来,平摊在一个版面上,

  • 通过分类划分不同的限界上下文;
  • 在每个上下文里找到聚合根

总归还是需要走一次这个过程,只不过有时候可能参与的人比较多,有时候就开发自己去做这个事情了。

而且,对领域驱动设计而言,它还是蛮兼容并包的,尤其是战略设计阶段,我们完全可以利用已知的工具去辅助建模,比如说强大的UML语言,使用何种工作是没有一个统一标准的,只要能够完成建模的目标,都是合理的。

至于最终落地的时候采用哪种架构方式,就看团队喜好了,一般还是建议用分层架构,比较容易上手。

实际上在我们的实践过程中发现,想要达到纯正的领域,驱动建模还是比较复杂的,就是说你总归有一些代码是胶水代码,这些胶水代码实际上我们最终还是放到了一些service里面,通过这些service然后去串联各个不同的聚合。

我的建议就是说建模的时候尽量采用纯领域驱动去做(尽量采用充血模型),然后在实际操作的时候还是可以使用一些传统的事务脚本式的编程方法,通过核心的领域驱动建模,加上流程化的穿插去完成建模和开发。我们这种采用的是折中后的充血模型,就是没有采用完全的充血模型,而是半充血,通过application Service将不同领域的能力归拢起来,将流程化的操作沉到domain Service,然后去调用实体本身的行为去完成业务操作。

这种方式在实践中证明是可行的,而且接受度高,参与开发的同学能够很快上手。

总结很赞,一定要结合本公司、业务、团队、产品的实际情况。

总之,限界上下文的划分的确是初期最重要的事情,这件事情需要参与的每个人付出非常多的思考,如果在战略设计阶段产、研、领域专家这个共识没有建立,后面的工作很容易走样。

聚合粒度如何控制呢?一个聚合根就够了,为什么还要细分各种子域?

首先我们要明确的是DDD中的方案和思路是从业务领域建模出发的,而DDD最大的特点和所谓缺陷 也是领域建模。我们以下单过程中,保存一个订单为例来讨论这个问题。

如果建模不合理,导致出现一个较大的聚合根,那么从聚合根触发的一个save操作必然需要同时联动多个实体。

一般来说,为了保证聚合内实体状态一致,我们还是会采用和事务脚本编码 类似的本地事务,我默认你用的是Spring,当然或多或少会有性能问题,至于影响多大,我觉得我说了不算,实践是检验真理的唯一标准。

另一方面,如果是跨聚合的,领域事件就派上用场了。我发表一个不成熟的观点,「DDD本身就是反性能的」,为了高性能,聚合太充血以及聚合过于复杂,本身就是违背对性能要求的初衷。

所以save类操作,还是需要控制一下事务的粒度,但是根本上还是要控制聚合的粒度 防止出现一个上帝对象。

另一方面,如果是跨聚合的,领域事件就派上用场了,这时候追求的是最终一致,不太适合采用强一致的事务。

因此还是需要case  by case,结合具体的案例去分析,方案是有很多的。

甚至在非交易场景下,放弃事务也是一种方式,比如,异步持久化。失败重试,异步补偿。

如果交易类场景,聚合过于复杂的情况下,单机性能提升确实不明显,这是DDD的天然特性(说的难听点,这是DDD的问题)。

ACL(防腐层)应该怎么用?它的作用和优势有哪些?它在DDD分层中处于哪个位置?

首先我们要明确的是,对于跨领域的调用,我们要用到ACL。

严格的做法是,对于所有跨域的调用都必须要经过acl,去跟对方的application层进行交互,一句话就是 「通过application去聚合领域的内部能力,通过acl去防腐,通过事件去进行广播,进行异步交互,通过CQRS来进行一些读服务的优化」

ACL具体如何使用比较优雅?

我们的实践是,面向接口编程。具体的做法是,将对外访问通过接口提取出一个抽象,将抽象的接口和具体的业务逻辑放在同一层,如:可以放在domainService中。

而ACL接口的实现类则要放在基础设施层,因为基础设施层在DDD的六边形架构中,处于南向网关的位置,而ACL本身是对外的调用,因此属于本业务领域的出口,也就是南向网关。

举个例子,比方说你的一个orderDomainService需要去调用你的账户account ApplicationService这些能力,那么你就需要在你的订单的域内去定义一个账户的acl接口,然后你用你的订单的域去依赖这个acl接口,但是你acl的实现要放在你的infrastructure里面去,如图所示:

为什么这么做呢?其实它本质就体现了一种面向抽象编程的思想。也就是说 「高层模块不应该依赖低层模块,二者都应该依赖其抽象」

也许有的读者会疑惑,对于domainService和applicationService,感觉他们没什么区别,业务逻辑写在哪儿都差不多。

其实原因很简单,这恰恰说明你面对的业务并不复杂,一个领域实体就是一个聚合根、然后一个领域服务就是一个应用服务,因此看起来applicationService就是domainService的委派。

对于复杂一些的场景,一个聚合根下需要联动超过2个子域,就需要用applicationService对多个domainService进行组合,组合的结果就是一种复合服务,说的高端一些,这样的一个applicationService就是沉淀了一组领域能力。而这样的applicationSerivce就是在对domainSerivce进行编排。

其实你的application层本质上是一种聚合层,它的作用是用来协调整合下面的domain层,然后基于这些domain具体的细分的原子能力进行组合,组织成复合能力并且进行一些事件的发送和通知。

所以我觉得叫application层,叫它聚合能力层也是可以的。我们在一些复杂业务流程中,用到了一些类似于服务编排的一些功能或者方式吧,这里编排的实际上就是application。

其实对于一些复杂的业务来说,你的applications其实是没有事务的,因为你的事物全部都拆分为若干小事务,下沉在各种domainService里面去了,然后你的application层是去聚合这样一个一个的小事务,完成一个复杂的事情,并且在完成了这些事情之后,发送一些事件通知来通知别的领域,或者说别的上下文中的某些词语去完成一些他们自己的工作,这种时候发送的事件就不是简单的领域内事件,而是一些跨领域的事件。

而领域事件本质上是一种最终一致性的事务实现机制。

回到问题上来,我们总结一下:

  • 凡是需要提供出去的能力都是自己的领域模型和自己的领域服务;通过application层进行领域能力的聚合和沉淀;
  • 凡是从别人那里要来的东西,都需要经过防腐层ACL进行屏蔽和处理,以及执行实体转换等二次加工逻辑。


浏览 78
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报