DDD作为一种优秀的设计思想,的确为复杂业务治理带来了曙光。然而因为DDD本身难以掌握,很容易造成DDD从理论到工程落地之间出现巨大的鸿沟。就像电影里面的桥段,只谈DDD理论姿势很优美,一旦工程落地就跪了…所以DDD的项目,工程落地很重要,否则很容易变成“懂得了很多道理,却依然过不好这一生”。
这篇文章,我会从DDD的核心概念讲起,但重点会讲如何把理论落地成代码,期望给那些正在探索DDD的同学一些指引和启发。
一 DDD的核心概念
DDD难以掌握的原因之一是因为其涉及很多概念,比如像Ubiquitous Language、Bounded Context、Context Mapping、Domain、Domain Service、Repository、Aggregation root、Entity、Value Object等等。这里简要介绍一下DDD的核心概念,了解这些概念可以更好地帮助我们落地DDD。
1.1 DDD概念
DDD(Domain Driven Design) 领域驱动模型,是一种处理高度复杂领域设计思想,不是一种架构,而是一种架构设计方法,是一种设计模式。说白了就是把一个复杂的软件应用系统中各个部分进行拆解和封装,以达到高内聚低耦合的效果。
1.2 战略设计
对领域内的名词,动词分析,提取领域模型。官方解释:在某个领域,核心围绕着让下文的设计,主要关注上下文的划分,上下文映射的设计,通用语言的设计。
说白了就是: 在某个系统,核心围绕着子系统的设计,主要关注子系统的划分,交互方式和核心术语的定义。
1.2.1 事件风暴
事件风暴的基本思想,就是将软件开发人员和领域专家聚集在一起,完成领域模型设计(领域分析和领域建模)。划分出微服务逻辑边界和物理边界,定义领域模型中的领域对象,指导微服务设计和开发。
- 领域分析:是根据需求划分出初步的领域和限界上下文,以及上下文之间的关系;然后分析每个上下文内部,抽取每个子域的领域概念,识别出哪些是实体,哪些是值对象;
- 领域建模:就是对实体、值对象进行关联和聚合,划分出聚合的范畴和聚合根;
DDD需要进行领域分析和领域建模,除了事件风暴之外实现的方法有,领域故事讲述,四色建模法,用例法等。
事件风暴是建立领域模型的主要方法,但是在 DDD 领域建模和系统建设过程中,有很多的参与者,包括领域专家、产品经理、项目经理、架构师、开发经理和测试经理等。对同样的领域知识,不同的参与角色可能会有不同的理解,那大家交流起来就会有障碍,怎么办呢?因此,在 DDD 中就出现了“通用语言”和“限界上下文”这两个重要的概念。
1.2.2 问题空间
问题空间属于需求分析阶段,重点是明确这个系统要解决什么问题,能够提供什么价值,也就是关注系统的What与Why。
问题空间将问题域提炼成更多可管理的子域,是真对于问题域而言的。问题空间在研究和解决业务问题时,DDD 会按照一定的规则将业务领域进行细分,当领域细分到一定的程度后,DDD 会将问题范围限定在特定的边界内,在这个边界内建立领域模型,进而用代码实现该领域模型,解决相应的业务问题。简言之,DDD 的领域就是这个边界内要解决的业务问题域。
领域可以进一步划分为子领域。我们把划分出来的多个子领域称为子域,每个子域对应一个更小的问题域或更小的业务范围,每个子域又包含了核心子域,支撑子域,通用子域。
领域的核心思想就是将问题域逐级细分,来降低业务理解和系统实现的复杂度。通过领域细分,逐步缩小服务需要解决的问题域,构建合适的领域模型。
说白了就是,就是系统下面有多个子系统,就是分了一些类型,比如电商系统,订单就是核心领域,支付调用银行,支付宝什么的就是支撑子域,相当于我们俗称的下游,通用子域,就是一些鉴权,用户中心,每个系统都会用到,就设计成通用子域,关键就是讨论过程如何得出这些问题域,是战略设计要解决的。
1.2.3 统一语言
Eric Evans在解释DDD本质的时候,重点提到“Exploration and reshaping the ubiquitous languages",也就是探索并重塑统一语言。统一语言是DDD中非常重要的概念,因为语言是我们认知的基础,语言都不统一,就像一个人说阿拉伯语,一个人说汉语,那怎么能交流的起来呢?
对于统一语言,我建议每个项目都要有一份自己的核心领域词汇表。这个词汇表至少要包含中文、英文、缩写、解释四列,中文是我们日常交流和文档中经常要体现的,所以需要统一,这样我们在交流的时候才能高效,没有歧义;英文和英文缩写主要体现在我们的设计和代码上,也就是说我们的“统一语言”不仅仅是停留在交流和文档中,还要和代码保持一致,这样才能做到知行合一,提升代码的可读性和系统的可理解性。
比如一个CRM系统,我们可以从业务需求中挖掘出一些重要的领域概念,把这些概念整理成词汇表会如下所示。
有了这个核心领域词汇表,以后团队的交流、文档、设计和代码都应该以这个词汇表为准,这里需要注意的是,词汇表中英文对中文的翻译不一定非常“准确”,不过没关系,语言就是一个符号,共识即正确,只要大家容易理解达成一致即可。就像上面词汇表中私海这个概念的翻译是Private Sea,这是一个典型的Chinglish,正统的翻译是Territory,但是大家都认为Private Sea更容易理解,只要达成共识,用这个名称也挺好。
1.2.4 界限上下文
大型软件系统的单体结构很难应付日益膨胀的复杂度。和解决所有复杂问题一样,除了分而治之,各个击破,别无他法。事实证明,对于微服务的边界划分使用DDD的战略设计是一个有效手段。AWS全球云架构战略副总裁Adrian Cockcroft就曾说过:Microservices is a loosely-coupled, service-oriented architecture with bounded context.(微服务就是在限界上下文下的松耦合的SOA。)
如上图所示,通过服务划分,我们可以聚焦在一个大系统下的一个Bounded Context里面,从而把原来大而复杂的问题空间,划分成多个小的、可以理解的小问题域。如何把一个大的模型分解成小的部分没有什么具体的公式。如果非要给服务划分一个评判标准的话,那么这个评判标准应该是高内聚低耦合。
- 高内聚体现在要尽量把那些相关联的以及能形成一个自然概念的因素放在一个模型里。如果我们发现两个服务之间的交互过于紧密,比如有非常频繁的API调用或者数据同步,那么这两个域可能都不够内聚,放在一起可能会更好。
- 低耦合是和内聚性相对应的,如果领域不够内聚,他们之间的耦合自然就高了,如果两个服务,界限不清晰,领域高度重合,就会造成了严重的耦合问题。
系统耦合是一方面,人员耦合是另外一个考量因素。总体上来说,我不提倡微服务(Bounded Context)划分太细,因为服务太多,会加重运维成本。但服务也不能太粗,试想一下,如果一个服务需要8个人去维护,在上面做开发。那么解决代码冲突,环境冲突,发布等待都将是一个问题。通常一个服务,只需要一到两个人维护是相对比较合理的粒度。
除了服务的粒度之外,关于领域的类型我们也有必要去了解一下,领域的类型划分旨在帮助我们理解领域的主次之分,从而知道什么是我当前Bounded Context的核心。在DDD中,领域被分成三种类型。
- 核心域(Core Domain),顾名思义这是我领域的核心。有一点需要注意,Core的概念是随着你视角的变化而变化的。对于本领域来说是Core,对于另外一个领域而言可能只是Support。
- 支撑子域(Supporting Subdomian),虽然不是当前问题的核心领域,但也是必不可少的。比如授信子服务离不开客户信息,所以客户服务是授信服务的支撑子域。
- 通用子域(Generic Subdomain),如果一个子域被用于整个业务系统,那么这个子域便是通用子域。通常像账号、角色、权限都是常见的通用子域,每个系统都需要。
1.2.5 上下文映射
通过上面的战略设计,一个大型业务系统,会被划分成多个各自独立的Bounded Context,也就是多个微服务,这些服务需要互相协作,来完成完整的业务功能。
每一个限界上下文都有一套自己的“语言”,如果在该领域要使用其它领域的信息,我们就需要一个“翻译器”,把外域信息翻译成本领域的概念。这个在不同领域之间进行概念转化、信息传递的动作就叫上下文映射(Context Mapping)。上下文映射主要有两种解决方案:共享内核和防腐层。
- 共享内核(Shared Kernel):是指把两个子域中共同的实体概念抽取出来,形成一个组件(java中的jar包),然后通过内联(inline)的方式,分别被不同的子域使用。
共享内核的最大好处是代码复用和能力共享,然而坏处也很明显,即高耦合:任何对于“共享内核”的改动都要小心翼翼的协调两个领域的技术团队,且会影响两个领域。说实话,这个副作用有点伤不起,所以在实践中,更推荐的上下文映射方法是防腐层。 - 防腐层(Anti-Corruption,AC):是指在一个领域中,如果需要使用其它领域的信息,可以通过AC进行防腐和转义。实际上,在微服务的环境下,服务调用是一个普遍的诉求,因为没有一个服务是孤立的,都需要借助其它服务提供的数据,共同完成业务活动。
AC的做法有一定的代价,因为你要做一次信息转换,把外域的信息转成本域的领域概念。其好处是双方都拥有了更大的自主权和灵活度。系统架构就是这样,我们永远要在重复(Duplication)耦合低和复用(Reuse)耦合高之间取一个折中,进行权衡。
1.3 战术设计
领域模型内的设计和编码实现。官方解释:核心关注上下文中的实体建模,定制值和实体等,更偏向开发的细节。
说白了就是:上下文中的子系统的设计与实现。核心关注子系统的代码实现,以面向对象的思维设计类的属性和方法。
1.3.1 解决空间:
解决方案域属于系统设计阶段,针对识别出来的问题域,寻求合理的解决方案,也就是关注系统的How。在领域驱动设计中,核心领域(Core Domain)与子领域(Sub Domain)属于问题域的范畴,限界上下文(Bounded Context)则属于解决方案域的范畴。
说白了就是,得出这些问题域之后,就基于这些问题域来求解,属于解决空间。相当于,知道了y=2x,知道了x是多少,然后求y的值。解决空间就是指,领域之间的关系是什么样子,每个领域中通用的术语 ,具体在领域内怎么实现代码,进行领域建模就可以了。
从问题域到解决方案域,实际上就是从需求分析到设计的过程,也是我们逐步识别限界上下文的过程。
1.3.2 领域概念
从广义上讲,领域具体指一种特定的范围或区域。在DDD中上下文的划分完的东西叫作领域,领域下面又划分了,核心领域,支撑子域,通用子域。
子域:在领域不断划分的过程中,领域会细分为不同的子域,子域可以根据自身重要性和功能属性划分为三类子域,它们分别是:核心域、通用域和支撑域。
- 核心域(Core Domain),顾名思义这是我领域的核心。有一点需要注意,Core的概念是随着你视角的变化而变化的。对于本领域来说是Core,对于另外一个领域而言可能只是Support。
- 支撑子域(Supporting Subdomian),虽然不是当前问题的核心领域,但也是必不可少的。比如授信子服务离不开客户信息,所以客户服务是授信服务的支撑子域。
- 通用子域(Generic Subdomain),如果一个子域被用于整个业务系统,那么这个子域便是通用子域。通常像账号、角色、权限都是常见的通用子域,每个系统都需要。
说白了就是,系统下面有多个子系统,就是分了一些类型,比如电商系统,订单就是核心领域,支付调用银行,支付宝什么的就是支撑子域,相当于我们俗称的下游,通用子域,就是一些鉴权,用户中心,每个系统都会用到,就设计成通用子域,关键就是讨论过程如何得出这些域,是战略设计要解决的。
1.3.3 领域模型:
领域模型将现实世界抽象为了信息世界,把现实世界中的客观对象,抽象为某一种信息结构,而这种信息结构并不依赖于具体的计算机系统。它不是对软件设计的描述,它是和技术无关的(Technology-Free)。
例如,电商的核心领域模型就是商品、会员、订单、营销等实体,和你使用什么技术实现是没有关系的,你用Java可以实现,用PHP,GO也能实现。但不管是哪种技术实现方式,都不应该影响我们对领域模型的抽象和理解。
正因为领域模型的技术无关性,并且领域模型是我们的核心,这才有了洋葱圈架构,即领域模型处在架构的最内核,并且不依赖任何外围的技术细节。
这里顺便回答一下同学经常问的事务(Transaction)在哪里实现的问题,为了保持领域的技术无关性,事务最好被管理在App的Service中。
关于如何设计领域模型,简单来说,就是分析语言。这也是为什么我们一直在强调统一语言的重要性,因为只有真正的理解了业务,把重要的领域概念阐述清楚,才有可能设计出比较好的领域模型。
具体的建立领域模型的步骤,可以分为以下三步:
- 理解问题:我们需要用简短的语句把问题描述清楚。用户故事或者用例,是建模的关键前序动作。除了用户故事外,我们当然也可以使用事件风暴(Event Storming),四色建模法等手段,只是我觉得用户故事比较简单易行,所以推荐用这种方式。
- 挖掘概念(Digging out concepts):领域概念隐含在语句中,重点关注语句中的名词(nouns),因为nouns常常以为这重要的领域概念。这一步不容易做到,因为自然语言有很大的随意性,很多同义词、多义词混淆其中。而且,有些关键概念也不一定就是名词,也可能通过动词(verbs)进行伪装。
- 建立关联:寻找关系,需要关注动词(verbs)。因为关联意味着两个模型之间存在语义联系,在用例中的表现通常为两个名词被动词连接起来。
1.3.4 领域事件
聚合之间产生的业务协同使用领域事件的方式来完成,领域事件就是将上游聚合处理完成这个动作通过事件的方式进行抽象。
在DDD中有一个原则,一个业务用例对应一个事务,一个事务对应一个聚合根,也就是在一次事务中只能对一个聚合根操作。但在实际应用中,一个业务用例往往需要修改多个聚合根,而不同的聚合根可能在不同的限界上下文中,引入领域事件即不破坏DDD的一个事务只修改一个聚合根的原则,也能实现限界上下文之间的解耦。对于领域事件发布,在领域服务发布,在不使用领域服务的情况下,则由应用层在调用资源库持久化聚合根之后再发布领域事件。
一个事件可能当前限界上下文内也需要消费,即可能有多个限界上下文需要消费,一个事件对应多个消费者。
一个完整的领域事件 = 事件发布 + 事件存储 + 事件分发 + 事件处理。
- 事件发布:构建一个事件,需要唯一标识,然后发布;
- 事件存储:发布事件前需要存储,因为接收后的事建也会存储,可用于重试或对账等;就是每次执行一次具体的操作时,把行为记录下来,执行持久化。
- 事件分发:服务内的应用服务或者领域服务直接发布给订阅者,服务外需要借助消息中间件,比如Kafka,RabbitMQ等,支持同步或者异步。
- 事件处理:先将事件存储,然后再处理。
当然了,实际开发中事件存储和事件处理不是必须的。因此实现方案:发布订阅模式,分为跨上下文(kafka,RocketMq)和上下文内(spring事件,Guava Event Bus)的领域事件。
1.3.5 领域服务
聚合根与领域服务负责封装实现业务逻辑。领域服务负责对聚合根进行调度和封装,同时可以对外提供各种形式的服务,对于不能直接通过聚合根完成的业务操作就需要通过领域服务。
说白了就是,聚合根本身无法完全处理这个逻辑,例如支付这个步骤,订单聚合不可能支付,所以在订单聚合上架一层领域服务,在领域服务中实现支付逻辑,然后应用服务调用领域服务。
在以下几种情况时,我们可以使用领域服务:
- 对于不能直接通过聚合根完成的业务操作就需要通过领域服务。
- 在DDD中,每个实体只能操作自己实体的变化,不能改另一个实体的状态。跨实体的状态变化需要抽象出一个领域服务,不能直接修改实体的状态,只能调用实体的业务方法。
- 以多个领域对象作为输入参数进行计算,结果产生一个值对象。
- 执行一个显著的业务操作
- 对领域对象进行转换
遵守以下规范:
- 同限界上下文内的聚合之间的领域服务可直接调用
- 两个限界上下文的交互必须通过应用服务层抽离接口->适配层适配。
1.3.6 应用服务
应用服务作为总体协调者,先通过资源库获取到聚合根,然后调用聚合根或者领域服务中的业务方法,最后再次调用资源库保存聚合根。
作用:
- 除了同步方法调用外,还可以发布或者订阅领域事件,权限校验、事务控制,一个事务对应一个聚合根。
- 应用层方法主要执行服务编排等轻量级逻辑,尤其针对跨多个领域的业务场景,效果明显。
- 参数校验,简单的crud,可直接调用仓库接口
1.3.7 实体和聚合相关
1,值对象
官方解释,描述了领域中的一件东西,将不同的相关属性组合成了一个概念整体,当度量和描述改变时,可以用另外一个值对象予以替换,属性判等,固定不变。
说白了就是,不关心唯一值,具有校验逻辑,等值判断逻辑,只关心值的类。只有数据初始化操作和有限的不涉及修改数据的行为,基本不包含业务逻辑。比如下单的地址。
当你决定一个领域概念是否是一个值对象时,需考虑它是否拥有以下特征:
- 度量或者描述了领域中的一件东西
- 可作为不变量
- 将不同的相关的属性组合成一个概念整体(Conceptual Whole)
- 当度量和描述改变时,可以用另一个值对象予以替换
- 可以和其他值对象进行相等性比较
- 不会对协作对象造成副作用
- 当你只关心某个对象的属性时,该对象便可作为一个值对象。为其添加有意义的属性,并赋予它相应的行为。需要将值对象看成不变对象,不要给它任何身份标识, 还应尽量避免像实体对象一样的复杂性。
值对象本质上就是一个集。该集合有若干用于描述目的、具有整体概念和不可修改的属性。该集合存在的意义是在领域建模的过程中,值对象可保证属性归类的清晰和概念的完整性,避免属性零碎。
2,实体
官方解释,实体和值对象会形成聚合,每个聚合一般是在一个事务中操作,一般都有持久性操作。聚合中,跟实体的生命周期决定了聚合整体的生命周期。说白了,就是对象之间的关联,只是规定了关联对象规则(必须是由实体和值对象组成的),操作聚合时类似hibernate的One-Many对象的操作,一起操作,不能单独操作。
3,聚合
官方解释,实体和值对象会形成聚合,每个聚合一般是在一个事务中操作,一般都有持久性操作。聚合中,跟实体的生命周期决定了聚合整体的生命周期。说白了,就是对象之间的关联,只是规定了关联对象规则(必须是由实体和值对象组成的),操作聚合时类似hibernate的One-Many对象的操作,一起操作,不能单独操作。
聚合的规范:
- 我们把一些关联性极强、生命周期一致的实体、值对象放到一个聚合里。
- 聚合是领域对象的显式分组,旨在支持领域模型的行为和不变性,同时充当一致性和事务性边界。
- 聚合在 DDD 分层架构里属于领域层,领域层包含了多个聚合,共同实现核心业务逻辑。跨多个实体的业务逻辑通过领域服务来实现,跨多个聚合的业务逻辑通过应用服务来实现。
- 比如有的业务场景需要同一个聚合的 A 和 B 两个实体来共同完成,我们就可以将这段业务逻辑用领域服务来实现;而有的业务逻辑需要聚合 C 和聚合 D 中的两个服务共同完成,这时你就可以用应用服务来组合这两个服务。
在DDD中,聚合也可以用来表示整体与部分的关系,但不再强调部分与整体的独立性。聚合是将相关联的领域对象进行显示分组,来表达整体的概念(也可以是单一的领域对象)。比如将表示订单与订单项的领域对象进行组合,来表达领域中订单这个整体概念。
4,聚合根
聚合根(Aggreate Root, AR) 就是软件模型中那些最重要的以名词形式存在的领域对象。聚合根是主要的业务逻辑载体,DDD中所有的战术实现都围绕着聚合根展开。70%的场景下,一个聚合内都只有一个实体,那就是聚合根。
说白了就是: 聚合的根实体,最具代表性的实体。比如订单和订单项聚合之后的聚合根就是订单。
聚合根的特征:
- 它作为实体本身,拥有实体的属性和业务行为,实现自身的业务逻辑。
- 它作为聚合的管理者,在聚合内部负责协调实体和值对象按照固定的业务规则协同完成共同的业务逻辑。
- 聚合根之间的引用通过ID完成。在聚合之间,它还是聚合对外的接口人,以聚合根 ID 关联的方式接受外部任务和请求,在上下文内实现聚合之间的业务协同。也就是说,聚合之间通过聚合根 ID 关联引用,如果需要访问其它聚合的实体,就要先访问聚合根,再导航到聚合内部实体,外部对象不能直接访问聚合内实体。
简单概括一下:通过事件风暴(我理解就是头脑风暴,不过我们一般都是先通过个人理解,然后再和相关核心同学进行沟通),得到实体和值对象;将这些实体和值对象聚合为“投保聚合”和“客户聚合”,其中“投保单”和“客户”是两者的聚合根;找出与聚合根“投保单”和“客户”关联的所有紧密依赖的实体和值对象;在聚合内根据聚合根、实体和值对象的依赖关系,画出对象的引用和依赖模型。
5, 仓库
负责提供聚合根或者持久化聚合根。仓库帮助我们持久化整个聚合的,存一个对象会把相关对象都存下来。从技术上讲,Repository和DAO所扮演的角色相似,不过DAO的设计初衷只是对数据库的一层很薄的封装,而Repository是更偏向于领域模型。另外,在所有的领域对象中,只有聚合根才“配得上”拥有Repository,而DAO没有这种约束。
6, 工厂
比如说创建一个实体,里面有五个值对象组成,每次创建的时候都得new一次,这里用工厂简化,工厂帮助我们创建聚合。这一方面可以享受到工厂模式本身的好处,另一方面,DDD中的Factory还具有将“聚合根的创建逻辑”显现出来的效果。Factory有两种实现方式:
- 直接在聚合根中实现Factory方法,常用于简单的创建过程
- 独立的Factory类,用于有一定复杂度的创建过程,或者创建逻辑不适合放在聚合根上,工厂也可以使用converter来代替
二 工程落地
DDD分为战略设计和战术设计。从上面的第一章节的目录就可以看出,战略设计和战术设计又分别有哪些名词和步骤。
2.1 战略设计
参与人员:业务专家(领域专家),产品经理,技术专家(研发人员)。
战略设计更偏向于软件架构的层面,官方解释,在某个领域,核心围绕上下文的设计。讲求的是领域和限界上下文(Bounded Context,BC)的划分,以及各个限界上下文之间的上下游关系,还有通用语言的设计。也就是从业务视角出发,归好类,把边界划分好,明确界限上下文,可以用事件风暴来做。会得到通用语言,上下文,上下文之间的交互关系,边界,不同的域。
说白了就是:在某个系统,核心围绕子系统的设计;主要关注,这些子系统的划分,子系统的交互方式,还有子系统的核心术语的定义。当前如此火热的“在微服务中使用DDD”这个命题,究其最初的逻辑无外乎是“DDD中的限界上下文可以用于指导微服务中的服务划分”,
事实上,限界上下文依然是软件模块化的一种体现。
三步走:
- 需求分析:根据需求划分出初步的领域和限界上下文,以及上下文之间的关系;
- 领域分析:进一步分析每个上下文内部,抽取每个子域的领域概念,识别出哪些是实体,哪些是值对象;
- 领域建模:对实体、值对象进行关联和聚合,划分出聚合的范畴和聚合根;
2.2 战术设计
参与人员:技术专家(研发人员)
战术设计便更偏向于编码实现,官方解释,核心关注上下文中的实体建模,定义值对象,实体等,更偏向开发细节。用领域模型指导设计及编码的实现,以技术为主导。
说白了就是: 上下文对应的就是某一个子系统,子系统里代码实现怎么设计,就是战术设计要解决的问题。核心关注某个子系统的代码实现,以面向对象的思维设计类的属性和方法,和设计类图没有什么区别,只是有一些规则而已,就是指导我们划分类。DDD战术设计的目的是使得业务能够从技术中分离并突显出来,让代码直接表达业务的本身。
其中包含了实体,聚合,聚合根,值对象,聚合之间的关系,仓库,工厂,防腐层,充血模型,领域服务,领域事件等概念。
战术层面可以说DDD是一种放大的设计模式。
三步走:
- 编写核心业务逻辑,由领域模型驱动软件设计,通过代码来表现该领域模型,在实体和领域服务中实现核心业务逻辑;
- 为聚合根设计仓储,并思考实体或值对象的创建方式;
- 在工程中实践领域模型,并在实践中检验模型的合理性,倒推模型中不足的地方并重构。
三 DDD架构
分层架构的一个重要原则是每层只能与位于其下方的层发生耦合,较低层绝不能直接访问较高层。分层架构可以简单分为两种:
- 严格分层架构:某层只能与位于其直接下方的层发生耦合
- 松散分层架构:则允许某层与它的任意下方层发生耦合
我们在实际运用过程中多使用的是松散分层架构。
3.1 传统的四层架构
将领域模型和业务逻辑分离出来,并减少对基础设施、用户界面甚至应用层逻辑的依赖,因为它们不属业务逻辑。将一个夏杂的系统分为不同的层,每层都应该具有良好的内聚性,并且只依赖于比其自身更低的层。
传统分层架构的 基础设施层 位于底层,持久化和消息机制便位于该层。可将基础设施层中所有组件看作应用程序的低层服务,较高层与该层发生耦合以复用技术基础设施。即便如此,依然应避免核心的领域模型对象与基础设施层直接耦合。
3.2 改良版四层架构
传统架构的缺陷:就是将基础设施层放在最底层存在缺点,比如此时领域层中的一些技术实现令人头疼:违背分层架构的基本原则,难以编写测试用例等。
因此通过Java设计六大原则中的依赖倒置原则实现各层对基础资源的解耦:也就是低层服务(如基础设施层)应依赖高层组件(比如用户界面层、应用层和领域层)所提供接口。高层定义好仓库的接口,基础设施层实现各层定义好的仓库接口。
依赖倒置原则:具体依赖于抽象,而不是抽象依赖于具体。
3.2.1 用户接口层
- 一般包括用户接口、Web 服务、rpc请求,mq消息等外部输入均被视为外部输入的请求。对外暴露API,具体形式不限于RPC、Rest API、消息等。
- 一般都很薄,提供必要的参数校验和异常捕获流程。
- 一般会提供VO或者DTO到Entity或者ValueObject的转换,用于前后端调用的适配,当然dto可以直接使用command和query,视情况而定。
- 用户接口层很重要,在于前后端调用的适配。若你的微服务要面向很多应用或渠道提供服务,而每个渠道的入参出参都不一样,你不太可能开发出太多应用服务,这样Facade接口就起很好的作用了,包括DO和DTO对象的组装和转换等。
3.2.2 应用层
- 应用层方法提供用例级别的能力透出,不处理业务逻辑,而只是调用领域层,对领域服务/聚合根方法调用的封装,负责领域的组合、编排、转发、转换和传递。
- 应用服务作为总体协调者,先通过资源库获取到聚合根,然后调用聚合根中的业务方法,最后再次调用资源库保存聚合根
- 除了同步方法调用外,还可以发布或者订阅领域事件,权限校验、事务控制,一个事务对应一个聚合根。
- 应用层方法主要执行服务编排等轻量级逻辑,尤其针对跨多个领域的业务场景,效果明显。
- 参数校验,简单的crud,可直接调用仓库接口
- 跨上下文(kafka,RocketMq)和上下文内(spring事件,Guava Event Bus)的领域事件
- 仓储层接口
3.2.3 领域层
- 包含了业务核心的领域模型:实体(聚合根+值对象),使用充血模型实现所有与之相关的业务功能,主要表达业务概念,业务状态信息以及业务规则。
- 真正的业务逻辑都在领域层编写,聚合根负责封装实现业务逻辑,对应用层暴露领域级别的服务接口。
- 聚合根不能直接操作其它聚合根,聚合根与聚合根之间只能通过聚合根ID引用;同限界上下文内的聚合之间的领域服务可直接调用;两个限界上下文的交互必须通过应用服务层抽离接口->适配层适配。
- 跨实体的状态变化,使用领域服务,领域服务不能直接修改实体的状态,只能调用实体的业务方法
- 在所有的领域对象中,只有聚合根才拥有Repository,因为Repository不同于DAO,它所扮演的角色只是向领域模型提供聚合根。
- 防腐层接口
- 仓储层接口
3.2.4 基础设施层
- 为业务逻辑提供支撑能力,提供通用的技术能力,仓库写增删改查类似DAO。
- 防腐层实现(封装变化)用于业务检查和隔离第三方服务,内部try catch
- 聚合根工厂负责创建聚合根,但并非必须的,可以将聚合根的创建写到聚合根下并改为静态方法。工厂组组装复杂对象,可能会调用第三方服务,仓库集成工厂Facotry/build应对复杂对象的封装,也可以使用converter。
- 多于技术有关,如:DB交互的接口、Cache相关、MQ、工具类等
- 抽象系统内第三方组件的交互,为上层提供技术层面的支持,与业务细节无关。
3.3 洋葱架构
在整洁架构里,同心圆代表应用软件的不同部分,从里到外依次是领域模型、领域服务、应用服务和最外围的容易变化的内容,比如用户界面和基础设施。
整洁架构最主要的原则是依赖原则,它定义了各层的依赖关系,越往里依赖越低,代码级别越高,越是核心能力。外圆代码依赖只能指向内圆,内圆不需要知道外圆的任何情况。
在洋葱架构中,各层的职能划分:
领域模型:实现领域内核心业务逻辑,它封装了企业级的业务规则。领域模型的主体是实体,一个实体可以是一个带方法的对象,也可以是一个数据结构和方法集合。
领域服务:实现涉及多个实体的复杂业务逻辑。应用服务实现与用户操作相关的服务组合与编排,它包含了应用特有的业务流程规则,封装和实现了系统所有用例。
最外层主要提供适配的能力,适配能力分为主动适配和被动适配。主动适配主要实现外部用户、网页、批处理和自动化测试等对内层业务逻辑访问适配。被动适配主要是实现核心业务逻辑对基础资源访问的适配,比如数据库、缓存、文件系统和消息中间件等。
红圈内的领域模型、领域服务和应用服务一起组成软件核心业务能力。
3.4 CQRS架构(命令查询隔离架构)
CQRS — Command Query Responsibility Segregation,故名思义是读写分离,就是将 command 与 query 分离的一种模式。
- Command :命令则是对会引起数据发生变化操作的总称,即我们常说的新增,更新,删除这些操作,都是命令。
- Query:查询则和字面意思一样,即不会对数据产生变化的操作,只是按照某些条件查找数据。
Command 与 Query 对应的数据源可以公用一种数据源,也可以是互相独立的,即更新操作在一个数据源,而查询操作在另一个数据源上。
CQRS三种模式
- 共享模型/共享存储:读写公用一种领域模型,读写模型公用一种。
- 分离模型/共享存储:读写分别用不同的领域模型,读操作使用读领域模型,写操作使用写领域模型。
- 分离模式/分离存储:也叫做事件源 (Event source) CQRS,使用领域事件保证读写数据的一致性。也就是当 command 系统完成数据更新的操作后,会通过领域事件的方式通知 query 系统。query 系统在接受到事件之后更新自己的数据源。
CQRS(读写操作分别使用不同的数据库)
软件中的读模型和写模型是很不一样的,我们通常所讲的业务逻辑更多的时候是在写操作过程中需要关注的东西,而读操作更多关注的是如何向客户方返回恰当的数据展现。
因此在DDD的写操作中,我们需要严格地按照“应用服务 -> 聚合根 -> 资源库”的结构进行编码,而在读操作中,采用与写操作相同的结构有时不但得不到好处,反而使整个过程变得冗繁,还多了模型转换,影响效率。本来读操作就需要速度快,性能高。
因此本文CQRS实战中的读操作是基于数据模型,应用层提供一个单独的用于读的仓库,然后绕过聚合根和资源库,也就是绕过领域层,在应用层直接返回数据。而写操作是基于领域模型,通过应用服务->聚合根/领域服务->资源库的代码结构进行编码
3.5 六边形架构(端口适配架构)
六边形架构的核心理念是:应用是通过端口与外部进行交互的
下图的红圈内的核心业务逻辑(应用程序和领域模型)与外部资源(包括 APP、Web 应用以及数据库资源等)完全隔离,仅通过适配器进行交互。它解决了业务逻辑与用户界面的代码交错问题,很好地实现了前后端分离。六边形架构各层的依赖关系与整洁架构一样,都是由外向内依赖。
六边形架构将系统分为内六边形和外六边形两层,这两层的职能划分如下:红圈内的六边形实现应用的核心业务逻辑;外六边形完成外部应用、驱动和基础资源等的交互和访问,对前端应用以 API 主动适配的方式提供服务,对基础资源以依赖倒置被动适配的方式实现资源访问。六边形架构的一个端口可能对应多个外部系统,不同的外部系统也可能会使用不同的适配器,由适配器负责协议转换。这就使得应用程序能够以一致的方式被用户、程序、自动化测试和批处理脚本使用。
3.6 总结
这三种架构模型的设计思想微服务架构高内聚低耦合原则的完美体现,而它们身上闪耀的正是以领域模型为中心的设计思想,将核心业务逻辑与外部应用、基础资源进行隔离。
红色框内部主要实现核心业务逻辑,但核心业务逻辑也是有差异的,有的业务逻辑属于领域模型的能力,有的则属于面向用户的用例和流程编排能力。按照这种功能的差异,我们在这三种架构中划分了应用层和领域层,来承担不同的业务逻辑。
领域层实现面向领域模型,实现领域模型的核心业务逻辑,属于原子模型,它需要保持领域模型和业务逻辑的稳定,对外提供稳定的细粒度的领域服务,所以它处于架构的核心位置。
应用层实现面向用户操作相关的用例和流程,对外提供粗粒度的 API 服务。它就像一个齿轮一样进行前台应用和领域层的适配,接收前台需求,随时做出响应和调整,尽量避免将前台需求传导到领域层。应用层作为配速齿轮则位于前台应用和领域层之间。
参考文档:
https://blog.csdn.net/significantfrank/article/details/123267395
https://blog.csdn.net/qq_41889508/article/details/124907312