前言

Github:https://github.com/HealerJean

博客:http://blog.healerjean.com

前言:

领域驱动架构是针对领域驱动设计建立的一种架构风格。它以领域为核心驱动力,以业务能力为核心关注点,建立目标系统的架构解决方案。其核心元模型为系统上下文与限界上下文,并以它们为边界,形成各自的架构模式:系统分层架构模式与菱形对称架构模

一、菱形对称架构

限界上下文是架构映射阶段的基本架构单元,每个限界上下文都是一个自治的独立王国。一个典型的限界上下文是以领域模型为核心关注点进行纵向切分的自治单元。

它在边界内维护着由自己控制的架构体系,使得内部所有的软件元素共同形成一个相对独立的主体,为系统贡献了内聚的业务能力。它在领域驱动设计中的重要性不言而喻。

然而,Eric Evans 埃文斯 在提出限界上下文的概念时,并没有提出与之匹配的架构模式。他提出的分层架构是对整个系统的层次划分,核心思想是将领域单独分离出来。这是从技术维度对整个系统的横向切分,与限界上下文领域维度的纵向切分形成了一种交错的架构体系。从系统层次观察这种交错的架构体系,可以映射出系统级的架构,而对于限界上下文内部,我们也亟需一种架构模式来表达它内部的视图,以满足它的自治特性。领域驱动设计社区做出的尝试是为限界上下文引入六边形架构。”

限界上下文的内部应遵循如下图所示的菱形对称架构:

菱形对称架构的核心思想:

  • 内外分离:内部的领域层与外部的网关层分离,保证业务和技术的正交性
  • 南北对称:南向网关采用抽象思想,隔离外部资源变化对内部领域层带来的影响;北向网关采用封装思想,通过定义远程服务和本地服务隔离内部领域逻辑对外部调用者的影响

image-20240822105708867

1、六边形架构

六边形架构又被称为端口适配器,由 Alistair Cockburn 卡博恩 提出。它强调将应用程序的核心逻辑(领域模型)与外部系统(如数据库、用户界面、外部服务等)隔离开来。这种架构通过定义明确的端口(接口)来实现内外分离,使得应用程序的核心逻辑可以独立于外部系统进行测试和演化

应用程序封装了领域逻辑,并将其放在六边形的边界内,使得它与外界的通信只能通过端口和适配器进行。

端口存在两个方向:入口和出口。与之相连的适配器自然也存在两种适配器:入口适配器和出口适配器。入口适配器负责处理系统外部发送的请求(即驱动应用程序运行的用户、程序、自动化测试或批处理脚本向入口适配器发起),将该请求适配为符合内部应用程序执行的输入格式,转交给端口,再由端口调用应用程序。出口适配器负责接收内部应用程序通过出口端口传递的请求,对其进行适配后,向位于外部的运行时设备和数据库发起请求。

例子:考虑一个电子商务应用,其核心领域模型包括商品、订单和用户等实体。在六边形架构中,这些实体和它们之间的业务逻辑构成了应用程序的内核。外部系统,如数据库、支付网关或第三方物流服务,都通过适配器与内核进行交互。这样,当需要替换数据库或支付服务提供商时,只需更改相应的适配器,而无需修改内核代码。

image-20240822110920073

1)内部六边形

Cockburn 对六边形架构的初始定义中,应用程序位于六边形边界内部,封装了支持业务功能的领域逻辑。入口端口与出口端口在六边形边界上,前者负责接收外部的入口适配器转换过来的请求,后者负责发送应用程序的请求给外部的出口适配器,由此可以勾勒出一个清晰的六边形,如图所示。

image-20240822110909432

2)外部六变形

限界上下文是在专有知识语境下业务能力的体现。这一业务能力固然以领域模型为核心,却必须通过与外部环境的协作方可支持其能力的实现。因此,限界上下文的边界实则包含了对驱动它运行的入口请求的适配与响应逻辑,也包含了对外部设备和数据库的访问逻辑。要将限界上下文与六边形架构结合起来,就需要将入口适配器和出口适配器放在限界上下文的边界内,构成一个外部的六边形。

六边形架构清晰地勾勒出限界上下文的两个边界

外部边界:通过外部六边形将单独的业务能力抽离出来,隔离了不同的业务关注点。我将此六边形称为“应用六边形”。

内部边界:通过内部六边形将领域单独抽离出来,隔离了业务复杂度与技术复杂度。我将此六边形称为“领域六边形”。”

image-20240822111053947

3)案例分析

以预订机票场景为例。用户通过浏览器访问订票网站,向订票系统发起订票请求。

1、根据六边形架构,浏览器访问的网站前端位于应用六边形之外,属于驱动应用程序运行的起因。订票请求通过浏览器发送给以 REST 风格服务契约定义的控制器服务 ReservationControllerReservationController 作为入口适配器,介于应用六边形与领域六边形之间

2、在接收到以 JSON 格式传递的前端请求后,将其转换(反序列化)为入口端口 ReservationAppService需要的请求对象。入口端口为应用服务,位于领域六边形的边界之上。

3、当它接收到入口适配器转换后的请求对象后, 调用位于领域六边形边界内的领域服务 TicketReservation,执行领域逻辑。

4、在执行订票的领域逻辑时,需要向数据库添加一条订票记录。这时,位于领域六边形边界内的领域模型对象会调用出口端口ReservationRepository。出口端口为资源库,位于领域六边形的边界之,定义为接口。

5、真正访问数据库的逻辑则由介于应用六边形与领域六边形间的出口适配器 ReservationRepositoryAdapter 实现。该实现访问了数据库,将端口发送过来的插入订票记录的请求转换为数据库能够处理的消息,执行插入操作。该业务场景在六边形架构中的体现如下图

image-20240822112326288

2)总结

六边形架构中的端口是解耦的关键

入口端口体现了“封装”的思想,既隔离了外部请求转换必需的技术实现,如 REST 风格服务的序列化机制与 HTTP 请求路由等基础设施功能,又防止了领域模型向外泄露,因为端口公开的服务接口方法已经抹掉了领域模型的信息。

出口端口体现了“抽象”的思想,它通常被定义为抽象接口,不包含任何具体访问外部设备和数据库的实现。

入口端口抵御了外部资源可能对当前限界上下文造成的侵蚀,因此,入口适配器与入口端口之间的关系是一个依赖调用关系;出口端口隔离了领域逻辑对技术实现以及外部框架或环境的依赖,因此,出口适配器与出口端口之间的关系是接口实现关系

2、整洁架构

整洁架构由Robert Martin 提出,它强调将业务逻辑与具体的实现细节(如数据库、UI 框架等)分离开来整洁架构将系统划分为四个层次:实体、用例、接口适配器和框架与驱动

核心点:分层 + 依赖规则

1)分层:分离关注点

分层:关注点分离思想

这些架构都会将软件切割成不同的层,至少有一个层是只包含软件的业务逻辑的,用户接口、系统接口属于其他层。

2)依赖原则

依赖规则:代码依赖只能使由外向内,内层结构的代码不能包含有任何外层结构的信息。整洁架构最主要的原则是依赖原则,它定义了各层的依赖关系,越往里依赖越低,代码级别越高,越是核心能力外圆代码依赖只能指向内圆,内圆不需要知道外圆的任何情况

1、越靠近圆心即越是稳定的,即代表高层策略,越外围表示是低层组件,

2、依赖方式:从图中也可以看出,低层组件依赖高层组件(策略)

3、业务实体层:包含业务实体,即应用的业务对象,封装最通用最高层的业务逻辑 (单独某个业务实体的逻辑),它不应该受外界影响

4、用户用例层:实现用户某个用例场景的业务逻辑封装,是对业务实体的组装、封装

5、接口适配层:目的是进行数据的交换,包含有网关、控制器、展示器, 如:实体层和用户实例层使用的数据转化成为持久层能使用的数据,比如数据库

6、框架与驱动层:包含数据库、用户界面、web 框架等

image-20240822130201780

3、分层架构

整洁架构与六边形架构一脉相承。但是,六边形架构仅仅区分了内外边界,提炼了端口与适配器角色,却没有规划限界上下文内部各个层次与各个对象之间的关系

整洁架构又是通用的架构思想,它强调将业务逻辑与具体的实现细节(如数据库、UI 框架等)分离开来。整洁架构将系统划分为四个层次:实体、用例、接口适配器和框架与驱动。

二者都无法完美地契合限界上下文的架构诉求。因此,当我们将六边形架构与整洁架构思想引入限界上下文时,还需要引入分层架构给出更为细致的设计指导,即确定层、模块和角色构造型之间的关系

