您当前的位置:首页 > 电脑百科 > 程序开发 > 架构

DDD 中关于应用架构的那些事

时间:2023-06-02 14:39:57  来源:  作者:IT168企业级

对领域驱动设计中关键的一些概念,大家有了更为深入的认识是不够的,在具体实践中我们还会面临诸如代码如何分层、不同上下文之间如何集成,以及某些时候还会用到CQRS。本文就来补齐领域驱动设计中剩余的一些内容,希望能够助你更游刃有余地应对开发中遇到的各种问题。

作者 | 于振

责编 | 韩楠

你好,今天我想与你聊聊DDD中的应用架构。在过往我分享的几篇文章中,我们介绍了领域驱动设计中的一些基本概念,这里,再做一个简单的回顾。

·《基础问题不简单|怎么合理使用值对象,让你的代码更清晰、更安全?》

·《不想只做Cruder?实体、聚合根,还不快去了解下》

·《如何通过仓储,对实体进行持久化处理?》

·《实体表达力不够?那你应该试试领域服务》

·《如何使用工厂,进一步解耦领域对象的职责》

·《领域模型细节太多不便使用?那就加个应用服务吧》

·《DDD在Go中如何落地|如何在业务中使用领域事件?》

使用值对象和实体帮助我们构建了具有丰富行为的领域模型,实体创建出来后需要通过仓储进行持久化,如果领域模型跟数据模型存在差异,就还需要通过 Converter 进行转换,以及通过 Snapshot 对实体进行追踪。

如果某些行为不适合放到某个实体上,就需要使用领域服务,同时,为了一定程度地防止领域服务的滥用,我们规定领域服务在命名上必须有一个动词。

为了解耦领域对象的创建过程和其自身行为,我们又介绍了工厂方法。

对于外部用户来说,领域之内的各个对象描述的,都是细粒度的领域概念,为了方便外部调用,同时屏蔽领域对象的具体细节,就又有了应用服务。

最后,通过领域事件,进一步解耦了不同上下文之间的依赖,即使在同一边界之内的不同的聚合根,也可以实现数据的最终一致性。

至此,大家应该对领域驱动设计中关键的一些概念,有了更为深入的认识。但仅仅是这些应该是还不够的,在具体实践中,我们还面临着诸如代码如何分层、不同上下文之间如何集成,以及某些时候还会用到CQRS。

在这篇文章中,我们就来补齐领域驱动设计中剩余的一些内容。

首先,我们从代码的分层开始说起。

01 DDD的分层架构

分层架构作为一种历史悠久的架构模式,在很多的场景中都得到了应用。

大家比较熟悉的应该就是 MVC 对应用三层架构的拆分。MVC 这种分层是自上而下的。

随着业务越来越复杂,人们逐渐发现, MVC 架构在应对复杂的业务问题时会显得力不从心。

于是,后面逐渐演化出了六边形架构、洋葱架构、整洁架构等架构模式。这几种架构也是一种分层架构,但这种分层不是由上而下的,而是由内而外的。

我们以洋葱架构为例:

可以看到,最关键的是中心的领域模型,它包括了所有的应用逻辑与规则。在这一层中不会直接引用技术实现,这样就能够确保在技术层面的改动不会影响到领域核心。

在领域层之外又包裹了领域服务层、应用服务层,而具体的技术实现则是被置于最外层的。

这种架构的好处就在于,它屏蔽掉了应用程序在UI层、DB层,以及各种中间件层的本质区别,所有的这些外部资源都被抽象成了对系统的输入输出,然后我们就能够以一致的方式来处理不同的请求类型,并且,在与实际运行的设备和数据库相隔离的情况下,也可以先行开发和测试。

在 DDD 的技术实现中,就用到了这种分层方式。

下图是 Eric Evans 在其经典著作《领域驱动设计》中给出的一个典型的 DDD 系统所采用的分层架构:

