跨越DDD从理论到工程落地的鸿沟

DDD作为一种优秀的设计思想,的确为复杂业务治理带来了曙光。然而因为DDD本身难以掌握,很容易造成DDD从理论到工程落地之间出现巨大的鸿沟。就像电影里面的桥段,只谈DDD理论姿势很优美,一旦工程落地就跪了……所以DDD的项目,工程落地很重要,否则很容易变成“懂得了很多道理,却依然过不好这一生”

0d71842ff9919676f8eac9bfb82837e4.png

如上图所示,通过服务划分,我们可以聚焦在一个大系统下的一个Bounded Context里面,从而把原来大而复杂的问题空间,划分成多个小的、可以理解的小问题域。

如何把一个大的模型分解成小的部分没有什么具体的公式。如果非要给服务划分一个评判标准的话,那么这个评判标准应该是高内聚低耦合。

高内聚体现在要尽量把那些相关联的以及能形成一个自然概念的因素放在一个模型里。如果我们发现两个服务之间的交互过于紧密,比如有非常频繁的API调用或者数据同步,那么这两个域可能都不够内聚,放在一起可能会更好。

低耦合是和内聚性相对应的,如果领域不够内聚,他们之间的耦合自然就高了,如果两个服务,界限不清晰,领域高度重合,就会造成了严重的耦合问题。

系统耦合是一方面,人员耦合是另外一个考量因素。总体上来说,我不提倡微服务(Bounded Context)划分太细,因为服务太多,会加重运维成本。但服务也不能太粗,试想一下,如果一个服务需要8个人去维护,在上面做开发。那么解决代码冲突,环境冲突,发布等待都将是一个问题。通常一个服务,只需要一到两个人维护是相对比较合理的粒度。

除了服务的粒度之外,关于领域的类型我们也有必要去了解一下,领域的类型划分旨在帮助我们理解领域的主次之分,从而知道什么是我当前Bounded Context的核心。在DDD中,领域被分成三种类型。

  1. 核心域(Core Domain),顾名思义这是我领域的核心。有一点需要注意,Core的概念是随着你视角的变化而变化的。对于本领域来说是Core,对于另外一个领域而言可能只是Support。

  2. 支撑子域(Supporting Subdomian),虽然不是当前问题的核心领域,但也是必不可少的。比如授信子服务离不开客户信息,所以客户服务是授信服务的支撑子域。

  1. 通用子域(Generic Subdomain),如果一个子域被用于整个业务系统,那么这个子域便是通用子域。通常像账号、角色、权限都是常见的通用子域,每个系统都需要。

1.3 上下文映射

通过上面的战略设计,一个大型业务系统,会被划分成多个各自独立的Bounded Context,也就是多个微服务,这些服务需要互相协作,来完成完整的业务功能。

每一个限界上下文都有一套自己的“语言”,如果在该领域要使用其它领域的信息,我们就需要一个“翻译器”,把外域信息翻译成本领域的概念。这个在不同领域之间进行概念转化、信息传递的动作就叫上下文映射(Context Mapping)。上下文映射主要有两种解决方案:共享内核和防腐层。

所谓的共享内核(Shared Kernel),是指把两个子域中共同的实体概念抽取出来,形成一个组件(java中的jar包),然后通过内联(inline)的方式,分别被不同的子域使用。

d88913daddfede9e9c9cec1cd4b4123d.png

AC的做法有一定的代价,因为你要做一次信息转换,把外域的信息转成本域的领域概念。其好处是双方都拥有了更大的自主权和灵活度。系统架构就是这样,我们永远要在重复(Duplication)耦合低和复用(Reuse)耦合高之间取一个折中,进行权衡

1.4 领域模型

领域模型将现实世界抽象为了信息世界,把现实世界中的客观对象,抽象为某一种信息结构,而这种信息结构并不依赖于具体的计算机系统。它不是对软件设计的描述,它是和技术无关的(Technology-Free)。