1)什么是分层架构

分层架构是一种软件架构模式,它将软件系统分为多个相互独立的层,每一层都有其特定的职责。这些层通常按照功能进行组织,从用户界面到数据访问,每一层都只与它的上下层直接通信。常见的分层包括:表示层(或UI层)、业务逻辑层、数据访问层和数据库层

2)分层架构的优势

1、清晰的职责:每一层都负责处理特定的任务,例如,表示层负责用户界面,业务逻辑层处理业务规则,数据访问层负责数据的持久化等。这种职责的分离使得系统更加模块化,便于管理和开发。

2、易于测试:分层架构支持单元测试和集成测试。由于各层之间的低耦合性,可以独立地对某一层进行测试,提高测试效率。

3、安全性:可以在不同层之间设置安全检查,比如在数据访问层实现权限控制,防止未授权的数据访问。

4、可维护性和可扩展性:当系统需要改变或扩展时,通常只需要关注特定的层次。例如,更换数据库技术可能只涉及到数据访问层的修改。

5、促进重用:因为每一层都是独立的,它们可以在多个项目中复用,减少了开发的工作量。

3)实施分层架构的策略

1、明确层级职责:确保每一层的职责清晰定义,并且各层之间的交互简洁明了。

2、使用抽象接口:层与层之间通过抽象接口进行通信,这样可以在不影响到其他层的情况下,对某一层进行修改或替换。

3、依赖倒置原则:高层模块不应依赖于低层模块,两者都应该依赖于抽象。抽象不应依赖于细节,细节应依赖于抽象。这有助于降低层与层之间的耦合度

4、逐层开发和测试:采用逐层开发和测试的方法,先开发和测试最底层,然后逐步向上,直至整个系统完成。

5、管理好跨层交互:减少跨层直接调用,尽量通过抽象层进行交互,以保持系统的结构清晰。

6、持续优化性能:虽然分层架构强调的是职责分离,但也需要关注性能问题。合理的设计和优化可以减少层与层之间的通信开销,提升系统的整体性能。

4、常见的分层架构

1)分层架构模式-CS 架构

CS 架构( Client/Server Architecture )是基于客户端和服务器之间的通信,将应用程序的逻辑和数据存储在服务器上,而客户端(终端设备)会运行一部分程序代码来处理数据和交互操作。

image-20240822183039282

2)分层架构模式-BS 架构

BS 架构(Browser/Server Architecture)是基于浏览器和服务器之间的通信,将应用程序的逻辑和数据存储在服务器上,而客户端(浏览器)只是通过网络请求数据和交互操作

image-20240822183146460

3)MVC 架构(模型层-视图层-和控制器层)

MVCModel-View-Controller:这是 Web 开发中常用的一种分层架构模式,它将系统划分为模型层、视图层、和控制器层(业务逻辑)