在上图中可以看到,整个架构划分成了四个层,各层所表示的含义及其职责描述如下:

1、用户接口层

这一层主要负责直接面向外部用户或者系统,接收外部输入,并返回结果。

用户接口层是比较轻的一层,不含业务逻辑。可以做一些简单的入参校验,也可以记录一下访问日志,对异常进行统一的处理。同时,对返回值的封装也应当在这层完成。

2、应用层

应用层,通常是用户接口层的直接使用者。

但是在应用层中并不实现真正的业务规则,而是根据实际的 use case 来协调领域层提供的能力,也可以说,应用层主要做的是编排工作。

另外,应用层还负责了事务这个比较重要的功能。

3、领域层

领域层是整个业务的核心层。我们一般会使用充血模型来建模实际的领域对象。

同时,由于业务的核心价值在于其运作模式,而不是具体的技术手段或实现方式。因此,领域层的编码原则上不允许依赖其他外部对象。

4、基础设施层

基础设施层,是在技术上具体的实现细节,它为上面各层提供通用的技术能力。

比如我们使用了哪种数据库,数据是怎么存储的,有没有用到缓存、消息队列等,都是在这一层要实现的。

对于这四个层次的划分,大家通常都没有太多的异议。但是在层与层之间的依赖关系上,后续又衍生出了很多的改良版本。比如在 IDDD 一书中,就给出了下图所示的分层架构:

这里最大的不同,就是将领域层放到了整个架构的最下面,也即领域层之下就不再有任何的其他依赖。这么做是没有问题的,但是最上面的基础设施层看起来却怪怪的。

在实际开发中,领域层的领域服务往往需要访问持久化组件,以及基础设施层中的其他组件,而对于持久化组件来说,不可避免地需要依赖领域层的实体对象。如此一来,领域层和基础设施层,就产生了双向依赖关系。

实际的解决方式,就是让领域层和基础设施层 都依赖一个统一的抽象,比如对于模型的持久化有 Repository 接口,对其他外部资源的访问也可以通过接口的形式来解耦合。但是 Repository 接口跟其他接口 又有些不太一样,Repository 因为需要参与到实体的整个生命周期中,所以在很多时候 Repository 都被看作是领域层中的一员。而对基础设施层中其他组件的抽象,是不适合定义到领域层的。

▶︎ DDD代码模型

结合上面的描述,这个时候再来看代码的组织形式,就比较清晰了。默认情况下,一个上下文对应了一个服务,我们这里以包含单个上下文的情况为例,给出如下的代码目录结构:

对上面的代码结构做一个简短的说明:

Application,对应到架构里的应用层,其内可能包含一些 assembler 和 DTO,assembler 主要用于将领域对象转换成返回需要的数据格式,这些数据格式以DTO的形式进行定义,这些DTO没有任何的业务逻辑,就是单纯的数据对象。

• domAIn,对应的是领域层,仓储的接口也是放在这一层的。

• handler,对应的是架构里的用户接口层,但其本质上还是属于基础设施层的一部分,这里单独提出来也仅仅是为了凸显它的重要性。在这一层,只可以直接访问应用层。

• infra,对应的是基础设施层,根据对不同资源的继承需求,可以在 infra 下继续分包。

• interfaces,是对基础设施层中除持久化以外的中间件的抽象,也即我们在这里定义访问中间件的接口,具体的实现还是放在基础设施层。这里将接口单独放到一个包中,为的是避免在领域层与应用层对基础设施层的直接依赖,如此就通过依赖反转解耦了具体的技术细节。

至此,我们就明确了代码的分层组织结构,以及彼此之间的依赖关系。

我们在文章开头提到的第二个问题是上下文的集成,在实际工作中,相信大家都会使用到微服务,这样一来,如何集成就成为我们必须要考虑的问题。

02 与其他上下文集成

上下文的集成无外乎两种方式, 一种是通过RPC进行集成,另一种是通过领域事件进行集成。