例如,电商的核心领域模型就是商品、会员、订单、营销等实体,和你使用什么技术实现是没有关系的,你用Java可以实现,用PHP,GO也能实现。但不管是哪种技术实现方式,都不应该影响我们对领域模型的抽象和理解。

正因为领域模型的技术无关性,并且领域模型是我们的核心,这才有了洋葱圈架构,即领域模型处在架构的最内核,并且不依赖任何外围的技术细节。

9bdb7d67e1f45c924ccec884eb52079e.png

虽然可以退化,但不应该成为你轻易放弃Domain层的理由。据我观察,很多同学不喜欢DDD,其根本原因还不在于对象之间的转换成本(实际上,这个转换成本也没那么大),而在于他不清楚Domain的职责,不知道哪些东西应该放到Domain里面。一种典型的错误做法是把所有的业务逻辑都放到了Domain层,包括我们上面说的CRUD统统放到了领域层,这样的DDD当然没人喜欢

2.3 把业务逻辑都写进Domain/strong>

每当我看到同学把所有业务逻辑都写进Domain层,我就会问他,“你这样把App层的所有业务逻辑都搬到Domain层,能得到什么益处呢把这些代码直接放在App层的区别在哪里呢且,放在App层,因为少了一个层次,代码会更加简单,为什么要劳心劳力地再加一个Domain层呢

“那要怎么办呢同学一边点头一边疑惑地问。

我给的方案是“先把App做厚,再把App做薄”。什么意思是我们先可以把业务逻辑都写到App里面,在写的过程中,我们会发现有一些业务逻辑,不仅仅是过程式的代码,它也是领域知识(Domain knowledge),应该被更加清晰、更加内聚的表达出来,那么我们就可以把这段代码沉淀为领域能力

举一个例子,还是以用户注册为例,一开始,正如我们一直这样做的,直接在App层写出如下的过程代码:

写好后,我们再回过头来审视一下,看看哪些东西可以沉淀为领域能力,然后优化我们的代码。

我们先看年龄和国籍校验,年龄和国籍都是customer的属性,那么谁对它们最熟悉呢然是customer自身了,对于这样的业务知识,无能是从可理解性的角度,还是从功能内聚和复用性的角度,把它们沉淀到customer身上都会更合适,于是,我们可以在customer实体上沉淀这些业务知识:

健康码有点特殊,虽然它也是Customer的健康码,但是它并不存在于本应用中,而是存在于另一个服务中,需要通过远程调用的方式来获取。这在我们的分布式系统中,是非常常见的现象,即我们要通过分布式的服务交互来共同完成业务功能。

如果直接调用外部系统,基于外系统的DTO,当然也能完成代码功能,但这样做会有三个问题:

  1. 表达晦涩,我只是要检查一下健康码,却有一堆的代码。(这只是示意,真实的远程调用肯定要比这个代码多)

  2. 复用性差,校验健康码不仅仅客户注册会用到,可能很多客户相关的操作都会用到,难道都要这么写一遍/p>

  1. 没有防腐和隔离,HealthCodeResponse不是我这个领域的东西,怎么能让它如此轻易的侵入到我的业务代码中呢/p>

解决上面的问题,我们就可以充分发挥Domain层的边界上下文(Bounded Context)的作用,使用上下文映射(Context Mapping),把外领域的信息映射到本领域。即我可以认为HealthCode就是属于Customer的,至于这个HealthCode是怎么来的,那是Gateway和infrastructure要帮我处理的问题,它可能来自于自身的数据库,也可能来自于RPC的远程调用,总之那是infrastructure要处理的“技术细节”问题,对于上层的业务代码不需要关心。

按照这样的思路,我们可以新建一个HealthCodeGateway来解开对健康码系统的耦合。

于此同时,把如何获取HealthCode这样的技术细节问题丢给infrastructure去处理。

来源:张建飞(Frank)

声明:本站部分文章及图片转载于互联网,内容版权归原作者所有,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2022年2月1日
下一篇 2022年2月1日

相关推荐