模型(Model:负责处理业务逻辑和数据持久化,是应用程序的核心部分。它通常包含应用程序的数据结构和业务规则。

视图(View:负责显示用户界面,将模型中的数据以图形化方式呈现给用户。视图可以接收用户的输入,但并不直接处理这些输入,而是将其传递给控制器。

控制器(Controller:负责接收用户的输入,调用模型和视图去完成用户的需求。控制器是模型和视图之间的桥梁,它处理用户请求并决定调用哪个模型组件去处理请求,然后再确定用哪个视图来显示模型处理返回的数据。

示例:在线图书管理系统

  职责 示例 注意
模型 负责处理与图书管理相关的业务逻辑和数据。 Book 类:包含图书的属性(如ISBN、书名、作者、库存量等)和方法(如获取图书信息、更新库存量等)。
BookService 类:封装与图书相关的业务逻辑,如借阅图书时检查库存量是否足够、更新借阅记录等。
模型层不直接与数据库交互,而是通过数据访问对象(DAO)或仓库(Repository)来实现。
视图 负责展示用户界面,接收用户输入。 BookListView:显示图书列表的 JSP页面或HTML模板。
BorrowFormView:用户填写借阅信息的表单页面。
视图层通常使用 JSPThymeleafFreeMarker 等技术实现,它们从控制器接收数据并渲染成 HTML页面展示给用户。
控制器 接收用户的输入,调用模型和视图完成操作。 BookController 类:包含处理 HTTP 请求的方法。
@GetMapping("/books"):显示图书列表页面。
@PostMapping("/borrow"):处理借阅图书的请求,调用BookService的借阅方法,并返回借阅成功或失败的视图。
控制器层是模型和视图之间的桥梁,它处理用户的请求并调用相应的业务逻辑,然后将结果返回给视图层展示。

4)三层架构(表现层-业务逻辑层-数据访问层)

企业应用三层架构通常包括表示层、业务逻辑层和数据访问层。这种结构清晰分离了用户界面、业务处理和数据管理的责任,每一层都可以独立开发和维护,提高了系统的灵活性和可维护性。适用于大多数企业级应用开发。最广为人知的应该它

三层架构的优点是分离了应用程序的不同层次,使得每个层次都可以独立开发、测试和部署。此外,三层架构也提高了应用程序的可维护性和可扩展性。

表现层(Presentation Layer):负责与用户交互,接收用户的输入和向用户展示输出,将用户的请求发送到应用程序的下一层。它通常是前端界面,如网页、移动应用等。。

业务逻辑层(Business Logic Layer):处理系统的业务逻辑,包括数据处理、业务规则等。它是连接表示层和数据访问层的桥梁,负责将表示层的数据请求转发给数据访问层,并将数据访问层返回的数据处理后再返回给表示层。。

数据访问层(Data Access Layer):负责与数据库进行交互,进行数据的读取和存储。它封装了所有与数据库相关的操作,为业务逻辑层提供数据服务。

image-20240822184436742

示例:在线图书管理系统

  职责 示例 注意
表示层(或界面层) 与用户直接交互,接收用户请求并展示处理结果。 Web 页面( HTML / JSP / Thymeleaf 等):提供用户界面,如图书列表页面、借阅表单页面等。
⬤ 控制器(Controller):在 MVC 架构中已详细介绍,但在这里作为表示层的一部分,负责接收请求并调用业务逻辑层。
 
业务逻辑层 处理系统的核心业务逻辑。 BookService 类:与 MVC 架构中的相同,封装与图书相关的业务逻辑。
⬤ 其他服务类(如UserServiceLibraryService等):根据系统需求定义的其他业务逻辑服务。
 
数据访问层 负责与数据库交互,执行数据的增删改查操作。 BookDAOBookRepository 接口:定义与图书相关的数据操作方法,如查询图书列表、更新库存量等。
⬤ 实现类(如BookDAOImplBookRepositoryImpl):实现上述接口,具体实现与数据库的交互。
 

5)DDD四层架构(表现层-应用层-领域层-基础设施层层)

和三层架构主要区别为将三层架构的业务逻辑层拆解为应用层和领域层

表现层:用户界面层,或者表现层,责向用户显示信息和解释用户命令,完成前端界面逻辑

应用层:定义软件要完成的任务,即应用案例(Use Cases,并协调领域层和表现层进行工作。它不包含业务规则或知识,而是作为一个简单的、一致的机制来指定软件如何响应外部请求。

领域层:也可称为模型层,系统的核心,负包含系统的核心业务逻辑和领域模型,负责实现业务规则、状态变更等。。即包含了该领域(问题域)所有复杂的业务知识抽象和规则定义。该层主要精力要放在领域对象分析上,可以从实体,值对象,聚合(聚合根),领域服务,领域事件,仓储,工厂等方面入手

基础设施层:主要有2方面内容,一是为领域模型提供持久化机制,当软件需要持久化能力时候才需要进行规划二是通用技术支持,一些公共通用技术支持也放到基础设施层去实现。,如消息通信,通用工具,配置等的实现;

image-20240822194148871

image-20240822194737889

image-20240822210644298

示例:在线图书管理系统

  示例 注意
表示层 Web 页面( HTML / JSP / Thymeleaf 等):提供用户界面,如图书列表页面、借阅表单页面等。
⬤ 控制器(Controller):在 MVC 架构中已详细介绍,但在这里作为表示层的一部分,负责接收请求并调用业务逻辑层。
 
应用层 实现:服务类或接口,它们不直接访问数据库,而是调用领域层中的实体和值对象来处理业务逻辑。
示例BookApplicationService 类,它包含如 borrowBookreturnBook 等方法,这些方法调用领域层中的逻辑来执行实际的业务操作。
 
领域层 实体(Entities)、值对象(Value Objects)、领域服务(Domain Services)、聚合(Aggregates)和仓库接口(Repository Interfaces,注意这里的接口是定义在领域层,但实现通常在数据访问层)
Book 实体:表示图书,包含ISBN、标题、作者等属性。
Borrowing 聚合:包含借阅操作的业务逻辑,如检查库存、更新借阅记录等。
BookRepository 接口:定义与图书数据相关的操作,如按ISBN查找图书。
 
数据访问层 数据访问对象(DAO)、仓库(Repository)模式的实现类
BookRepositoryImpl 类:实现 BookRepository 接口,与数据库进行交互,执行具体的SQL查询或更新操作。
⬤ 数据库表:BooksBorrowings 等,存储图书和借阅记录的数据。
 

4)分层架构知识库

a、分层架构差异和场景

  应用场景 层级职责 关注点
MVC MVC 架构更适用于构建用户界面复杂、交互性强的Web应用程序 MVC 架构中的控制器层更接近于表示层,负责处理用户请求并调用模型和视图 MVC架构更侧重于表现层的分离和职责的明确划分
三层架构 三层架构则更适用于构建企业级应用程序和大型系统,以实现系统的模块化、可扩展性和可维护性。 而三层架构中的业务逻辑层则专注于处理核心业务逻辑,数据访问层专注于与数据库的交互 三层架构则更侧重于系统整体的层次结构和各层之间的依赖关系。`

b、分层的依据与原则是什么

认知规则:机器为本,用户至上。机器是运行系统的基础,而我们打造的系统却是为用户提供服务的。

分层架构中的层次越往上,其抽象层次就越面向业务、面向用户;分层架构中的层次越往下,其抽象层次就变得越通用,面向设备。

过多的层会引入太多的间接而增加不必要的开支,层太少又可能导致关注点不够分离,使得系统的结构不够合理。

问题1:为什么经典分层架构为三层架构?

答案:正是源于这样的认知规则:其上,面向用户的体验与交互;居中,面向应用与业务逻辑;其下,面对各种外部资源与设备。

问题2:分层的依据是什么呢?

答案:在为系统建立分层架构时,我们完全可以基于这个经典的三层架构,沿着水平方向进一步切分属于不同抽象层次的关注点。

分层的第一个依据是基于关注点为不同的调用目的划分层次。领域驱动设计的分层架构之所以要引入应用层,目的就是为了给调用者提供完整的业务用例,无需与细粒度的领域模型对象直接协作。

分层的第二个依据是面对变化分层时应针对不同的变化原因确定层次的边界,严禁层次之间互相干扰,或者至少将变化对各层带来的影响降到最低。例如,数据库结构的修改会影响到基础设施层的数据模型以及领域层的领域模型,但当我们仅需要修改基础设施层中数据库访问的实现逻辑时,就不应该影响到领域层了。层与层之间的关系应该是正交的。

c、层与层之间是怎样协作的

在我们固有的认识中,分层架构的依赖都是自顶向下传递的,这也符合大多数人对分层的认知模型。从抽象层次看,层次越处于下端,就会变得越通用,与具体的业务隔离得越远,从而形成基础设施层。为了避免重复制造轮子,它还会调用位于系统外部的平台或框架,如依赖注入框架、ORM框架、消息中间件等,以完成更加通用的功能。若依赖的传递方向仍然采用自顶向下,就会导致包含领域逻辑的领域层依赖于基础设施层,又因为基础设施层依赖于外部平台或框架,使得领域层也将受制于它们。

依赖倒置原则提出了对自顶向下依赖的挑战,它要求“高层模块不应该依赖于低层模块,二者都应该依赖于抽象。”这个原则正本清源,给了我们当头棒喝——谁规定在分层架构中,依赖就一定要沿着自顶向下的方向传递?我们常常理解依赖,是因为被依赖方需要为依赖方(调用方)提供功能支撑,这是从功能重用的角度来考虑的。但我们不能忽略变化对系统产生的影响!与建造房屋一样,我们自然希望分层的模块“构建”在稳定的模块之上。谁更稳定?抽象更稳定。

因此,依赖倒置原则隐含的本质是:我们要依赖不变或稳定的元素(类、模块或层)也就是该原则的第二句话:抽象不应该依赖于细节,细节应该依赖于抽象

层之间的协作并不一定是自顶向下的传递通信,也有可能是自底向上通信,例如在计算机集成制造系统(Computer Integrated Manufacture System, CIMS)中,往往会由低层的设备监测系统去监测(侦听)设备状态的变化。当状态发生变化时,需要将变化的状态通知到上层的业务系统。如果说自顶向下的消息传递被描述为“请求(或调用)”,则自底向上的消息传递则被形象地称之为“通知”。倘若我们颠倒一下方向,自然也可以视为这是上层对下层的观察,故而可以运用观察者模式(Observer Pattern),在上层定义Observer接口,并提供 update()方法供下层主题(Subject)在感知状态发生变更时调用。

现在,我们对分层架构有了更清醒的认识。它通过水平抽象体现了关注点分离,只要存在相同抽象层次的关注点,就可以单独为其建立一个逻辑层,抽象层数不是固定的,每一层的名称也不必一定遵循经典的分层架构要求。对系统逻辑层的划分需得结合该系统的具体业务场景而定。当然,我们也要认识到层次多少的利弊:过多的层会引入太多的间接而增加不必要的开支,层太少又可能导致关注点不够分离,使得系统的结构不够合理。

我们还需要正视分层架构中各层之间的协作关系,打破高层依赖低层的固有思维,*分层架构是否允许跨层调用‌取决于具体的架构设计和原则。在某些情况下,分层架构允许跨层调用,即每一层都可以使用比它低的所有层的服务,而不仅仅是相邻的低层。这种设计原则被称为松散分层系统

5、演进为菱形对称架构

回到限界上下文的内部视图。六边形架构通过外部的应用六边形与内部的领域六边形将整个限界上下文分隔为三个区域:

可惜,六边形架构并未对这三个区域命名,这就为团队的协作交流制造了障碍。例如,当团队成员正在讨论一个入口端口的设计时,需要确定入口端口在代码模型的位置,即确定入口端口所在的命名空间。我们既不可能说它放在“领域六边形的边线”上,也不可能为该命名空间定义一个冗长的包名,例如 currentbc.boundaryofdomainhexagon。命名的目的是为了交流,然后形成一种约定,就可以做到不言自明。因此,我们需要寻找一种架构的统一语言为这些区域命名,如此即可将六边形的设计元素映射到代码模型对应的命名空间。

1)划分层次

从关注点分离的角度看,六边形架构实则就是隔离内外的分层架构,因此我们完全可以将两个六边形隔离出来的三个区域映射到领域驱动设计的分层架构中。映射时,自然要依据设计元素承担的职责来划分层次:

入口适配器:响应边界外客户端的请求,需要实现进程间通信以及消息的序列化和反序列化,这些功能皆与具体的通信技术有关,故而映射到基础设施层

入口端口:负责协调外部客户端请求与内部应用程序之间的交互,恰好与应用层的协调能力相配,故而映射到应用层

应用程序:承担了整个限界上下文的领域逻辑,包含了当前限界上下文的领域模型,毫无疑问,应该映射到领域层

出口端口:作为一个抽象的接口,封装了对外部设备和数据库的访问,由于它会被应用程序调用,遵循整洁架构思想内部层次不能依赖外部层次的原则,只能将它映射到领域层

出口适配器:访问外部设备和数据库的真正实现,与具体的技术实现有关,映射到基础设施层

image-20240822214815533

通过这一映射,我们就为六边形架构的设计元素找到了统一语言。例如,入口端口属于应用层,它的命名空间自然应命名为currentbc.application。这一映射关系与命名规则实则就是指导团队开发的架构原则。当团队成员在讨论设计方案时,一旦确定该类属于入口端口,不言自明,团队的所有成员都知道它归属于应用层,应该定义在 application 命名空间下。

问题1:为什么将出口端口放在领域层?

**1、职责划分与依赖关系 **

  • 职责明确:领域层是应用程序的核心,负责实现业务逻辑和规则。出口端口作为与外部系统(如数据库、消息队列等)交互的接口,其职责更接近于对领域模型数据的封装和转换,以便与外部系统进行有效的通信。这种职责划分使得领域层能够更专注于业务逻辑的实现,而不必过多关注底层技术的细节。
  • 依赖倒置原则在六边形架构中,通常遵循依赖倒置原则,即高层模块(如领域层)不应该依赖于低层模块(如基础设施层),而应该依赖于抽象接口或类。将出口端口放在领域层,可以使领域层依赖于抽象的出口端口接口,而不是具体的基础设施实现,从而提高了系统的灵活性和可扩展性。

2、架构的稳定性和可扩展性

  • 稳定性:将出口端口放在领域层,有助于保持领域层的稳定性。因为领域层的变更通常较少,且主要集中在业务逻辑的调整上,而将与外部系统交互的细节封装在出口端口中,可以减少领域层对外部系统的直接依赖,降低变更风险。
  • 可扩展性:当需要添加新的外部系统或修改现有系统的交互方式时,只需在出口端口层进行相应的调整,而无需修改领域层的代码。这种架构方式使得系统更容易扩展和维护。

3、遵循领域驱动设计原则

  • 领域模型与资源库:在领域驱动设计中,领域模型包含了业务逻辑和规则,而资源库(Repository)是领域模型中用于管理聚合生命周期的组件。将资源库作为出口端口放在领域层,可以使得领域模型能够直接通过资源库与外部数据库进行交互,而无需经过基础设施层的转换。这种方式更符合领域驱动设计的原则,即领域模型应该直接管理其生命周期和数据存储。

4、隔离外部设备与领域模型

  • 抽象隔离:虽然基础设施层通常负责处理与外部设备的具体交互,但将出口端口放在领域层可以为领域模型与外部设备之间提供一层抽象隔离。这种隔离有助于减少领域模型对外部设备具体实现的依赖,使得领域模型更加专注于业务逻辑的实现。

2)北向网关&南向网关

根据入口与出口方向的不同,为了体现它所处的方位,我将这个由端口与适配器共同组成的网关分别命名为“北向网关”与“南向网关”。

a、北向网关分为:本地网关与远程网关

北向网关提供了由外向内的访问通道,这一访问方向符合整洁架构的依赖方向,因此不必对北向网关元素进行抽象,只需为外部的调用者提供服务契约即可。为了避免内部领域模型的泄露,北向网关的服务契约不能直接暴露领域模型对象,需要为组成契约的方法参数和返回值定义专门的模型,由于这个模型主要用于调用者的请求和响应,因而称之为“消息契约模型”。

北向网关的服务契约必须调用领域模型的业务方法才能满足调用者的请求,由于领域模型并不知道消息契约模型,需要北向网关负责完成这两个模型之间的互换。由于北向网关既要对外提供服务契约,又要对内完成模型的转换,相当于它同时承担了端口与适配器的作用,因而不用再区分入口端口和入口适配器

限界上下文的外部请求可能来自进程之外,也可能是进程之内,进程内外的差异,决定了通信协议的不同,有必要根据进程的边界将北向网关分为:本地网关与远程网关。前者支持进程内通信,后者用于进程间通信

b、南向网关:可直接命名为端口与适配器

南向网关负责封装领域层对外部环境的访问。所谓“外部环境”,包括如数据库、消息队列、文件系统之类的环境资源,也包括系统内的上游限界上下文与系统外的伴生系统,它们也就是组成整洁架构的最外层圆环,包含了技术细节。这些外部环境变化的方向与频率和领域模型完全不同,需要分离抽象接口与具体实现,也就是六边形架构的出口端口与出口适配器,现在,它们共同组成了南向网关。

南向网关的命名已经代表了出口方向,就不要区分入口还是出口,可直接命名为端口与适配器。端口未提供任何实现,即使它被领域层的领域模型调用,也不会将技术实现混入到领域逻辑中。运行时,系统通过依赖注入将适配器实现注入到领域层,满足领域逻辑对外部设备的访问需求。

3)菱形堆成架构

菱形对称架构从分层架构与六边形架构汲取了营养,形成了以领域为轴心的内外分层对称结构,以此作为推荐的限界上下文内部架构。内部的领域层定义了核心的领域模型,外部的网关层根据方向划分为北向网关与南向网关。考虑到目前的系统多采用前后端分离的架构,且前端 UI 的设计更多是从用户体验的角度对视图元素进行划分,与限界上下文的边界划分并不吻合,因此,限界上下文边界并未将前端 UI 包含在内。一个遵循了菱形对称架构的限界上下文包括的设计元素为:

  • 北向网关的远程网关
  • 北向网关的本地网关
  • 领域层的领域模型
  • 南向网关的端口抽象
  • 南向网关的适配器实现

image-20240823165040526

a、样例介绍

限界上下文以领域模型为核心向南北方向对称发散,从而在边界内形成清晰的逻辑层次,内部领域层与外部网关层恰好体现了业务复杂度与技术复杂度的分离。每个组成元素之间的协作关系表现了清晰直观的自北向南的调用关系。仍以预订机票场景为例,参与该场景的各个类在菱形对称架构下的位置与协作关系如下图所示:

1、本地网关 ReservationAppService 映射为领域驱动设计元模型中的应用服务,它对外提供了完整的预定机票用例,对内调用了领域层的领域模型对象。为了支持分布式调用

2、在本地网关之上还定义了远程网关 ReservationController ,它是一个面向前端视图遵循 MVC 模式设计的远程服务。本地网关和远程网关共同构成了北向网关。

3、端口 ReservationRepository 映射为领域驱动设计元模型中的资源库,是对数据库访问的抽象

4、适配器 ReservationRepositoryAdapter 提供了端口的实现。端口与适配器共同构成了南向网关。领域层的领域服务TicketReservation 会调用端口,并在运行时将适配器注入到领域服务,以支持机票预订记录的持久化。

image-20240823172413013

b、界限上下文&菱形对称架构

菱形对称架构灵活地运用了分层架构的依据与原则,对经典的分层架构进行了调整,引入统一的网关层简化了层次,形成了由内部领域层与外部网关层组成的内外分层架构。

最小完备:菱形的边界即为限界上下文的边界,以“最小完备”的方式实现了业务关注点的纵向切分;

自我履行:内部的领域模型自成一体,以“自我履行”的方式响应外部网关对它的调用;

稳定空间:端口对外部资源访问的抽象,防止了外部的变化对领域模型的影响,使得限界上下文的内部成为了“稳定空间”。显然,菱形对称架构是对自治单元设计的呼应,能够更好地保证限界上下文具有响应变化的演进能力。

独立进化:远程网关与本地网关对领域模型的封装,避免了内部的变化对外部的调用者产生影响,满足了限界上下文的“独立进化”能力;

二、引入上下文映射

菱形对称架构还能够有机地与上下文映射模式结合起来,充分展现了这一架构风格更加适用于领域驱动设计。二者的结合主要体现在北向网关与南向网关对上下文映射模式的借用。

1、北向网关的演变

1)开放的主机服务

对比上下文映射的通信集成模式,我们发现开放主机服务模式的设计目标与菱形对称架构的北向网关完全一致。开放主机服务为限界上下文提供对外公开的一组服务,以便于下游限界上下文方便地调用它。根据限界上下文通信边界的不同,进程内通信调用本地网关,进程间通信调用远程网关,二者都遵循开放主机服务模式,

a、远程服务

远程服务是为跨进程通信定义的开放主机服务。根据通信协议和消费者的差异,远程服务可分为

⬤ 资源(Resource)服务

⬤ 供应者(Provider)服务

⬤ 控制器(Controller

⬤ 服务与事件订阅者(Event Subscriber)服务。

b、本地服务

本地服务是为进程内通信定义的开放主机服务,对应于应用层的应用服务。引入本地服务的价值在于:

⬤ 对领域模型形成了一层间接的外观层,避免领域模型被泄露在外

⬤ 对于进程内协作的限界上下文,降低了跨进程调用的通信成本与序列化成本

2)发布语言

服务接口的消息契约则满足发布语言模式,形成两个限界上下文之间的交换模型。由于北向网关封装了内部的领域模型,因此远程服务和本地服务的方法定义都不应该包含领域模型对象,而是通过引入发布语言(PL)模式定义消息契约模型。

⬤ 当远程服务为资源服务或提供者服务时,它们主要面向下游限界上下文或第三方调用者,服务的消息契约模型由请求消息与响应消息组成。

当远程服务为控制器服务时,面向的调用者往往为 UI 前端,服务的消息契约模型实际为面向前端的展现模型。

当远程服务为事件的订阅者时,毫无疑问,服务的消息契约模型就是事件。

当外部请求从远程服务进入时,必须经由本地服务发起对领域层的调用请求;因此,本地服务需要完成消息契约模型与领域模型之间的转换。有时候,远程服务与它调用的本地服务采用的消息契约模型并不相同,那么远程服务还需要完成两种不同的消息契约模型的转换

2、南向网关的演变

防腐层( ACL )作为一种上下文映射模式,主要是为了防止上游限界上下文的变化,其核心思想为 控制权的转移:下游团队不再寄希望于上游团队履行契约不变化的承诺,而是在自己的自治边界内定义接口,从而具有了该接口的控制权,如此才有可能守住内部领域模型的稳定性,满足“稳定空间”的自治特性。

如果将防腐层防止腐化的目标从上游限界上下文扩大至当前限界上下文的所有外部环境,包括如数据库、消息队列这样的环境资源,也包括系统外部的伴生系统,那么,防腐层就承担了菱形对称架构南向网关的角色其中南向网关的端口提供了抽象,并由适配器封装访问外部环境的具体实现

根据一个限界上下文可能要与之协作的外部环境的不同,南向网关的端口可以分为

资源库(repository)端口:隔离对外部数据库的访问,对应的适配器提供聚合的持久化能力

客户端(client)端口:隔离对上游限界上下文或第三方服务的访问,对应的适配器提供对服务的调用能力

事件发布者(event publisher)端口:隔离对外部消息队列的访问,对应的适配器提供发布事件消息的能力

其他端口:若限界上下文还需要与其他外部环境,如文件、网络,也可以定义对应的端口

一、改进的菱形对称架构

image-20240823174741746

1、界限上下文代码模型

菱形对称架构对领域驱动设计的分层架构做出了调整。遵循整洁架构的精神,作为远程服务调用者的 UI 展现层视为外部资源被推出了限界上下文的边界之外。菱形对称架构还去掉了应用层和基础设施层的概念,以统一的网关层进行概括,并以北向与南向分别体现了来自不同方向的请求。如此形成的对称结构突出了领域模型的核心作用,更加清晰地体现了业务逻辑、技术功能与外部环境之间的边界。

currentcontext
- ohs(northbound)
- remote
- controllers
- resources
- providers
- subscribers
- local
- appservices
- pl(messages)
- domain
- acl(southbound)
- ports
- repositories
- clients
- publishers
- adapters
- repositories
- client
- publishers
- pl(messages)

2、菱形对称架构的价值

当我们为限界上下文引入菱形对称架构之后

⬤ 更加清晰地展现上下文映射模式之间的差异,并凸显了防腐层与开放主机服务的重要性

⬤ 遵循菱形对称架构的领域驱动架构亦具有更好的响应变化的能力。

1)展现上下文映射模式

我们以查询订单业务场景来展现菱形对称架构对上下文映射模式的体现。查询订单时,需要获取订单项对应商品的商品信息,即产生订单上下文与商品上下文的协作关系,进而产生两个限界上下文的“模型依赖”。随着设计视角的变化,选择的上下文映射模式也在相应发生变化,菱形对称架构可以清晰地体现这一变化。

a、领域模型依赖

领域模型依赖:上下文映射采用共享内核或遵奉者模式,下游上下文的领域模型直接重用上游上下文的领域模型。

作为上游的商品上下文,它的团队总是高高在上,不大愿意理睬下游团队的互换,而下游团队又不愿意抛开上游团队另起炉灶,就会无奈选择遵奉者模式。又或者你认为商品上下文设计的领域模型足够稳定,且具有非常大的重用价值,就可以主动选择共享内核模式。它们的共同特点都是重用上游的领域模型,此时的模型依赖应准确地描述为“领域模型的依赖”。通过菱形对称架构表现为下图

如图,清晰地展现了重用领域模型的方式会突破菱形对称架构北向网关修筑的堡垒,让商品上下文的领域模型直接暴露在外,下游限界上下文修筑的南向网关防线也如同虚设,因为它被 OrderService“ 完美”地忽略了。

image-20240826155802096

b、消息契约模型依赖

消息契约模型依赖:上下文映射采用防腐层与开放主机服务模式的结合,下游上下文可以直接重用上游上下文的消息契约模型,也可以各自定义,但在逻辑概念上仍然存在依赖关系。

如果订单上下文与商品上下文处于同一进程,根据菱形对称架构的定义,位于下游的订单上下文可以通过其南向网关发起对商品上下文北向网关中本地服务的调用。为了保护领域模型,商品上下文在北向网关中还定义了消息契约模型,表现如下图:

此时的菱形对称架构体现了防腐层模式与开放主机服务模式的共同协作,两边的领域模型互不知情,但为了避免不必要的模型定义,位于下游的订单上下文直接重用了上游定义的消息契约模型类 ProductResponse,如此还可减少转换消息模型对象的成本。此时的“模型依赖”可以视为“消息契约模型的依赖”,由于南向网关中的 ProductClient 端口对调用关系进行了抽象,防腐层的价值仍然存在。

注意:虽然 ProductClientAdapter 直接重用了上游的 ProductResponse 类,但在 ProductClient 端口的接口定义中,却不允许出现上游的消息契约模型。否则,就会让消息契约模型侵入到订单上下文的领域模型中。为此,南向网关客户端端口的接口方法应操作自己的领域模型,然后在适配器的内部实现完成消息契约模型与领域模型的转换。

image-20240826161140250

c、无模型依赖

无模型依赖:上下文映射采用分离方式模式,限界上下文根据自己的知识语境定义自己的领域模型,从而解除了对上游的模型依赖关系。 是最佳的应对方案,真正展现了限界上下文的价值,即对领域模型的控制力

如果订单上下文与商品上下文位于不同进程,它们之间的模型依赖就不存在了,我们需要为它们定义属于自己的模型对象。例如,订单上下文的 ProductClientAdapter 调用了商品上下文北向网关的外部服务 ProductResource,该服务操作的消息契约模型为ProductResponse 。为了支持消息反序列化,就需要在订单上下文的南向网关定义与之一致的 ProductResponse 类:

如下图所示,通过在各自限界上下文内部定义各自的消息契约模型,彻底解除了两个限界上下文之间的“模型依赖”,并通过南向网关防腐层与北向网关开放主机服务降低了彼此的耦合。

但并不意味着它们是彻底解耦的。即使各自定义消息契约模型,斩断了因为重用引起的依赖链条,它们仍然存在隐含的逻辑概念映射关系,真可以说是藕断丝连,并存在变化的级联反应。

譬如说,国家政策要求电商平台销售的所有商品都需要添加一个“是否绿色环保”的新属性。为此,商品上下文领域层的 Product 类新增了 isGreen() 属性,北向网关层定义的 ProductResponse 类也需随之调整。这一知识的变更也会传递到下游的订单上下文。通过对上游的开放主机服务进行版本管理,或在下游引入防腐层进行隔离保护,一定程度可维持订单领域的稳定性;但是,如果需求要求获得的商品信息同样呈现“是否绿色环保”的属性,就必须修改订单上下文中 ProductResponse 类的定义了。

image-20240826175952360

若真正体会了限界上下文作为知识语境的业务边界特征,就可以将订单包含的商品信息视为订单上下文的领域模型,隐含的统一语言为“已购商品(Purchased Product )”,它与商品上下文的商品属于不同的领域概念,处于不同的业务边界,然后共享同一个 productId

在订单上下文中,Product 作为 Order 聚合的组成部分,它的生命周期与 Order 的生命周期绑定在一起,统一由 OrderRepository管理。这意味着在保存订单时,也已保存了与订单相关的商品信息,在获取订单及其商品信息时,自然就无需求助于商品上下文。此时,查询订单的业务场景就不会带来二者之间的协作关系,形成了分离方式的上下文映射模式:

或许有人会提出疑问:由于订单上下文的商品信息仅含订单需要的商品基本信息,若需获取更多商品信息,是否意味着订单上下文需要向商品上下文发起请求呢?其实不然,因为这一请求并非订单上下文发起,而是客户通过在前端点击商品的“查看详情”按钮发起调用。由于页面已经包含了 productId 的值,前端可直接向商品上下文的远程服务 ProductController 发起调用请求,与订单上下文无关。

image-20240826180353455

3)响应变化的能力

限界上下文之间产生协作时,通过菱形对称架构可以更好地响应协作关系的变化。它设定了一个基本原则:即下游限界上下文需要通过南向网关与上游限界上下文的北向网关进行协作,简言之,就是防腐层与开放主机服务的协作。这是两种通信集成模式的融合,当这一方式运用到两种团队协作模式上时,既能促进上下游团队之间的合作,又能保证各个团队相对的独立性。这两种团队协作模式就是:

客户方/供应方模式

发布者/订阅者模式

a、客户端/供应方模式

客户方/供应方模式是采用同步通信实现上下游团队协作的模式,参与协作的角色包括:

⬤ 下游客户方:防腐层的客户端端口作为适配上游服务的接口,客户端适配器封装对上游服务的调用逻辑

⬤ 上游供应方:开放主机服务的远程服务与本地服务为下游限界上下文提供具有业务价值的服务

客户方的适配器到底该调用供应方的远程服务还是本地服务,取决于这两个限界上下文的通信边界

1、客户方与供应方同一进程

如果客户方与供应方位于同一个进程边界内,客户方的适配器就可以直接调用本地服务。对本地服务的调用发生在同一个进程中,通信机制更加健壮,更加可控,无需分布式通信的网络传输成本,也省掉了消息协议的序列化成本。供应方的本地服务作为北向网关,同样提供了对领域模型的保护,确保了领域模型的独立性和完整性。协作图如下所示:

image-20240826201930916

2、客户方和供应方不同进程

如果客户方与供应方处于不同的进程边界,就由远程服务来响应客户方适配器发起的调用。根据通信协议与序列化机制的不同,可以选择资源服务或供应者服务作为远程服务来响应这样的分布式调用。远程服务在收到客户端请求后,会通过本地服务将请求最终传递给领域层的领域模型。协作图如下所示:

image-20240826202213312

b、发布者/订阅者模式

发布者/订阅者模式是采用异步通信实现上下游团队协作的模式,参与协作的角色包括:

⬤ 上游发布者:防腐层的发布者端口负责发布事件,它并不需要关心下游订阅者会如何消费事件,但需要就事件契约与下游团队沟通达成一致

⬤ 下游订阅者:开放主机服务的订阅者远程服务需要侦听事件总线,获取发布者发布的事件,然后将事件传递给本地服务,由本地服务对事件进行处理

注意,发布者/订阅者模式的上下游关系与参与协作的网关方向和客户方/供应方完全不同。

1、发布者虽然是上游,却由南向网关的防腐层负责发布事件;

2、订阅者虽然是下游,却由北向网关的开放主机服务负责订阅事件,进而处理事件。

因为领域驱动设计定义的上下游关系,是因为上游的变动会影响下游,上游的知识会传递给下游。在发布者/订阅者模式中,二者之间的耦合主要来自对事件的定义。因此,当发布者修改了事件后,会影响到订阅者,发布者传递给下游的知识,其实就是事件本身。

1、远程通信

当两个团队分别为事件的发布者与订阅者时,它们之间往往通过引入事件总线作为中介来维持彼此的通信。因此,在各自的限界上下文内部,都需要隔离领域模型与事件总线的通信机制。

采用菱形对称模型,即可通过网关层的设计元素来实现这种隔离。事件的发布者位于防腐层,发布者端口提供抽象定义,发布者适配器负责将事件发布给事件总线;事件的订阅者属于开放主机服务层的远程服务,在订阅到事件之后,交由本地服务来处理事件。

2、本地通信

应用事件:如果事件由本地服务发布,考虑到本地服务实际就是领域驱动设计中的应用服务,可以将该事件命名为“应用事件( Application Event)”。既然由北向网关的本地服务发布事件,它的触发者就有可能是当前限界上下文外部的客户端调用,例如前端UI发起对远程服务的调用,然后委派给了本地服务。在本地服务调用领域层执行完整个用例的领域逻辑后,组装好待发布的应用事件,通过调用南向网关的发布者端口,由注入的发布者适配器最终完成事件的发布。整个调用时序如下所示:

image-20240826203459864

领域事件:如果是领域层的领域模型对象在执行某一个领域行为时发布了事件,该事件就为“领域事件(Domain Event)”。由于发布事件需要与外部的事件总线协作,就需要调用南向网关的发布者端口。为了保证领域模型中聚合的纯粹性,就应该由领域模型的领域服务调用发布者端口发布领域事件。调用时序如下所示:

image-20240826203742523

c、总结

引入事件总线的发布者/订阅者模式具有松散耦合的特点,在结合了防腐层与开放主机服务之后,领域模型并不依赖发布事件与订阅事件的实现机制,意味着它们对事件总线的依赖也能够降到最低。只要通过积极的团队协作,定义符合上下游共同目标的事件,它们就具有了非常强的响应业务变化的能力。

无论是客户方/发布方模式,还是发布者/订阅者模式,菱形对称架构都能够将上游的变化产生的影响降到最低。一个自治的限界上下文,需要菱形对称架构来保证。由采用菱形对称架构的限界上下文组成的业务系统,既有高内聚的领域内核,又有松耦合的协作空间,就能更好地响应变化,使得系统具有更强的架构演进能力。

二、菱形对称架构的运用

通过一个简化的提交订单业务服务来说明在菱形对称架构下,限界上下文之间以及内部各个设计元素是如何协作的。参与协作的限界上下文包括订单上下文仓储上下文通知上下文客户上下文假定每个限界上下文以微服务形式部署,位于不同的进程

流程如下:

1、客户向订单上下文发送提交订单的客户端请求;

2、订单上下文向库存上下文发送检查库存量的客户端请求;

3、库存上下文查询库存数据库,返回库存信息;

5、若库存量符合订单需求,则订单上下文访问订单数据库,插入订单数据;

6、插入订单成功后,移除购物车对应的购物车项;

7、订单上下文调用库存上下文的锁定库存量服务,对库存量进行锁定;

8、提交订单成功后,发布 OrderPlaced 事件到事件总线;

9、通知上下文订阅 OrderPlaced 事件,调用客户上下文获得该订单的客户信息,组装通知内容;

10、通知上下文调用短信服务,发送短信通知客户。”

1、订单上下文的内部协作

客户要提交订单,通过前端 UI 向订单上下文远程服务 OrderController 提交请求,然后将请求委派给应用服务OrderAppService

1)订单上下文-远程服务 OrderController

package com.dddexplained.diamonddemo.ordercontext.northbound.remote.controller;

@RestController
@RequestMapping(value="/orders")
public class OrderController {
   @Autowired
   private OrderAppService orderAppService;

   @PostMapping
   public void placeOrder(PlacingOrderRequest request) {
      orderAppService.placeOrder(request);
   }
}

2)订单上下文-本地服务 OrderAppService

package com.dddexplained.diamonddemo.ordercontext.northbound.local.appservice;

@Service
public class OrderAppService {
   @Autowired
   private OrderService orderService;

   @Transactional(rollbackFor = ApplicationException.class)
   public void placeOrder(PlacingOrderRequest request) {
     
   }
}

3)订单上下文-远程服务&本地服务消息契约

远程服务与本地服务操作的消息契约模型定义在 message包中,这些消息契约模型都定义了如 to()from() 之类的转换方法,用于消息契约模型与领域模型之间的互相转换

package com.dddexplained.diamonddemo.ordercontext.message;

import java.io.Serializable;
import com.dddexplained.diamonddemo.ordercontext.domain.Order;

public class PlacingOrderRequest implements Serializable {
   
  public Order to() {
      return new Order();
   }
}

2、订单上下文与库存上下文的协作

订单上下文的应用服务 OrderAppService 收到 PlacingOrderRequest 请求,在将该请求对象转换为 Order 领域对象后,通过领域服务 OrderService 提交订单。提交订单时,需要验证订单的有效性,再检查库存量。验证订单的有效性由 Order 聚合根承担,库存量的检查通过南向网关的客户端端口 InventoryClient

1)订单上下文-应用程序 OrderService

package com.dddexplained.diamonddemo.ordercontext.domain;

@Service
public class OrderService {
  
   @Autowired
   private InventoryClient inventoryClient;

   public void placeOrder(Order order) {
      if (order.isInvalid()) {
         throw new InvalidOrderException();
      }

      InventoryReview inventoryReview = inventoryClient.check(order);
      if (!inventoryReview.isAvailable()) {
         throw new NotEnoughInventoryException();
      }

      ......
   }
}

2)订单上下文-出口端口 InventoryClient

由于南向网关的客户端端口 InventoryClient 是面向领域模型的,端口的接口定义不能掺杂任何与领域模型无关的内容,故而接口方法操作的对象应为领域模型对象:

package com.dddexplained.diamonddemo.ordercontext.southbound.port.client;

public interface  InventoryClient{
  
  InventoryReview check(Order order);
   
  void lock(Order order);
}

2)订单上下文-出口适配器 InventoryClientAdapter

客户端适配器 InventoryClientAdapter 实现了端口接口,需要在其内部将领域模型对象转换为上游远程服务能够识别的消息契约对象

package com.dddexplained.diamonddemo.ordercontext.southbound.adapter.client;

@Component
public class InventoryClientAdapter implements InventoryClient {
   
   private static final String INVENTORIES_RESOURCE_URL = "http://inventory-service/inventories";

   @Autowired
   private RestTemplate restTemplate;

   @Override
   public InventoryReview check(Order order) {
      CheckingInventoryRequest request = CheckingInventoryRequest.from(order);
      InventoryReviewResponse reviewResponse = restTemplate.postForObject(
        INVENTORIES_RESOURCE_URL, request, InventoryReviewResponse.class);
      return reviewResponse.to();
   }

   @Override
   public void lock(Order order) {
      LockingInventoryRequest inventoryRequest = LockingInventoryRequest.from(order);
      restTemplate.put(INVENTORIES_RESOURCE_URL, inventoryRequest);
   }
}


订单上下文与库存上下文位于不同进程,需要各自定义消息契约,故而在订单上下文的南向网关中定义对应的消息契约模型CheckingInventoryRequestInventoryReviewResponse

package com.dddexplained.diamonddemo.ordercontext.message;

import java.io.Serializable;
import com.dddexplained.diamonddemo.ordercontext.domain.Order;

public class CheckingInventoryRequest implements Serializable {
   public static CheckingInventoryRequest from(Order order) {}
}

package com.dddexplained.diamonddemo.ordercontext.message;

import java.io.Serializable;
import com.dddexplained.diamonddemo.ordercontext.domain.InventoryReview;

public class InventoryReviewResponse implements Serializable {
   public InventoryReview to() {}
}

3、库存上下文的内部协作

当下游的订单上下文发起对库存上下文远程服务 InventoryResource 的调用时,又会通过应用服务 InventoryAppService 来调用领域服务 InventoryService ,然后,经由端口 InventoryRepository 与适配器 InventoryRepositoryAdapter 访问库存数据库,获得库存量的检查结果

1)库存上下文-远程服务:InventoryResource

package com.dddexplained.diamonddemo.inventorycontext.northbound.remote.resource;

@RestController
@RequestMapping(value="/inventories")
public class InventoryResource {
   @Autowired
   private InventoryAppService inventoryAppService;

   @PostMapping
   public ResponseEntity<InventoryReviewResponse> check(CheckingInventoryRequest request) {
      InventoryReviewResponse reviewResponse = inventoryAppService.checkInventory(request);
      return new ResponseEntity<>(reviewResponse, HttpStatus.OK);
   }
}

2)库存上下文-本地服务 InventoryAppService

package com.dddexplained.diamonddemo.inventorycontext.northbound.local.appservice;

@Service
public class InventoryAppService {
   @Autowired
   private InventoryService inventoryService;

   public InventoryReviewResponse checkInventory(CheckingInventoryRequest request) {
      InventoryReview inventoryReview = inventoryService.reviewInventory(request.to());
      return InventoryReviewResponse.from(inventoryReview);
   }
}

3)库存上下文-应用程序 InventoryService

package com.dddexplained.diamonddemo.inventorycontext.domain;

@Service
public class InventoryService {
   @Autowired
   private InventoryRepository inventoryRepository;

   public InventoryReview reviewInventory(List<PurchasedProduct> purchasedProducts) {
      List<String> productIds = purchasedProducts.stream()
        										.map(p -> p.productId()).collect(Collectors.toList());
      List<Product> products = inventoryRepository.productsOf(productIds);

      List<Availability> availabilities = products.stream()
        					.map(p -> p.checkAvailability(purchasedproducts)).collect(Collectors.toList());
      return new InventoryReview(availabilities);
   }
}


4)库存上下文-出口端口 InventoryRepository

package com.dddexplained.diamonddemo.inventorycontext.southbound.port.repository;

@Repository
public interface InventoryRepository {
   List<Product> productsOf(List<String> productIds);
}

3、订单上下文成功提交订单

领域服务 OrderService 在确认了库存量满足订单需求后,通过端口 OrderRepository 以及适配器 OrderRepositoryAdapter 访问订单数据库,插入订单数据。一旦订单插入成功,还需要移除购物车中对应的购物车项。

由于购物车与订单都在订单上下文中,订单上下文的领域服务 OrderService 可以直接调用领域服务 ShoppingCartService。移除购物车项后,领域服务 OrderService 还要调用库存上下文的远程服务 InventoryResource 锁定库存量,从而成功完成订单的提单。领域服务 OrderService 的实现如下:”

1)订单上下文-应用程序 OrderService

package com.dddexplained.diamonddemo.ordercontext.domain;

@Service
public class OrderService {

   @Autowired
   private OrderRepository orderRepository;
  
   @Autowired
   private InventoryClient inventoryClient;
  
   public void placeOrder(Order order) {
      if (!order.isValid()) {
         throw new InvalidOrderException();
      }
      InventoryReview inventoryReview = inventoryClient.check(order);
      if (!inventoryReview.isAvailable()) {
         throw new NotEnoughInventoryException();
      }
      orderRepository.add(order);
     
      ShoppingCartService.removeItems(order.customerId(), Order.purchasedProducts());
      
      inventoryClient.lock(order);
   }
}

4、订单上下文发布应用事件

订单上下文的应用服务 OrderAppService 会在 OrderService 成功提交订单之后组装 OrderPlaced 应用事件,调用端口EventPublisher,经由适配器 EventPublisherAdapter 将事件发布到事件总线:

发布的 OrderPlaced 应用事件属于订单上下文南向网关的消息契约。位于不同进程的订阅者要订阅该应用事件,为了满足反序列化的要求,需要在所属限界上下文的北向网关定义与之对应的应用事件。在提交订单的业务场景中,该事件的订阅者只有通知上下文,因此在通知上下文北向网关的消息契约中也定义了一个相同的OrderPlaced应用事件。”

摘录来自 解构领域驱动设计 张逸 此材料可能受版权保护。

package com.dddexplained.diamonddemo.ordercontext.northbound.local.appservice;
@Service
public class OrderAppService {
   
   @Autowired
   private OrderService orderService;
  
   @Autowired
   private EventPublisher eventPublisher;
  
  private static final Logger logger = LoggerFactory.getLogger(OrderAppService.class);
   
   @Transactional(rollbackFor = ApplicationException.class)
   public void placeOrder(PlacingOrderRequest request) {
      try {
         Order order = request.to();
         orderService.placeOrder(order);
         OrderPlaced orderPlaced = OrderPlaced.from(order);
         eventPublisher.publish(orderPlaced);
      } catch (DomainException ex) {
         logger.warn(ex.getMessage());
         throw new ApplicationException(ex.getMessage(), ex);
      }
   }
}

5、通知上下文订阅应用事件

1)通知上下文-远程服务 EventSubscriber

通知上下文的远程服务 EventSubscriber 订阅了 OrderPlaced 事件,一旦接收到该事件,就交由事件处理器处理该事件。事件处理器是一个接口,定义为:

public interface OrderPlacedEventHandler {
   void handle(OrderPlaced event);
}
package com.dddexplained.diamonddemo.notificationcontext.northbound.remote.subscriber;
public class EventSubscriber {
  
   @Autowired
   private OrderPlacedEventHandler eventHandler;
   
  @KafkaListener(id = "order-placed", 
                  clientIdPrefix = "order", 
                  topics = {"topic.e-commerce.order"}, 
                  containerFactory = "containerFactory")
    public void subscribeEvent(String eventData) {
      OrderPlaced orderPlaced = JSON.parseObject(eventData, OrderPlaced.class);
      eventHandler.handle(orderPlaced);
   }

2) 通知上下文-本地服务 NotificationAppService

package com.dddexplained.diamonddemo.notificationcontext.northbound.local.appservice;
@Service
public class NotificationAppService implements OrderPlacedEventHandler {
   @Autowired
   private NotificationService notificationService;
   public void handle(OrderPlaced orderPlaced) {
      notificationService.notify(orderPlaced.to());
   }
}

6、通知上下文与客户上下文的协作

1、NotificationService 领域服务会调用端口 CustomerClient ,然后可以经由适配器 CustomerClientAdapter 向客户上下文的远程服务 CustomerResource 发送调用请求。

2、在客户上下文内部,由北向南,依次通过远程服务 CustomerResource 、应用服务CustomerAppService 、领域服务CustomerService 和南向网关的端口 CustomerRepository 与适配器 CustomerRepositoryClient 完成对客户信息的查询,回调用者需要的信息。

3、通知上下文的领域服务 NotificationService 在收到该响应消息后,组装领域对象 Notification ,再通过本地的端口 SmsClient 与适配器 SmsClientAdapter ,调用短信服务发送通知短信:

package com.dddexplained.diamonddemo.notificationcontext.domain;

@Service
public class NotificationService {
  
  @Autowired
  private CustomerClient customerClient;
   
  @Autowired
  private SmsClient smsClient;
  
  public void notify(Notification notification) {
      CustomerResponse customerResponse = customerClient.customerOf(notification.to().id());
      notification.filledWith(customerResponse.to());
      smsClient.send(notification.to().phoneNumber(), notification.content());
   }
}

7、流程结束-总结

整个流程到此结束。显然,若每个限界上下文都采用菱形对称架构,代码结构就会变得非常清晰。各个层各个模块各个类都有着各自的职责,泾渭分明,共同协作。

同时,网关层对领域层的保护是不遗余力的,没有让任何领域模型对象泄露到限界上下文边界之外。唯一带来的成本就是需要重复定义消息契约对象,并实现领域模型与消息契约模型之间的转换逻辑

三、系统分层架构

系统上下文界定了目标系统解空间的范围,通过运用分而治之的思想,将整个解空间分解为多个限界上下文,降低了目标系统的规模。菱形对称架构模式为限界上下文内部建立了清晰的结构,并对它们之间的协作进行约束和指导。

相较而言,系统上下文位于更高的层次,也需要引入与之匹配的架构模式,把限界上下文当作基本的架构单元,从目标系统的角度确定整个系统的结构。这个架构模式就是系统分层架构

1、关注点分离

对于一个大型的复杂系统,遵循关注点分离原则对其进行分解总是最为有效的降低复杂度的手段。

1、传统的系统的纵横切分:

关注点分离需要按照变化的方向进行,如此才能满足架构的正交性。将目标系统视为一个长方体,沿纵向与横向两个方向的关注点对长方体进行纵横切分。传统的纵横切分只有一个抽象层次,即系统的抽象层次


image-20240905145824595

2、领域驱动设计切分:

限界上下文根据业务能力对整个系统进行了纵向切分,并在它的自治边界内建立了独立的架构。限界上下文的纵向切分并非彻底的、自顶向下的完整切分,例如它并没有将前端的展现层包含在内,其自治性又突破了纵横交错的界限,形成了图12-23所示的架构。

这样的架构虽然仍然存在水平方向与垂直方向的切分,但如果将限界上下文看作一个黑箱,它更像一种扁平结构。扁平结构保证了架构的简单性,但随着系统变得越来越复杂,其抽象层次的不同会使得它的简单性无法满足系统的复杂度,就好似一家大型国际企业,往往无法采用扁平的组织结构进行有效管理。

之所以出现扁平结构,是因为我们将所有的限界上下文放在了同一个抽象层次。在系统层面,限界上下文统一了业务、持久化和数据库,作为一个整体与UI展现逻辑形成了横向切分,遂演变为由UI展现层与限界上下文组成的两层结构。

界限上下文:

image-20240905145921980

2、映射子领域

领域域驱动设计依据重要程度划分为核心子领域、支撑子领域和通用子领域。既然架构映射阶段是对问题空间的求解过程,那么,子领域针对问题空间的优先级划分自然会影响到解空间的架构决策,并将其映射到整个系统的架构上。

业务价值层-核心子领域:子领域的划分关键在于“选择核心”,也就是精炼出那些“能够表示业务领域并解决业务问题的模型部分。这部分模型能够为系统增加业务价值,形成问题空间的核心子领域到解空间限界上下文的映射。

基础层-通用子领域&支持子领域:它们抽象出来的概念要么是很多业务都需要的,要么就是支撑业务的某个方面,这意味着它们借助核心子领域映射的限界上下文间接为系统提供业务价值。不要将基础层与领域驱动设计的基础设施层混为一谈,因为在菱形对称架构中,基础设施层实际上是网关层

image-20240905200043467

设计时,一定要注意系统上下文的边界。系统边界之外的功能不属于任何一个限界上下文。

假设某个开源库实现了文件上传和下载功能,情形就发生了变化。由于该开源库不在系统上下文的边界内,对需要调用该功能的限界上下文而言,只需在南向网关定义文件上传和下载的端口,并由适配器调用该开源库实现该功能。

但是,如果文件上传和下载的适配器需要适配大量功能,甚至需要定义与文件相关的领域模型对象,或者提供系统专用的配置信息,说明该适配功能具有了独立能力,就可以在基础层定义文件传输上下文。业务价值层的各个限界上下文仍然保留南向网关的文件上传和下载端口,只是在适配器中将原来对外部开源框架的调用转为对文件传输上下文服务的调用。”

3、边缘层

虽然前端 UI 被推到限界上下文的边界之外,但系统上下文层次的架构必须考虑它,毕竟前端 UI 的实现也属于目标系统解空间的范围。

由于设计视角的不同,前端 UI 往往无法与限界上下文一一对应,一个页面可能需要向多个限界上下文的远程服务发送请求,不同请求的返回值用于渲染同一个页面的不同控件。若前端 UI 还需要支持多种前端应用,就会导致前端 UI 与限界上下文的协作关系形成叠加,进一步加剧了二者之间的不匹配关系。这种不匹配关系还会体现到团队组织上。

根据康威定律,前端开发人员不属于限界上下文领域特性团队的一部分,由此需要组建专门的前端团队。前后端的不匹配会为前端团队与后端领域特性团队制造交流障碍。

image-20240905201203434

1)BFF

前后端之间的不匹配问题亦可以引入间接层来解决。该间接层位于服务端,提供了与前端 UI 对应的服务端组件,并成为组成前端 UI 的一部分。面向不同的前端应用,间接层可以提供不同的服务端组件。由于引入的这一间接层具有后端服务的特征,却又为前端提供服务,因而被称为为前端提供的后端( Backends For FrontendsBFF ) 层

前端团队显然更加理解 UI 的设计。为了更好地协调前端团队与后端领域特性团队的沟通与协作,往往由前端团队定义 BFF 层的接口。甚至在技术选型上,为了消除前端开发人员的技术壁垒,选择基于 JavaScriptNode.js,然后由前端团队实现 BFF 层。 BFF 层作为中间层为不同的前端应用提供服务,如分别为 Web 前端与移动前端提供不同的服务接口。不仅如此,它还提供了聚合服务的职责,将本该由多个限界上下文提供的远程服务聚合为一个服务,如图所示。

image-20240905201738266

2)边缘层

BFF层同时履行了 UI 适配与服务聚合的职责,好像专门为后端建立了一个供前端访问的边缘。事实上,它确实可以建立一个网络边缘来保护后端的业务价值层与基础层。例如在物理部署上,可以将 BFF 层部署在一个 DMZ (demilitarized zone) 区,而将后端的业务价值层与基础层部署在安全级别更高的防火墙内网。这时的 BFF 层就不只限于其名称所指代的含义了。为了更好地体现这一中间层的边缘价值,可将其更名为边缘层 ( edge layer)。

边缘层的定义更加抽象。只要满足边缘含义的职责,事实上都可以封装在这一层。例如微服务架构风格所需的API网关,也属于介于后端与前端的边缘

image-20240905202056870

4、领域驱动架构风格

领域驱动设计在架构层面获得的抽象元素就是系统上下文与限界上下文,它们围绕领域为核心驱动力,

以业务能力为核心关注点,分别形成了两个层次的架构模式:系统分层架构模式与菱形对称架构模式

1、系统分层架构保证了整个系统上下文结构的一致性;

2、菱形对称架构规定了限界上下文清晰的边界,并对它们的交互形成了约束与指导。

毫无疑问,它们共同组成了一种独特的架构风格。根据其特征,我将其命名为领域驱动架构风格。”

领域驱动架构风格如图:

image-20240905202639693

5、架构模式

显然,支撑领域驱动架构风格演进能力的关键要素,正是领域驱动战略设计的核心模式——限界上下文。领域驱动架构风格充分利用了系统上下文对解空间的边界定义,并在约束一致性的同时,保证了设计的实用性。 系统分层架构对限界上下文进行了界定与规范,使得它们能够采取一致的方式提供业务能力。

系统分层架构对限界上下文进行了界定与规范,使得它们能够采取一致的方式提供业务能力。同时,根据限界上下文所属子领域的不同,结合具体的业务场景降低对限界上下文的设计要求,不再一视同仁地严格要求运用菱形对称架构;对于基础层的限界上下文,甚至可以不采用领域建模

架构模式 说明
单体架构模式 当限界上下文化身为运行在进程内部的库时,即演进为单体架构模式
微服务架构模式 当限界上下文的通信边界被界定为进程间通信时,即演进为微服务架构模式
面向服务架构模式 当限界上下文根据不同的业务场景定义为不同的通信边界时,即演进为面向服务架构模式或者认为是单体架构与微服务架构组成的混合架构
事件驱动架构模式 当限界上下文之间的协作采用发布者/订阅者映射模式时,即演进为事件驱动架构模式

ContactAuthor