通过领域事件集成,也就是领域事件的发送和消费,这个我们在前面的文章中已经做了比较详细的介绍,这里不再赘述。

接下来主要说说通过 RPC 进行集成。

▶︎ 开放主机与发布语言

我们先来看一个在 DDD 中,经常用来表示集成方式的示例图:

其中,被集成方(A上下文,U 是 Upstream 的缩写)采用了开放主机和发布语言的方式,而集成方(B上下文,D 是 Downstream 的缩写)则使用了防腐层。几个缩写的含义如下:

• OHS(Open Host Service):开放主机服务,即定义一种协议,子系统可以通过该协议来访问你的服务。

• PL(Published Language):发布语言,通常跟 OHS 一起使用,用于定义开放主机的协议。

• ACL(Anticorruption Layer):防腐层,一个上下文通过一些适配和转换,来跟另一上下文交互。

我们平时大多数时候的开发工作,都是跟 Grpc/Kitex 等 RPC 框架打交道的,不同的框架在设计之初都会定义一份协议,只有符合协议要求的请求 才能被正确地识别和处理。比如 Grpc 使用 HTTP2 作为传输协议,而 Kitex 则主要使用自定义的 TTHeader 协议。

这些框架在使用上,一个共同特点就是需要通过 IDL(Interface description language) 来定义服务可以提供的能力。IDL 中可以定义多个接口,每个接口都有一个方法名,同时需要指定传递什么参数,返回什么数据。这样的一份 IDL 就可以认为是我们为系统定义的发布语言。

还是以前面多次提到的商品服务为例,商品服务作为上下文集成中的被集成方,通过 thrift 定义了其可以提供的服务,比如下面是对 GetProductDetail 接口的定义:

所以,如果我们是一个服务的提供方,只要我们使用 Grpc/Kitex,那么就可以认为我们是使用 OSH 和 PL 方式来进行集成的。

▶︎ 防腐层

防腐层一般用在下游上下文中,可以用来隔绝上游上下文中可能发生的变化。

在上面的例子中,商品服务提供了一个 GetProductDetail 接口,用以返回关于 Product 的全量信息。但是对于其他集成方来说,可能只是想拿到产品的很少一部分信息,比如在订单服务中要展示订单的详情,而详情只需要产品的图片和名称即可。

可以看到,作为服务的提供方,其具有追求普适性和灵活性的特点,而服务的调用方,在使用时却想要能够集中满足特定需求的接口。

这种张力是导致在边界上出现问题的主要原因,是无法避免的,但是却是可以解决的,应对的方法就是使用防腐层。

从图中可以看出,Subsystem A 和 Subsystem B 的调用关系并不是直接产生的,都要通过中间的一个ACL,ACL 除了负责执行具体的技术性调用,还将 A 和 B 的领域模型隔离开来,并承担了彼此模型之间的翻译转换功能。

除此以外,还可以在 ACL 做缓存、兜底、开关等功能。

对于集成方来说,一般采用独立接口的形式,接口的定义放在 interfaces 中,上面这个例子就可以这样定义:

因为实现是跟具体的技术相关的,所以实现需要放到基础设施层。整体的目录层级如下:

具体的实现可以参考下面的代码,简单来说就是将通过 RPC 获取到的上游模型,转换为自己领域内的模型:

在传统意义的防腐层实现中,会有一个适配器和一个对应的翻译器,其中适配器的作用是适配对其他上下文的调用,而翻译器就是将调用的结果转换成本地上下文中的元素。

在这里,我们为了保持代码的简单,没有特意声明这样两个对象,rpc的方法在这里起到了适配器的作用,至于翻译器,我们只是简单的提出了一个方法,在方法名上做了特殊的前缀修饰。

最后,ProductRpcClient 会作为 ProductClient 的实现类,最终被注入到服务中。

03 CQRS 简单实现

我们在看一些资料时,可能会看到有的地方叫CQS有的又叫CQRS。CQS 和 CQRS 都表示命令与查询的分离,本质上没有太大的区别。

CQS 是在《面向对象软件架构》一书中提出来的概念,作者 Bertrand Meyer 认为,一个方法原则上不应该既修改数据又返回数据,所以就有了两类方法:

1、查询:返回数据,但不修改数据,不会产生副作用;

2、命令:修改数据,但不返回数据,存在副作用。

CQRS 是对 CQS 概念的升华,因为查询端只返回数据,完全不修改数据,所以我们所有的查询不需要走领域实体,甚至没必要使用 ORM 框架,总之,我们可以通过各种手段来提升查询的效率。

关于CQS与CQRS的更多信息,可以参考这篇文章,和这篇。

下图是在各种技术文章中你会经常看到的一个非常典型的 CQRS 架构示例:

图中左侧部分代表的是对 Command 的处理,右侧是对 Query 的执行。很明显的一个区别是,在 Query 中不再强制必须走领域模型,而是在应用层可以直接访问基础设施层。

在实际开发中,对 Query 的处理其实是比较灵活的,其目的无外乎是提高查询的效率,另一方面也可以保证领域模型职责的单一。通常在查询相对简单的时候会复用领域模型,在稍微复杂时,会直接访问底层的数据模型,如果查询变得更加复杂,会将数据的存储也独立出来。

下面我们就依次说说这几种情况要如何处理。

▶︎ 复用领域模型

这种是最简单的情况,对应的读模型就是领域模型,要查询的数据基本上都是模型里的属性。

比如,我们有一个库存的聚合根:

展示的数据如下:

这个时候,就可以通过 assembler 直接转成对应的 view:

因为聚合根和仓储是一一对应的,所以,在应用服务中直接通过 Repository 获取领域模型即可:

▶︎ 使用数据模型

在分页查询,或者是需要多个实体聚合查询的场景,如果直接通过 Repository 获取领域模型再组装,可能会产生很多无关查询,影响效率。

这个时候,可以根据要展示的数据直接使用数据模型,或者通过 sql 只获取指定的某几个字段。

比如,我们有 Product 和 Category 两个聚合根,它们都包含了大量的属性和业务逻辑,但是我们要展示的数据比较简单:

这个时候就可以通过直接 sql 的形式来绕过领域模型:

▶︎ 使用独立的读模型

这种情况下,一般对应的查询场景都比较丰富,通常都会有一个独立的查询服务,各种数据在聚合处理之后统一放到查询服务中。

如下所示,订单在创建后,会使用 EventPublisher 来发布相应的事件:

在订单查询服务中,会对订单创建这个事件进行监听,当收到对应的消息时,会将订单信息存储到ES里。

如此一来,订单数据就同时存在于 MySQL 以及 ES 中。而在查询的时候会只通过 ES。

04 结语

在这篇文章中,我们介绍了实践领域驱动设计的时候应该如何组织代码结构、如何进行上下文的集成,以及在复杂查询场景中使用CQRS。这些内容我同样是用脑图的形式为你总结:

希望通过今天的讲解,你能够更游刃有余地应对开发中遇到的各种问题。但总地来说,DDD只是一种思想,所谓的分层架构也并不是事实上的标准,在实际应用时,还要结合自身的理解,可以适当地去创新或进行改进。

到目前为止,关于领域驱动设计的所有内容就都已经介绍完了。在下一篇文章中,我们会结合一个虚构的商城系统,带你实战领域驱动设计。

【技术专家】

于振

现于某大型互联网公司,负责架构工作

曾就职于美团、快手等一线互联网公司



Tags:架构   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
美团外卖宣布新一轮组织架构调整:提拔多位年轻管理者,年轻化、扁平化成主基调
新浪科技讯 4月11日上午消息,继2月下旬、3月下旬两轮人员调整后,美团到店到家的组织架构调整仍在继续。近日,美团外卖以内部邮件的方式宣布了新一轮的组织调整:外卖事业部下成立...【详细内容】
2024-04-11  Search: 架构  点击:(2)  评论:(0)  加入收藏
对于微服务架构监控应该遵守的原则
随着软件交付方式的变革,微服务架构的兴起使得软件开发变得更加快速和灵活。在这种情况下,监控系统成为了微服务控制系统的核心组成部分。随着软件的复杂性不断增加,了解系统的...【详细内容】
2024-04-03  Search: 架构  点击:(5)  评论:(0)  加入收藏
大模型应用的 10 种架构模式
作者 | 曹洪伟在塑造新领域的过程中,我们往往依赖于一些经过实践验证的策略、方法和模式。这种观念对于软件工程领域的专业人士来说,已经司空见惯,设计模式已成为程序员们的重...【详细内容】
2024-03-27  Search: 架构  点击:(13)  评论:(0)  加入收藏
哈啰云原生架构落地实践
一、弹性伸缩技术实践1.全网容器化后一线研发的使用问题全网容器化后一线研发会面临一系列使用问题,包括时机、容量、效率和成本问题,弹性伸缩是云原生容器化后的必然技术选择...【详细内容】
2024-03-27  Search: 架构  点击:(10)  评论:(0)  加入收藏
京东小程序数据中心架构设计与最佳实践
一、京东小程序是什么京东小程序平台能够提供开放、安全的产品,成为品牌开发者链接京东内部核心产品的桥梁,致力于服务每一个信任我们的外部开发者,为不同开发能力的品牌商家提...【详细内容】
2024-03-27  Search: 架构  点击:(10)  评论:(0)  加入收藏
从 MySQL 到 ByteHouse,抖音精准推荐存储架构重构解读
ByteHouse是一款OLAP引擎,具备查询效率高的特点,在硬件需求上相对较低,且具有良好的水平扩展性,如果数据量进一步增长,可以通过增加服务器数量来提升处理能力。本文将从兴趣圈层...【详细内容】
2024-03-22  Search: 架构  点击:(24)  评论:(0)  加入收藏
全程回顾黄仁勋GTC演讲:Blackwell架构B200芯片登场
北京时间3月19日4时-6时,英伟达创始人黄仁勋在美国加州圣何塞SAP中心登台,发表GTC 2024的主题演讲《见证AI的变革时刻》。鉴于过去一年多时间里AI带来的生产力变革,以及英伟达...【详细内容】
2024-03-19  Search: 架构  点击:(17)  评论:(0)  加入收藏
高并发架构设计(三大利器:缓存、限流和降级)
软件系统有三个追求:高性能、高并发、高可用,俗称三高。本篇讨论高并发,从高并发是什么到高并发应对的策略、缓存、限流、降级等。引言1.高并发背景互联网行业迅速发展,用户量剧...【详细内容】
2024-03-13  Search: 架构  点击:(6)  评论:(0)  加入收藏
有了LLM,所有程序员都将转变为架构师?
编译 | 言征 出品 | 51CTO技术栈(微信号:blog51cto)生成式人工智能是否会取代人类程序员?可能不会。但使用生成式人工智能的人类可能会,可惜的是,现在还不是时候。目前,我们正在见...【详细内容】
2024-03-07  Search: 架构  点击:(19)  评论:(0)  加入收藏
如何判断架构设计的优劣?
架构设计的基本准则是非常重要的,它们指导着我们如何构建可靠、可维护、可测试的系统。下面是这些准则的转换表达方式:简单即美(KISS):KISS原则的核心思想是保持简单。在设计系统...【详细内容】
2024-02-20  Search: 架构  点击:(36)  评论:(0)  加入收藏
▌简易百科推荐
对于微服务架构监控应该遵守的原则
随着软件交付方式的变革,微服务架构的兴起使得软件开发变得更加快速和灵活。在这种情况下,监控系统成为了微服务控制系统的核心组成部分。随着软件的复杂性不断增加,了解系统的...【详细内容】
2024-04-03  步步运维步步坑    Tags:架构   点击:(5)  评论:(0)  加入收藏
大模型应用的 10 种架构模式
作者 | 曹洪伟在塑造新领域的过程中,我们往往依赖于一些经过实践验证的策略、方法和模式。这种观念对于软件工程领域的专业人士来说,已经司空见惯,设计模式已成为程序员们的重...【详细内容】
2024-03-27    InfoQ  Tags:架构模式   点击:(13)  评论:(0)  加入收藏
哈啰云原生架构落地实践
一、弹性伸缩技术实践1.全网容器化后一线研发的使用问题全网容器化后一线研发会面临一系列使用问题,包括时机、容量、效率和成本问题,弹性伸缩是云原生容器化后的必然技术选择...【详细内容】
2024-03-27  哈啰技术  微信公众号  Tags:架构   点击:(10)  评论:(0)  加入收藏
DDD 与 CQRS 才是黄金组合
在日常工作中,你是否也遇到过下面几种情况: 使用一个已有接口进行业务开发,上线后出现严重的性能问题,被老板当众质疑:“你为什么不使用缓存接口,这个接口全部走数据库,这怎么能扛...【详细内容】
2024-03-27  dbaplus社群    Tags:DDD   点击:(12)  评论:(0)  加入收藏
高并发架构设计(三大利器:缓存、限流和降级)
软件系统有三个追求:高性能、高并发、高可用,俗称三高。本篇讨论高并发,从高并发是什么到高并发应对的策略、缓存、限流、降级等。引言1.高并发背景互联网行业迅速发展,用户量剧...【详细内容】
2024-03-13    阿里云开发者  Tags:高并发   点击:(6)  评论:(0)  加入收藏
如何判断架构设计的优劣?
架构设计的基本准则是非常重要的,它们指导着我们如何构建可靠、可维护、可测试的系统。下面是这些准则的转换表达方式:简单即美(KISS):KISS原则的核心思想是保持简单。在设计系统...【详细内容】
2024-02-20  二进制跳动  微信公众号  Tags:架构设计   点击:(36)  评论:(0)  加入收藏
详解基于SpringBoot的WebSocket应用开发
在现代Web应用中,实时交互和数据推送的需求日益增长。WebSocket协议作为一种全双工通信协议,允许服务端与客户端之间建立持久性的连接,实现实时、双向的数据传输,极大地提升了用...【详细内容】
2024-01-30  ijunfu  今日头条  Tags:SpringBoot   点击:(15)  评论:(0)  加入收藏
PHP+Go 开发仿简书,实战高并发高可用微服务架构
来百度APP畅享高清图片//下栽のke:chaoxingit.com/2105/PHP和Go语言结合,可以开发出高效且稳定的仿简书应用。在实现高并发和高可用微服务架构时,我们可以采用一些关键技术。首...【详细内容】
2024-01-14  547蓝色星球    Tags:架构   点击:(115)  评论:(0)  加入收藏
GraalVM与Spring Boot 3.0:加速应用性能的完美融合
在2023年,SpringBoot3.0的发布标志着Spring框架对GraalVM的全面支持,这一支持是对Spring技术栈的重要补充。GraalVM是一个高性能的多语言虚拟机,它提供了Ahead-of-Time(AOT)编...【详细内容】
2024-01-11    王建立  Tags:Spring Boot   点击:(124)  评论:(0)  加入收藏
Spring Boot虚拟线程的性能还不如Webflux?
早上看到一篇关于Spring Boot虚拟线程和Webflux性能对比的文章,觉得还不错。内容较长,抓重点给大家介绍一下这篇文章的核心内容,方便大家快速阅读。测试场景作者采用了一个尽可...【详细内容】
2024-01-10  互联网架构小马哥    Tags:Spring Boot   点击:(115)  评论:(0)  加入收藏
站内最新
站内热门
站内头条