Java-EE-领域驱动设计实践指南-三-

71 阅读58分钟

Java EE 领域驱动设计实践指南(三)

原文:Practical Domain-Driven Design in Enterprise Java

协议:CC BY-NC-SA 4.0

五、货物跟踪器:Spring 平台

快速回顾一下我们迄今为止的旅程

我们将货物跟踪确定为主要问题空间/核心领域,并将货物跟踪应用作为解决这一问题空间的解决方案。

我们确定了货物跟踪应用的各种子域/有界上下文。

我们详细描述了每个有界上下文的域模型,包括集合、实体、值对象和域规则的标识。

我们确定了有界环境中所需的支持领域服务。

我们在有限的上下文中确定了各种操作(命令、查询、事件和故事)。

我们使用 Jakarta EE 实现了一个整体版本的货物跟踪器,并使用 Eclipse MicroProfile 平台实现了一个微服务版本的货物跟踪器。

本章将详细介绍使用 Spring 平台的货物跟踪应用的第三个 DDD 实现。Cargo Tracker 应用将再次使用基于微服务的架构进行设计,和以前一样,我们将把 DDD 工件映射到 Spring 平台中可用的相应实现。

随着我们在实现过程中的进展,将会有与前面章节重复的地方。这是为了适应可能只对特定实现感兴趣而不是浏览所有实现的读者。

说了这么多,让我们首先浏览一下 Spring 平台的概况。

Spring 平台

Spring 平台( https://spring.io/ )最初是作为 Java EE 的替代品发布的,现在已经成为构建企业应用的主要 Java 框架。通过其项目组合提供的功能范围非常广泛,几乎涵盖了构建企业应用所需的每个方面。

与 Jakarta EE 或 Eclipse MicroProfile 不同,在 Jakarta EE 或 Eclipse micro profile 中,有一组规范和多个供应商提供这些规范的实现,Spring 平台提供了一个项目组合。

项目组合涵盖以下主要领域:

  • 核心基础设施项目 提供一组基础项目来构建基于 Spring 的应用

  • 云原生项目 提供了构建具有云原生能力的 Spring 应用的能力

  • 数据管理项目 提供了在基于 Spring 的应用中管理任何类型数据的能力

平台内的各个项目如图 5-1 所示。

img/473795_1_En_5_Fig1_HTML.png

图 5-1

Spring 平台项目

正如所看到的,项目的广度是很大的,并且提供了广泛的能力。重申一下,本章的既定目标是利用基于微服务架构的 DDD 原则来实现货物跟踪应用。在这种情况下,我们将只使用可用项目 (Spring Boot、Spring 数据和 Spring 云流) 的子集来帮助我们实现目标。

简单回顾一下,微服务平台的要求如图 5-2 所示。

img/473795_1_En_5_Fig2_HTML.jpg

图 5-2

微服务平台要求

让我们简单地介绍一下这些项目的功能,并把它们映射到前面说明的需求。

Spring Boot:能力

Spring Boot 是任何基于 Spring 的微服务应用的基础。作为一个非常固执己见的平台,Spring Boot 使用统一的开发经验,帮助构建具有 REST、数据和消息传递功能的微服务。这是由 Spring Boot 在提供 REST、数据和消息传递功能的实际项目之上实现的抽象/依赖管理层来完成的。作为一名开发人员,您希望避免在构建微服务应用时管理依赖项和所需配置的麻烦。Spring Boot 通过提供初学者工具包为开发者抽象了所有这些。初学者工具包提供了所需的脚手架,使开发人员能够快速开始开发需要公开 API、处理数据和参与事件驱动架构的微服务。在我们的实现中,我们将依赖于 Spring Boot 提供的三个启动项目( spring-boot-starter-web、spring-boot-starter-data-jpa、spring-cloud-starter-stream-rabbit)。

我们将在实施过程中深入了解这些项目的细节。

从微服务需求映射的角度来看,图 5-3 所示的绿色方框是用 Spring Boot 实现的。

img/473795_1_En_5_Fig3_HTML.jpg

图 5-3

Spring Boot 提供的微服务平台组件

春云

Boot 提供了构建微服务应用的基础技术,而 Spring Cloud 则帮助实现基于 Spring Boot 的微服务应用所需的分布式系统模式。这些包括外部化配置、服务注册和发现、消息传递、分布式跟踪和 API 网关。此外,该项目还提供了与 AWS/GCP/Azure 等第三方云提供商进行原生集成的项目。

img/473795_1_En_5_Fig4_HTML.jpg

图 5-4

Spring Cloud 提供的微服务平台组件

  • 从微服务需求映射的角度来看,如图 5-4 所示的橙色方框是用 Spring Cloud 实现的。

  • Spring 平台没有为使用基于编排的 sagas 的分布式事务管理提供任何现成的功能。我们将使用事件编排实现 分布式事务,并使用 Spring Boot 和 Spring Cloud Stream 定制实现

Spring 框架摘要

我们现在对 Spring 平台通过 Spring Boot 和 Spring Cloud 项目构建微服务应用有了一个大致的概念。

让我们利用这些技术来实现货物跟踪器。作为实现的一部分,可能会有相当多的重复,因为某些读者可能只对这个实现感兴趣。

具有 Spring Boot 的有界上下文

有界上下文是我们基于 Spring 的货物跟踪微服务应用的 DDD 实现的解决方案阶段的起点。在微服务架构风格中,每个有界上下文必须是一个 自包含的独立可部署单元 ,不直接依赖于我们的问题空间中的任何其他有界上下文。

将货物跟踪应用分割成多个微服务的模式将和以前一样,也就是说,我们将核心域分割成一组 业务能力/子域解决方案,其中每一个都作为一个单独的有界上下文。

实现有界上下文包括将我们的 DDD 工件逻辑分组到一个可部署的工件中。货物跟踪应用中的每个有界上下文都将被构建成一个 Spring Boot 应用。Spring Boot 应用的结果工件是一个 自包含 fat JAR 文件 ,它包含所有需要的依赖项(例如,数据访问库、REST 库)和配置。fat JAR 文件还包含一个嵌入式 web 容器(在我们的例子中是 Tomcat)作为运行时。这确保了我们不需要任何外部应用服务器来运行我们的 fat JAR。Spring Boot 应用的剖析如图 5-5 所示。

img/473795_1_En_5_Fig5_HTML.jpg

图 5-5

剖析 Spring Boot 应用

从部署的角度来看,如图 5-6 所示,每个微服务都是一个独立的自包含可部署单元 (fat JAR 文件)。

微服务将需要一个 数据存储 来存储它们的状态。我们选择为每个服务模式采用 数据库, 也就是说,我们的每个微服务都将拥有自己独立的数据存储。就像我们的应用层有多种语言的技术选择一样,我们的数据存储也有多种语言的选择。我们可以选择一个普通的关系数据库(如 Oracle、MySQL、PostgreSQL),一个 NoSQL 数据库(如 MongoDB、Cassandra),甚至一个内存中的数据存储(如 Redis)。选择主要取决于微服务想要满足的可伸缩性需求和用例类型。对于我们的实现,我们决定选择 MySQL 作为数据存储。部署架构如图 5-6 所示。

img/473795_1_En_5_Fig6_HTML.jpg

图 5-6

我们 Spring Boot 微服务的部署架构

有界上下文:打包

要开始我们的打包,第一步是创建一个常规的 Spring Boot 应用。我们将使用 Spring Initializr 工具( https://start.spring.io/ ),这是一个基于浏览器的工具,有助于轻松创建 Spring Boot 应用。图 5-7 展示了利用 Initializr 工具创建预订微服务。

img/473795_1_En_5_Fig7_HTML.jpg

图 5-7

Spring Initializr 工具用于搭建具有依赖关系的 Spring Boot 项目

我们已经用创建了这个项目

  • –com . practical DD . cargo tracker

  • 神器–booking ms

  • 依赖——Spring Web Starter、Spring Data JPA、Spring Cloud Stream

单击生成项目图标。这将生成一个 ZIP 文件,其中包含预订 Spring Boot 应用以及所有可用的依赖项和配置。

Spring Boot 应用的主应用类用***@ spring boot application***注释进行了注释。它包含一个公共静态 void main 方法,并且是 Spring Boot 应用的入口点。

Booking Application类是我们的 Booking Spring Boot 应用中的主类。清单 5-1 显示了 BookingmsApplication 类:

package com.practicalddd.cargotracker.bookingms;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication //Main class marker annotation
public class BookingmsApplication {
    public static void main(String[] args) {
        SpringApplication.run(BookingmsApplication.class, args);
    }
}

Listing 5-1.Bookingms Application class

构建项目将产生一个 jar 文件( bookingms.jar ),使用命令“Java-JAR booking ms . JAR”将它作为一个简单的 JAR 文件运行,这将打开我们的 Spring Boot 应用。

有界上下文:包结构

决定了打包方面之后,下一步是决定我们每个有界上下文的包结构,也就是说,将各种 DDD 微文件工件逻辑分组到一个可部署的工件中。逻辑分组包括识别一个包结构,我们在这个包结构中放置各种 DD MicroProfile 工件,以实现我们对有界上下文的整体解决方案。

图 5-8 显示了我们任何有界上下文的高级包结构。

img/473795_1_En_5_Fig8_HTML.jpg

图 5-8

有界上下文的包结构

让我们稍微扩展一下包的结构。

图 5-9 显示了一个绑定上下文 Spring Boot 应用包结构的例子,其中BookingmsApplication是主要的 Spring Boot 应用类。

img/473795_1_En_5_Fig9_HTML.jpg

图 5-9

使用 Spring Boot 的预订有界上下文的包结构

让我们扩展一下包的结构。

接口

这个包将所有入站接口封装到由通信协议分类的有界上下文中。 接口 的主要用途是代表域模型协商协议(如 REST API(s)、WebSocket(s)、FTP(s)、自定义协议)。

作为一个例子,Booking Bounded Context 提供 REST APIs,用于发送 状态改变请求,即命令, 给它(例如,Book Cargo 命令,Assign Route to Cargo 命令)。同样,Booking Bounded Context 提供了 REST APIs,用于向其发送 状态检索请求,即查询, (例如,检索货物订舱明细,列出所有货物)。这被分组到“”包中。

**它还有事件处理程序,订阅由其他有界上下文生成的各种事件。所有事件处理程序都被分组到“ 【事件处理程序】” 包中。除了这两个包,接口包还包含了“ 变换 ”包。这用于将传入的 API 资源/事件数据转换为域模型所需的相应命令/查询模型。

由于我们需要支持 REST、事件和数据转换,包的结构如图 5-10 所示。

img/473795_1_En_5_Fig10_HTML.jpg

图 5-10

接口的封装结构

应用

应用服务充当有界上下文的域模型的外观。它们提供外观服务,将命令/查询发送到底层的域模型。作为命令/查询处理的一部分,它们也是我们向其他有界上下文发出出站调用的地方。

总而言之,应用服务

  • 参与命令和查询调度

  • 作为命令/查询处理的一部分,在必要时调用基础设施组件

  • 为基础领域模型提供集中的关注点(例如,日志、安全性、度量)

  • 对其他有界上下文进行标注

包装结构如图 5-11 所示。

img/473795_1_En_5_Fig11_HTML.jpg

图 5-11

应用服务的包结构

领域

这个包包含有界上下文的域模型。这是有界上下文的域模型的核心,它包含核心业务逻辑的实现。

我们的有界上下文的核心类如下:

  • 总计

  • 实体

  • 价值对象

  • 命令

  • 事件

包装结构如图 5-12 所示。

img/473795_1_En_5_Fig12_HTML.jpg

图 5-12

我们的领域模型的包结构

基础设施

基础设施包有三个主要目的:

  • 当有界上下文接收到与其状态相关的操作(状态的改变、状态的检索)时,它 需要一个底层知识库来处理操作;在我们的例子中,这个存储库是我们的 MySQL 数据库实例。基础设施包包含有界上下文与底层存储库通信所需的所有必要组件。作为我们实现的一部分,我们打算使用 JPA 或 JDBC 来实现这些组件。

  • 当有界上下文需要传递状态变化事件时,它需要底层事件基础结构来发布状态变化事件。在我们的实现中,我们打算使用一个 消息代理作为底层事件基础设施 (RabbitMQ 可从 rabbitmq.com 下载)。基础设施包包含有界上下文与底层消息代理通信所需的所有必要组件。

  • 我们在基础架构层中包括的最后一个方面是任何种类的特定于 Spring Boot 的配置。

包装结构如图 5-13 所示。

img/473795_1_En_5_Fig13_HTML.jpg

图 5-13

基础设施组件的包装结构

图 5-14 显示了我们任何有界上下文的整个包结构的完整摘要。

img/473795_1_En_5_Fig14_HTML.jpg

图 5-14

任何有界上下文的包结构

这就完成了我们的货物跟踪微服务应用的有界上下文的实现。我们的每个有界上下文都被实现为一个 Spring Boot 应用,一个胖罐子作为工件。有界的上下文被模块整齐地分组在一个包结构中,具有清晰分离的关注点。

让我们进入货物跟踪应用的实现。

货物跟踪实施

本章的下一节将详细介绍利用 DDD 和 Spring Boot/Spring Cloud 作为微服务应用的货物跟踪应用的实现。如前所述,其中一些部分是我们已经看到的内容的重复,但是再次浏览它将有助于强化 DDD 的概念。

图 5-15 显示了我们的各种 DDD 工件的逻辑分组的高级概述。正如所见,我们需要实现两组工件:

img/473795_1_En_5_Fig15_HTML.jpg

图 5-15

DDD 工件的逻辑分组

  • 域模型 将包含我们的 核心域/业务逻辑

  • 域模型服务 ,其中包含 对我们核心域模型 的支持服务

就域模型的实际实现而言,这转化为特定有界上下文/微服务的各种命令、查询和值对象

就域模型服务的实际实现而言,这转化为 有界上下文/微服务的域模型所需的接口、应用服务和基础设施

回到我们的货物跟踪器应用,图 5-16 从各种受限上下文及其支持的操作方面说明了我们的微服务解决方案。如所见,这包含每个有界上下文将处理 的各种 命令、每个有界上下文将服务的 查询、 以及每个有界上下文将订阅/发布事件。每个微服务都是一个独立的可部署工件,有自己的存储。

img/473795_1_En_5_Fig16_HTML.jpg

图 5-16

货物跟踪微服务解决方案

注意

某些代码实现将只包含摘要/片段,以帮助理解实现概念。本章的源代码包含这些概念的完整实现。

领域模型:实现

我们的领域模型是我们的有界上下文的中心特征,并且如前所述,有一组与之相关的工件。这些工件的实现是在 Spring Boot 提供的工具的帮助下完成的。

简单总结一下,我们需要实现的领域模型工件如下:

  • 核心域模型——聚集、实体和值对象

  • 域模型操作–命令、查询和事件

让我们逐一查看这些工件,看看 Spring Boot 为我们实现这些工件提供了哪些相应的工具。

核心领域模型:实现

任何有界上下文的核心域的实现都包括那些将清楚地表达有界上下文的业务意图的工件的识别。在高层次上,这包括聚合、实体和值对象的识别和实现。

聚合/实体/值对象

聚合是我们领域模型的核心。概括地说,我们在每个有界上下文中有四个聚合,如下图 5-17 所示。

img/473795_1_En_5_Fig17_HTML.jpg

图 5-17

我们有限的上下文/微服务中的聚合

聚合的实现包括以下几个方面

  • 聚合类实现

  • 通过业务属性丰富领域,最后

  • 实现实体/值对象

聚合类实现

因为我们打算使用 MySQL 作为每个有界上下文的数据存储,所以我们打算使用 Java EE 规范中的 JPA (Java Persistence API ),它提供了一种定义和实现与底层 SQL 数据存储交互的实体/服务的标准方法。

JPA 集成:Spring 数据

Spring Boot 通过使用 Spring Data JPA 项目 ( https://spring.io/projects/spring-data-jpa )提供了对 JPA 的支持,该项目提供了一种复杂而简单的机制来实现基于 JPA 的存储库。Spring Boot 提供了一个启动项目(Spring-boot-starter-Data-JPA),该项目自动为 Spring Data JPA 配置一组合理的默认设置(例如 Hibernate JPA 实现、Tomcat 连接池)。

当我们将 starter data JPA 项目配置为 Initializr 项目中的依赖项时,它的依赖项会自动添加。除此之外,我们还需要添加 MySQL Java 驱动程序库,以便连接到我们的 MySQL 数据库实例:

pom.xml

清单 5-2 显示了需要在 Spring Initializr 项目生成的 pom.xml 依赖文件 中进行的更改:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
application.properties

Listing 5-2.pom.xml dependency maintainance

除了依赖关系,我们还需要为每个 MySQL 实例配置连接属性。这是在我们的 Spring Boot 应用提供的application . properties文件中完成的。清单 5-3 展示了需要添加的配置属性。必要时,您需要用 MySQL 实例的详细信息替换这些值:

spring.datasource.url=jdbc:mysql://<<Machine-Name>>:<<Machine-Port>>/<<MySQL-Database-Instance-Name>>
spring.datasource.username=<<MySQL-Database-Instance-UserID>>
spring.datasource.password==<<MySQL-Database-Instance-Password>>

Listing 5-3.MySQL connection configuration

这些设置足以在我们的 Spring Boot 应用中设置和实现 JPA。如前所述,Spring Data JPA 项目配置了一组合理的缺省值,使我们能够以最小的努力开始。除非另有说明,否则我们所有有界上下文中的所有聚合都实现了相同的机制。

我们的每个根聚合类都被实现为一个 JPA 实体。JPA 没有提供特定的注释来将特定的类注释为根聚合,所以我们采用常规的 POJO,使用 JPA 提供的标准注释***@实体*** 。以 Booking Bounded Context 为例,它将 Cargo 作为根聚合,清单 5-4 显示了 JPA 实体所需的最小代码:

package com.practicalddd.cargotracker.bookingms.domain.model.aggregates;
import javax.persistence.*;
@Entity //JPA Entity Marker
public class Cargo {
}

Listing 5-4.Cargo root aggregate as a JPA Entity

每个 JPA 实体都需要一个标识符。对于我们的聚合标识符实现,我们选择从 MySQL 序列中为我们的货物聚合生成一个 【技术/代理标识符(主键) 。除了技术标识符,我们还选择拥有一个 业务密钥

业务键传达聚合标识符 clear 的业务意图,即新预订货物的预订标识符,并且是向域模型的外部消费者公开的键(稍后将详细介绍)。另一方面,技术关键字是集合标识符的纯内部表示,并且对于在集合及其依赖对象(参见下面的值对象/实体)之间的有界上下文 内维护关系 是有用的。

继续我们在 Booking Bounded 上下文中的 Cargo Aggregate 的例子,我们将技术/业务键添加到类实现中,直到现在。

清单 5-5 演示了这一点。“@ Id”注释标识我们的货物集合上的主键。没有特定的注释来标识业务键,所以我们只是使用 JPA 提供的“@ Embedded ”注释将其实现为常规的 POJO ( BookingId )和Embeddedit:

package com.practicalddd.cargotracker.bookingms.domain.model.aggregates;
import javax.persistence.*;
@Entity
public class Cargo {
    @Id //Identifier Annotation provided by JPA
    @GeneratedValue(strategy = GenerationType.IDENTITY) // Rely on a MySQL generated sequence
    private Long id;
    @Embedded //Annotation which enables usage of Business Objects instead of primitive types
    private BookingId bookingId; // Business Identifier
}

Listing 5-5.Identifier for the Cargo root aggregate

清单 5-6 显示了 BookingId 业务键类的实现:

package com.practicalddd.cargotracker.bookingms.domain.model.aggregates;
import javax.persistence.*;
import java.io.Serializable;
/**
 * Business Key Identifier for the Cargo Aggregate
 */
@Embeddable
public class BookingId implements Serializable {
    @Column(name="booking_id")
    private String bookingId;
    public BookingId(){}
    public BookingId(String bookingId){this.bookingId = bookingId;}
    public String getBookingId(){return this.bookingId;}
}

Listing 5-6.Business key implementation for the Cargo root aggregate

我们现在有了一个使用 JPA 的聚合(Cargo)的基本实现。除了处理活动绑定的上下文之外,其他聚合具有相同的实现机制。由于它是一个事件日志,我们决定只为聚合实现一个键,即活动 Id。

图 5-18 总结了我们所有聚合的基本实现。

img/473795_1_En_5_Fig18_HTML.jpg

图 5-18

我们聚合的基本实现

领域丰富性:业务属性

准备好基本的实现后,让我们继续讨论集合的核心部分——域丰富性。 任何有界语境的聚合应该能够清晰地表达有界语境的业务语言 。本质上,它在纯技术术语中的意思是我们的集合不应该是贫血的,也就是说,只包含 getter/setter 方法。

一个缺乏活力的集合违背了 DDD 的基本原则,因为它本质上意味着 在应用的多个层中表达的商业语言 ,从长远来看,这反过来又会导致一个不可维护的软件。

**那么我们如何实现一个域丰富的聚合呢?简而言之就是 业务属性和业务方式。 在这一节中,我们的重点将放在业务属性方面,同时我们将涵盖作为领域模型操作实现的一部分的业务方法部分。

聚合的业务属性捕获聚合的状态,作为使用业务术语而不是技术术语描述的属性。

让我们看一下我们的货物总量的例子。

将状态转换为业务概念,货物集合具有以下属性:

  • 货物的始发地

  • 货物的预订金额

  • 路线说明 (始发地、目的地、目的地到达截止日期)

  • 根据路线规格将货物分配到的 路线 。路线由多条 线路 组成,货物可能通过这些线路到达目的地

  • 货物的交付进度 对照其指定的路线规格和路线。交付进度提供了关于 路线状态、运输状态、货物的当前航程、货物的最后已知位置、下一预期活动以及货物上发生的最后活动的细节。

图 5-19 显示了货物集合及其与相关对象的关系。请注意我们如何能够清楚地用 的纯业务术语表示货物总量。

img/473795_1_En_5_Fig19_HTML.jpg

图 5-19

货物集合及其从属关联

JPA 为我们提供了一组注释 (@Embedded,@ Embedded),帮助我们使用业务对象实现聚合类。

清单 5-7 显示了我们的货物集合的例子,其中所有的 依赖项都被建模为业务对象:

package com.practicalddd.cargotracker.bookingms.domain.model.aggregates;
import javax.persistence.*;

import com.practicalddd.cargotracker.bookingms.domain.model.entities.*;
import com.practicalddd.cargotracker.bookingms.domain.model.valueobjects.*;
@Entity
public class Cargo {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Embedded
    private BookingId bookingId; // Aggregate Identifier
    @Embedded
    private BookingAmount bookingAmount; //Booking Amount
    @Embedded
    private Location origin; //Origin Location of the Cargo
    @Embedded
    private RouteSpecification routeSpecification; //Route Specification of the Cargo
    @Embedded
    private CargoItinerary itinerary; //Itinerary Assigned to the Cargo
    @Embedded
    private Delivery delivery; // Checks the delivery progress of the cargo against the actual Route Specification and Itinerary
}

Listing 5-7.Cargo root aggregate dependencies as business objects

聚合的依赖类 建模为实体对象或值对象 。概括地说,有界上下文中的实体对象有自己的身份,但总是存在于根聚合中,也就是说,它们不能独立存在,并且在聚合的整个生命周期中从不改变。另一方面,值对象没有自己的身份,在聚合的任何实例中很容易被替换

继续我们的示例,在货物集合中,我们有以下内容:

  • 【原点】 的货物作为一个 的实体对象 (位置)。这在货物集合实例中不能改变,因此被建模为实体对象。

  • 预订金额 的货物, 路线规格 的货物, 的货物行程 分配给货物,而 交付的货物 作为价值对象。这些对象在任何货物集合实例中都是可替换的,因此被建模为值对象。

让我们浏览一下场景和基本原理,为什么我们将它们作为值对象而不是实体,因为这是一个 重要的领域建模决策 :

  • 当一个新的货物被预订,我们将有 一个新的路线规格一个空货物行程、没有交货进度

  • 当货物被分配一个行程单时, 空载货物行程单 被一个 已分配货物行程单替代。

  • 当货物作为其行程的一部分通过多个港口时, 交付 进度在根集合内被更新和替换。

  • 最后,如果客户选择更改货物的交货地点或交货截止日期, 路线规格更改 ,将分配一个 新的货物路线 ,重新计算 交货,预订金额更改。

它们 都是可替换的,因此将 建模为值对象。 这是对聚合中的实体和值对象进行建模的 经验法则。

实现实体对象/值对象

使用 JPA 提供的***" @ embedded "***注释,将实体对象/值对象实现为 JPA 可嵌入对象。然后使用“ @Embedded ”注释将它们嵌入到聚合中。

清单 5-8 展示了嵌入聚合类的机制。

让我们看一下 Cargo Aggregate 的实体对象/值对象的实现。

清单 5-8 展示了 位置实体对象 。注意分组在 model.entities 下的包名( ):

package com.practicalddd.cargotracker.bookingms.domain.model.entities;
import javax.persistence.Column;
import javax.persistence.Embeddable;
/**
 * Location Entity class represented by a unique 5-digit UN Location code.
 */
@Embeddable
public class Location {
    @Column(name = "origin_id")
    private String unLocCode;
    public Location(){}
    public Location(String unLocCode){this.unLocCode = unLocCode;}
    public void setUnLocCode(String unLocCode){this.unLocCode = unLocCode;}
    public String getUnLocCode(){return this.unLocCode;}
}

Listing 5-8.Location entity object

清单 5-9 展示了 预订金额/路线规格值对象 的示例。注意分组在 model.valueobjects 下的包名( ):

package com.practicalddd.cargotracker.bookingms.domain.model.valueobjects;
import javax.persistence.Column;
import javax.persistence.Embeddable;
/**
 * Domain model representation of the Booking Amount for a new Cargo.
 * Contains the Booking Amount of the Cargo
 */
@Embeddable
public class BookingAmount {
    @Column(name = "booking_amount", unique = true, updatable= false)
    private Integer bookingAmount;
    public BookingAmount(){}
    public BookingAmount(Integer bookingAmount){this.bookingAmount = bookingAmount;}
    public void setBookingAmount(Integer bookingAmount){this.bookingAmount = bookingAmount;}
    public Integer getBookingAmount(){return this.bookingAmount;}
}

Listing 5-9.Booking Amount value object implementation

清单 5-10 展示了 路线规范值对象 :

package com.practicalddd.cargotracker.bookingms.domain.model.valueobjects;
import com.practicalddd.cargotracker.bookingms.domain.model.entities.Location;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import java.util.Date;
/**
 * Route Specification of the Booked Cargo
 */
@Embeddable
public class RouteSpecification {
    private static final long serialVersionUID = 1L;
    @Embedded
    @AttributeOverride(name = "unLocCode", column = @Column(name = "spec_origin_id"))
    private Location origin;
    @Embedded
    @AttributeOverride(name = "unLocCode", column = @Column(name = "spec_destination_id"))
    private Location destination;
    @Temporal(TemporalType.DATE)
    @Column(name = "spec_arrival_deadline")
    @NotNull
    private Date arrivalDeadline;
    public RouteSpecification() { }
    /**
     * @param origin origin location
     * @param destination destination location
     * @param arrivalDeadline arrival deadline
     */
    public RouteSpecification(Location origin, Location destination,
                              Date arrivalDeadline) {
        this.origin = origin;
        this.destination = destination;
        this.arrivalDeadline = (Date) arrivalDeadline.clone();
    }
    public Location getOrigin() {
        return origin;
    }
    public Location getDestination() {

        return destination;
    }

    public Date getArrivalDeadline() {
        return new Date(arrivalDeadline.getTime());
    }
}

Listing 5-10.Route Specification value object implementation

其余的值对象( RouteSpecification、CargoItinerary 和 Delivery )使用“@ Embedded”注释以相同的方式实现,并使用“ @Embedded ”注释嵌入到货物聚合中。

注意

完整的实现请参考本章的源代码。

让我们看看其他聚合(处理活动、航行和跟踪)的简化类图。图 5-20 、 5-21 和 5-22 说明了这一点。

img/473795_1_En_5_Fig22_HTML.jpg

图 5-22

跟踪活动及其相关关联

img/473795_1_En_5_Fig21_HTML.jpg

图 5-21

航程及其从属关联

img/473795_1_En_5_Fig20_HTML.jpg

图 5-20

处理活动及其相关关联

这就完成了核心域模型的实现。接下来让我们看看域模型操作的实现。

注意

本书的源代码通过包分离展示了核心领域模型。您可以在 github.com/practicalddd.查看源代码,以便更清楚地了解域模型中的对象类型

领域模型操作

有界上下文中的域模型操作处理与有界上下文集合的状态相关联的任何种类的操作。其中包括 入站操作(命令/查询)和出站操作(事件)。

命令

命令负责在有界上下文中改变聚集的状态。

在有界环境中实现命令包括以下步骤:

  • 命令的识别/执行

  • 识别/实现处理命令的命令处理程序

命令的识别

命令的识别围绕着识别影响集合状态的任何操作。例如,预订命令有界上下文具有以下操作或命令:

  • 预订货物

  • 运送货物

这两种操作都导致有界环境内货物集合体的状态改变,因此被识别为命令。

命令的执行

一旦确定,就使用常规 POJOs 在 Spring Boot 实现中实现确定的命令。清单 5-11 展示了 BookCargoCommand 类对于 Book Cargo 命令的实现:

package com.practicalddd.cargotracker.bookingms.domain.model.commands;
import java.util.Date;
/**
 * Book Cargo Command class
 */
public class BookCargoCommand {

    private String bookingId;
    private int bookingAmount;
    private String originLocation;
    private String destLocation;
    private Date destArrivalDeadline;

    public BookCargoCommand(){}

    public BookCargoCommand(int bookingAmount,
                            String originLocation, String destLocation, Date destArrivalDeadline){

        this.bookingAmount = bookingAmount;
        this.originLocation = originLocation;
        this.destLocation = destLocation;
        this.destArrivalDeadline = destArrivalDeadline;
    }

    public void setBookingId(String bookingId){this.bookingId = bookingId;}

    public String getBookingId(){return this.bookingId;}

    public void setBookingAmount(int bookingAmount){ 

        this.bookingAmount = bookingAmount;
    }

    public int getBookingAmount(){
        return this.bookingAmount;
    }

    public String getOriginLocation() {return originLocation; }

    public void setOriginLocation(String originLocation) {this.originLocation = originLocation; }

    public String getDestLocation() { return destLocation; }

    public void setDestLocation(String destLocation) { this.destLocation = destLocation; }

    public Date getDestArrivalDeadline() { return destArrivalDeadline; }

    public void setDestArrivalDeadline(Date destArrivalDeadline) { this.destArrivalDeadline = destArrivalDeadline; }
}

Listing 5-11.BookCargoCommand class implementation

识别 命令处理程序

每个命令都有一个相应的命令处理程序。命令处理程序 的作用是处理输入的命令并设置集合的状态。命令处理程序是 域模型中唯一设置聚合状态的地方 。这是一个严格的规则,需要遵循它来帮助实现一个丰富的领域模型。

命令处理程序的实现

由于 Spring 框架不提供任何现成的功能来实现命令处理程序,我们的实现方法将只是 识别集合上的例程,这些例程可以被表示为命令处理程序 。对于我们的第一个命令 Book Cargo, 我们将聚合的构造函数标识为我们的命令处理程序;对于我们的第二个命令 Route Cargo, ,我们创建了一个新的例程“assignor Route()”,作为我们的命令处理程序。

清单 5-12 显示了货物集合构造器的代码片段。构造函数接受 BookCargoCommand 作为输入参数,并设置相应的聚集状态:

/**
 * Constructor Command Handler for a new Cargo booking
 */
public Cargo(BookCargoCommand bookCargoCommand){
    this.bookingId = new BookingId(bookCargoCommand.getBookingId());
    this.routeSpecification = new RouteSpecification(
                new Location(bookCargoCommand.getOriginLocation()),
                new Location(bookCargoCommand.getDestLocation()),
                bookCargoCommand.getDestArrivalDeadline()
        );
    this.origin = routeSpecification.getOrigin();
    this.itinerary = CargoItinerary.EMPTY_ITINERARY; //Empty Itinerary since the Cargo has not been routed yet
    this.bookingAmount = bookingAmount;
    this.delivery = Delivery.derivedFrom(this.routeSpecification,
            this.itinerary, LastCargoHandledEvent.EMPTY);
}

Listing 5-12.Command handler for the BookCargo command

清单 5-13 显示了assigno route()命令处理程序的代码片段。它接受RouteCargoCommand类作为输入,并设置聚合的状态:

/**
 * Command Handler for the Route Cargo Command. Sets the state of the Aggregate and registers the
 * Cargo routed event
 * @param routeCargoCommand
 */
public void assignToRoute(RouteCargoCommand routeCargoCommand) {
    this.itinerary = routeCargoCommand.getCargoItinerary();
    // Handling consistency within the Cargo aggregate synchronously
    this.delivery = delivery.updateOnRouting(this.routeSpecification,
            this.itinerary);

}

Listing 5-13.Command handler for the route assignment command

总之, 命令处理程序 在管理有界上下文内的聚合状态中扮演着非常重要的角色。命令处理程序的实际调用是通过应用服务进行的,我们将在下面的章节中看到。

图 5-23 展示了我们的命令处理程序实现的类图。

img/473795_1_En_5_Fig23_HTML.jpg

图 5-23

命令处理程序实现的类图

这就完成了域模型中命令的实现。我们现在来看看如何实现查询。

问题

有界上下文中的查询负责 向外部消费者提供有界上下文的集合 的状态。

为了实现查询,我们利用 JPA 命名查询、 即可以在集合上定义的查询来以各种形式检索状态。清单 5-14 展示了来自货物集合的代码片段,该代码片段定义了需要可用的查询。在这种情况下,我们有三个查询—查找所有货物,通过货物的预订标识符查找货物,以及所有货物的最终预订标识符:

@NamedQueries({
        @NamedQuery(name = "Cargo.findAll",
                query = "Select c from Cargo c"),
        @NamedQuery(name = "Cargo.findByBookingId",
                query = "Select c from Cargo c where c.bookingId = :bookingId"),
        @NamedQuery(name = "Cargo.findAllBookingIds",
                query = "Select c.bookingId from Cargo c") })
public class Cargo{}

Listing 5-14.Named queries within the Cargo root aggregate

总之,查询处理程序扮演着在有限的上下文中呈现 聚合状态的角色。这些查询的实际调用和执行是通过应用服务和存储库类进行的,我们将在接下来的章节中看到。

这就完成了域模型中查询的实现。我们现在将看到如何实现事件。

域事件

有界上下文中的事件是 将有界上下文的聚集状态变化发布为事件 的任何操作。由于命令会改变聚合的状态,因此可以有把握地假设在有界上下文中的任何命令操作都会导致相应的事件。

域事件在微服务架构中扮演着核心角色,以健壮的方式实现它们至关重要。微服务架构的分布式本质要求通过 编排机制使用事件,在基于微服务的应用的各种有界上下文之间保持状态和事务一致性

图 5-24 举例说明了在货物跟踪应用的各种有界上下文之间流动的事件。

img/473795_1_En_5_Fig24_HTML.jpg

图 5-24

微服务架构中的事件流

让我们用一个商业案例来解释这一点。

当货物被分配路线时,这意味着货物现在可以被跟踪,这需要向货物发出跟踪标识符。货物路线的分配是在预订有界环境中处理的,而跟踪标识符的发布是在跟踪有界环境中处理的。在整体式工作方式中,为货物分配路线和发布跟踪标识符的过程同时发生,因为 由于流程、运行时和数据存储 的共享模型,我们可以跨多个有界上下文维护相同的事务上下文。

然而,在微服务架构中,这是不可能实现的,因为它是 无共享架构 。当货物被分配一条路线时,Booking Bounded 上下文只负责确保货物集合的状态反映新的路线。跟踪有界上下文需要知道这种状态的变化,以便它能够相应地发布跟踪标识符,从而 完成业务用例 。这就是 域事件和 事件编排发挥重要作用的地方。如果货物绑定上下文可以引发货物集合已经被分配了路线的事件,则跟踪绑定上下文可以订阅该特定事件并发布跟踪标识符来完成该业务用例。 引发事件将事件 交付给各种有界上下文以完成一个业务用例的机制是 事件编排模式。

实现健壮的事件驱动的编排架构有四个阶段:

  • 注册 需要从有界上下文中提出的域事件。

  • 从有界上下文中提出 需要发布的领域事件。

  • 发布 从有界上下文中引发的事件。

  • 订阅 其他有界上下文中已经发布的事件。

考虑到这种体系结构的复杂性,实施分为多个方面:

  • 域事件的注册由聚合实现

  • 事件的引发/发布由出站服务实现

  • 订阅事件由接口/入站服务处理

由于我们正处于实现领域模型的阶段,所以我们将在这一节中涉及的唯一领域是聚合事件的注册。本章的后续部分将处理其他每个方面(出站服务将涵盖事件的引发/发布的实现,入站服务将涵盖事件订阅的实现)。

事件的登记

为了帮助实现这一点,我们将利用 Spring Data 提供的模板类“AbstractAggregateRoot”。这个模板类提供了注册发生的事件的能力。

让我们举一个例子来演示一下实现过程。清单 5-15 显示了扩展 AbstractAggregateRoot 模板类的 Cargo Aggregate 类:

package com.practicalddd.cargotracker.bookingms.domain.model.aggregates;
import javax.persistence.*;
import org.springframework.data.domain.AbstractAggregateRoot;
@Entity
public class Cargo extends AbstractAggregateRoot<Cargo> {
}

Listing 5-15.AbstractAggregateRoot template class

下一步是当聚合的 状态改变 时, 实现注册的聚合事件 。正如我们之前所述和看到的 ,聚集上的命令操作改变状态 并且是 最有可能注册聚集事件 的地方。在 Cargo Aggregate 中,我们有两个命令操作:第一个是在预订新货物时,第二个是在货物发送时。聚合状态更改被放置在聚合的命令处理程序内,货物预订被放置在货物聚合的 构造器方法内, 和货物路由被放置在货物聚合的assignor route 方法内。 我们将使用 AbstractAggregateRoot 模板类提供的***registerEvent()***方法在这两个方法中实现聚合事件的注册和引发。

清单 5-16 展示了货物集合的 命令处理方法 中集合事件注册的实现。我们在聚合“ addDomainEvent() ”中添加了一个新方法,它是对“registerEvent()”的封装。它将一个 通用事件对象作为输入参数,该对象是需要注册的事件。 在构造函数和 assignToRoute()方法中,我们调用 addDomainEvent()方法,并注册相应的事件,即cargobookdeventCargoRoutedEvent:

package com.practicalddd.cargotracker.bookingms.domain.model.aggregates;
import javax.persistence.*;
import org.springframework.data.domain.AbstractAggregateRoot;
@Entity
public class Cargo extends AbstractAggregateRoot<Cargo> {
/**
 * Constructor - Used

for a new Cargo booking. Registers the Cargo Booked Event
 * @param bookingId - Booking Identifier for the new Cargo
 * @param routeSpecification - Route Specification for the new Cargo
 */
     /**
     * Constructor Command Handler for a new Cargo booking. Sets the state
     * of the Aggregate and registers the Cargo Booked Event
     *
     */
    public Cargo(BookCargoCommand bookCargoCommand){
        this.bookingId = new BookingId(bookCargoCommand.getBookingId());
        this.routeSpecification = new RouteSpecification(
                    new Location(bookCargoCommand.getOriginLocation()),
                    new Location(bookCargoCommand.getDestLocation()),
                    bookCargoCommand.getDestArrivalDeadline()
            );
        this.origin = routeSpecification.getOrigin();
        this.itinerary = CargoItinerary.EMPTY_ITINERARY; //Empty Itinerary since the Cargo has not been routed yet
        this.bookingAmount = bookingAmount;
        this.delivery = Delivery.derivedFrom(this.routeSpecification,
                this.itinerary, LastCargoHandledEvent.EMPTY);

        //Add this domain event which needs to be fired when the new cargo is saved
        addDomainEvent(new
                CargoBookedEvent(
                        new CargoBookedEventData(bookingId.getBookingId())));
    }
    /**
     * Assigns route to the Cargo. Registers the Cargo Routed Event
     * @param itinerary
     */
        /**
     * Command Handler

for the Route Cargo Command. Sets the state of the
     * Aggregate and registers the Cargo routed event
     * @param routeCargoCommand
     */

    public void assignToRoute(RouteCargoCommand routeCargoCommand) {
        this.itinerary = routeCargoCommand.getCargoItinerary();
        // Handling consistency within the Cargo aggregate synchronously
        this.delivery = delivery.updateOnRouting(this.routeSpecification,
                this.itinerary);

        //Add this domain event which needs to be fired when the new cargo is saved
        addDomainEvent(new
                CargoRoutedEvent(
                new CargoRoutedEventData(bookingId.getBookingId())));
    }
/**
     * Method to register the event
     * @param event
     */
    public void addDomainEvent(Object event){
        registerEvent(event);
    }
}

Listing 5-16.Event registration within the Cargo root aggregate

清单 5-17 展示了cargobokedevent类的实现。封装事件数据的常规 POJO,即CargoBookedEventData:

/**
 * Event Class

for the Cargo Booked Event. Wraps up the Cargo Booked Event Data
 */
public class CargoBookedEvent {
    CargoBookedEventData cargoBookedEventData;
    public CargoBookedEvent(CargoBookedEventData cargoBookedEventData){
        this.cargoBookedEventData = cargoBookedEventData;
    }
    public CargoBookedEventData getCargoBookedEventData(){
        return cargoBookedEventData;
    }
}

Listing 5-17.CargoBookedEvent implementation class

清单 5-18 显示了 CargoBookedEventData 类的实现。这也是一个常规的 POJO,包含事件数据,在本例中只是预订 Id:

/**
 * Event Data for the Cargo Booked Event
 */
public class CargoBookedEventData {
    private String bookingId;
    public CargoBookedEventData(String bookingId){
        this.bookingId = bookingId;
    }
    public String getBookingId(){return this.bookingId;}
}

Listing 5-18.CargoBookedEventData implementation class

CargoRoutedEventCargoRoutedEvent data的实现遵循与前面相同的方法。

图 5-25 展示了我们实现的类图。

img/473795_1_En_5_Fig25_HTML.jpg

图 5-25

聚合事件注册实现的类图

概括来说,聚合一个命令处理后的寄存器域事件。这些事件的注册总是在集合的命令处理程序方法中实现。

这就完成了域模型的实现。我们现在将继续为域模型实现域模型服务。

领域模型服务

使用域模型服务有两个主要原因。第一种是通过 明确定义的接口使有界上下文的状态 对外部方 可用。 二是 与外部方 交互,将有界上下文的状态持久化到 数据存储库 (数据库),将有界上下文的状态改变事件发布到外部 消息代理、与其他有界上下文通信。

对于任何有界的上下文,有三种类型的域模型服务:

  • 入站服务 其中我们实现了定义良好的接口,使外部各方能够与域模型进行交互

  • 出站服务 在这里,我们实现了与外部存储库/其他有界上下文的所有交互

  • 应用服务 ,它充当域模型与入站和出站服务之间的外观层

图 5-26 展示了域模型服务的实现。

img/473795_1_En_5_Fig26_HTML.jpg

图 5-26

域模型服务实现摘要

入站服务

入站服务(或六角形架构模式中表示的入站适配器)充当我们的核心域模型的最外层网关。如上所述,它涉及到定义良好的接口的实现,这些接口使外部消费者能够与核心域模型进行交互。

入站服务的类型 取决于我们需要向 公开的操作类型 启用域模型的外部消费者。

考虑到我们正在为货物跟踪应用实现微服务架构模式,我们提供两种类型的入站服务:

  • 一个基于 REST 的 API 层,供外部消费者调用有界上下文上的操作( 命令/查询 )

  • 基于 Spring Cloud Stream 的事件处理层,消费来自消息代理的事件并进行处理

应用接口

REST API 的职责是代表有界上下文接收来自外部消费者的 HTTP 请求。这个请求可能是命令或查询。REST API 层的职责是将其翻译成由有界上下文的域模型识别的命令/查询模型,并将其委托给应用服务层进行进一步处理。

回头看图 4-5 ,其中详细列出了各种受限上下文 的所有操作(例如,预订货物、为货物分配路线、处理货物、跟踪货物) ,所有这些操作都将有相应的 REST APIs 来接受并处理这些请求。

REST API 在 Spring Boot 的实现是通过利用 Spring Web MVC 项目提供的 REST 功能来实现的。我们添加到项目中的spring-boot-starter-web依赖项提供了构建 API 所需的功能。

让我们看一个使用 Spring Web 构建的 REST API 的例子。清单 5-19 描述了 CargoBookingController 类 ,它为我们的 Cargo Booking 命令 提供了一个 REST API:

  • REST API 可在网址“/ cargobooking ”获得。

  • 它有一个 POST 方法,接受 BookCargoResource,这是 API 的输入有效负载。这个标注了“ @RequestBody ”。

  • 它依赖于 CargoBookingCommandService,后者是一个应用服务,充当一个外观(参见下面的实现)。利用基于构造函数的依赖注入将这种依赖注入到 API 类中。

  • 它使用汇编器实用程序类(BookCargoCommandDTOAssembler)将资源数据(BookCargoResource)转换为命令模型( BookCargoCommand )。

  • 转换后,它将流程委托给 CargoBookingCommandService 进行进一步处理。

  • 它向外部消费者返回一个响应,其中包含新预订货物的预订标识符。

package com.practicalddd.cargotracker.bookingms.interfaces.rest;
import com.practicalddd.cargotracker.bookingms.application.internal.commandservices.CargoBookingCommandService;
import com.practicalddd.cargotracker.bookingms.domain.model.aggregates.BookingId;
import com.practicalddd.cargotracker.bookingms.interfaces.rest.dto.BookCargoResource;
import com.practicalddd.cargotracker.bookingms.interfaces.rest.transform.BookCargoCommandDTOAssembler;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

@Controller    // This means that this class is a Controller
@RequestMapping("/cargobooking") // The URI of the API
public class CargoBookingController {
    private CargoBookingCommandService cargoBookingCommandService; // Application Service Dependency
    /**
     * Provide the dependencies
     * @param cargoBookingCommandService
     */
    public CargoBookingController(CargoBookingCommandService cargoBookingCommandService){
        this.cargoBookingCommandService = cargoBookingCommandService;
    }

    /**
     * POST method to book a cargo
     * @param bookCargoResource
     */
    @PostMapping
    @ResponseBody
    public BookingId bookCargo(@RequestBody  BookCargoResource bookCargoResource){
        BookingId bookingId  = cargoBookingCommandService.bookCargo(
                BookCargoCommandDTOAssembler.toCommandFromDTO(bookCargoResource));
        return bookingId;
    }
}

Listing 5-19.CargoBookingController implementation class

清单 5-20 展示了book cargo resource类的实现:

package com.practicalddd.cargotracker.bookingms.interfaces.rest.dto;

import java.time.LocalDate; 

/**
 * Resource class for the Book Cargo Command API
 */
public class BookCargoResource {

    private int bookingAmount;
    private String originLocation;
    private String destLocation;
    private LocalDate destArrivalDeadline;

    public BookCargoResource(){}

    public BookCargoResource(int bookingAmount,
                             String originLocation, String destLocation, LocalDate destArrivalDeadline){

        this.bookingAmount = bookingAmount;
        this.originLocation = originLocation;
        this.destLocation = destLocation;
        this.destArrivalDeadline = destArrivalDeadline;
    }

    public void setBookingAmount(int bookingAmount){
        this.bookingAmount = bookingAmount;
    }

    public int getBookingAmount(){
        return this.bookingAmount;
    }

    public String getOriginLocation() {return originLocation; }

    public void setOriginLocation(String originLocation) {this.originLocation = originLocation; }

    public String getDestLocation() { return destLocation; }

    public void setDestLocation(String destLocation) { this.destLocation = destLocation; }

    public LocalDate getDestArrivalDeadline() { return destArrivalDeadline; }

    public void setDestArrivalDeadline(LocalDate destArrivalDeadline) { this.destArrivalDeadline = destArrivalDeadline; }

}

Listing 5-20.CargoBookingResource implementation class

清单 5-21 展示了BookCargoCommandDTOAssembler类的实现:

package com.practicalddd.cargotracker.bookingms.interfaces.rest.transform;

import com.practicalddd.cargotracker.bookingms.domain.model.commands.BookCargoCommand;
import com.practicalddd.cargotracker.bookingms.interfaces.rest.dto.BookCargoResource;

/**
 * Assembler class to convert the Book Cargo Resource Data to the Book Cargo Model
 */
public class BookCargoCommandDTOAssembler {

    /**
     * Static method within the Assembler class
     * @param bookCargoResource
     * @return BookCargoCommand Model
     */
    public static BookCargoCommand toCommandFromDTO(BookCargoResource bookCargoResource){

        return new BookCargoCommand(
                                    bookCargoResource.getBookingAmount(),
                                    bookCargoResource.getOriginLocation(),
                                    bookCargoResource.getDestLocation(),

                                    java.sql.Date.valueOf(bookCargoResource.getDestArrivalDeadline()));
    }
}

Listing 5-21.DTOAssembler implementation class

清单 5-22 展示了 BookCargoCommand 类的实现:

package com.practicalddd.cargotracker.bookingms.domain.model.commands;

import java.util.Date;

/**
 * Book Cargo Command class
 */
public class BookCargoCommand {

    private int bookingAmount;
    private String originLocation;
    private String destLocation;
    private Date destArrivalDeadline;

    public BookCargoCommand(){}

    public BookCargoCommand(int bookingAmount,
                            String originLocation, String destLocation, Date destArrivalDeadline){

        this.bookingAmount = bookingAmount;
        this.originLocation = originLocation;
        this.destLocation = destLocation;
        this.destArrivalDeadline = destArrivalDeadline;
    }

    public void setBookingAmount(int bookingAmount){
        this.bookingAmount = bookingAmount;
    }

    public int getBookingAmount(){
        return this.bookingAmount; 

    }

    public String getOriginLocation() {return originLocation; }

    public void setOriginLocation(String originLocation) {this.originLocation = originLocation; }

    public String getDestLocation() { return destLocation; }

    public void setDestLocation(String destLocation) { this.destLocation = destLocation; }

    public Date getDestArrivalDeadline() { return destArrivalDeadline; }

    public void setDestArrivalDeadline(Date destArrivalDeadline) { this.destArrivalDeadline = destArrivalDeadline; }
}

Listing 5-22.BookCargoCommand implementation class

图 5-27 展示了我们实现的类图。

img/473795_1_En_5_Fig27_HTML.jpg

图 5-27

REST API 实现的类图

我们所有的入站 REST API 实现都遵循相同的方法,如图 5-28 所示。

img/473795_1_En_5_Fig28_HTML.jpg

图 5-28

入站服务实施流程摘要

  1. 对命令/查询的入站请求到达 REST API。API 类是使用 Spring Web MVC 项目实现的,当我们将Spring-boot-starter-Web依赖项添加到项目中时,这个项目就会被配置。

  2. REST API 类使用实用汇编器组件将资源数据格式转换为域模型所需的命令/查询数据格式。

  3. 命令/查询数据被发送到应用服务以供进一步处理。

事件处理程序

我们的有界上下文中存在的另一种类型的接口是事件处理程序。在有界上下文中,事件处理程序负责处理有界上下文感兴趣的事件。这些事件由应用中的其他有界上下文引发。这些“ 事件处理程序 ”是在订阅有界上下文中创建的,该上下文驻留在 入站/接口 层中。事件处理程序接收事件以及事件有效负载数据,并将它们作为常规命令操作进行处理。

事件处理程序的实现将利用 Spring Cloud Stream 提供的功能来完成。我们的消息代理将是 RabbitMQ,所以我们的实现将假设我们已经有了一个 RabbitMQ 实例并正在运行。我们不需要在 RabbitMQ 中创建任何特定的交换、目的地或队列。

我们将以跟踪有界上下文感兴趣的“ CargoRouted ”事件为例,该事件是预订有界上下文在处理 Route Cargo 命令后发布的:

  1. 第一步是 实现处理程序类 。handler 类被实现为一个常规的服务类,带有 "@Service "原型注释。我们使用“ @EnableBinding ”注释将服务类绑定到消息代理的通道连接。最后,我们在处理程序类中用带有目标详细信息的***“@ StreamListener***”注释标记事件处理程序方法。批注标记了接收发布到处理程序感兴趣的目标上的事件流的方法。

    清单 5-23 展示了 CargoRoutedEventHandler 类的实现:

  2. 我们还需要实现代理配置,比如 代理连接细节和代理目标/目的地映射 。清单 5-24 展示了需要在 Spring Boot 应用的 application.properties 文件中实现的配置。代理配置的属性具有 RabbitMQ 在我们第一次安装它时设置的默认值:

package com.practicalddd.cargotracker.trackingms.interfaces.events;

import com.practicalddd.cargotracker.shareddomain.events.CargoRoutedEvent;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.stereotype.Service;

/**
 * Event Handler

for the Cargo Routed Event that the Tracking Bounded Context is interested in
 */
@Service
@EnableBinding(Sink.class) //Bind to the channel connection for the message broker
public class CargoRoutedEventHandler {

    @StreamListener(target = Sink.INPUT) //Listen to the stream of messages on the destination
    public void receiveEvent(CargoRoutedEvent cargoRoutedEvent) {
        //Process the Event
    }
}

Listing 5-23.CargoRoutedEvent handler implementation class

spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.cloud.stream.bindings.input.destination=cargoRoutings
spring.cloud.stream.bindings.input.group=cargoRoutingsQueue

Listing 5-24.RabbitMQ configuration properties

目的地 配置有相同的值 ,该值在我们在 Booking Bounded 上下文中发布“CargoRouted”事件时使用(参见出站服务一节)。

图 5-29 展示了我们实现的类图。

img/473795_1_En_5_Fig29_HTML.jpg

图 5-29

我们的事件处理程序实现的类图

我们所有的事件处理器实现都遵循图 5-30 所示的相同方法。

img/473795_1_En_5_Fig30_HTML.jpg

图 5-30

事件处理程序实现的实现过程摘要

  1. 事件处理程序从消息代理接收入站事件。

  2. 事件处理程序使用实用汇编器组件将资源数据格式转换为域模型所需的命令数据格式。

  3. 命令数据被发送到应用服务以供进一步处理。

应用服务程序

应用服务在有界上下文中充当入站/出站服务和核心域模型之间的门面或端口。

在一个有界的上下文中,应用服务负责 接收来自入站服务的请求,并将它们委托给相应的服务, 即命令委托给 命令服务 ,查询委托给 查询服务。 作为 命令委托过程 的一部分,应用服务负责将聚合状态保存在底层数据存储中。作为查询委托过程的一部分,应用服务负责从底层数据存储中检索聚合状态。

作为这些职责的一部分,应用服务依靠 出站服务 来完成这些任务。出站服务提供连接到物理数据存储所需的必要基础架构组件。我们将分别深入探讨出站服务的实现( 参见出站服务 一节)。

图 5-31 说明了应用服务的职责。

img/473795_1_En_5_Fig31_HTML.jpg

图 5-31

应用服务的责任

应用服务:命令/查询委托

作为该职责的一部分,有界上下文中的应用服务接收处理命令/查询的请求。这些请求通常来自入站服务(API 层)。作为处理的一部分,应用服务首先利用域模型的 命令处理程序/查询处理程序 (参见域模型部分)来设置状态或查询状态。然后,它们利用出站服务来保存状态或执行对聚合状态的查询。

让我们先来看一个命令委托者应用服务类的例子,即 货物预订命令应用服务类 。这个类有两个例程——"book Cargo()"和"assignRouteToCargo()",分别处理 货物预订命令路线货物命令:

  • Application services 类被实现为一个常规的 Spring 托管 Bean,带有一个“***@ Service”***标记注释,表明它是一个服务类。

  • 通过 Spring 的构造函数依赖注入功能,为应用服务类提供了必要的依赖。在这种情况下,CargoBookingCommandApplicationService 类依赖于一个**(cargo repository)。**

*** 在这两个例程中,应用服务依赖于货物集合 ***(构造函数,assignor route)***上定义的命令处理程序来设置其状态。

*   应用服务利用 CargoRepository outbound 服务来存储任一操作中货物的状态。** 

**清单 5-25 演示了 货物订舱命令应用服务类 的实现:

package com.practicalddd.cargotracker.bookingms.application.internal.commandservices;

import com.practicalddd.cargotracker.bookingms.application.internal.outboundservices.acl.ExternalCargoRoutingService;
import com.practicalddd.cargotracker.bookingms.domain.model.aggregates.BookingId;
import com.practicalddd.cargotracker.bookingms.domain.model.aggregates.Cargo;
import com.practicalddd.cargotracker.bookingms.domain.model.commands.BookCargoCommand;
import com.practicalddd.cargotracker.bookingms.domain.model.commands.RouteCargoCommand;
import com.practicalddd.cargotracker.bookingms.domain.model.entities.Location;
import com.practicalddd.cargotracker.bookingms.domain.model.valueobjects.CargoItinerary;
import com.practicalddd.cargotracker.bookingms.domain.model.valueobjects.RouteSpecification;
import com.practicalddd.cargotracker.bookingms.infrastructure.repositories.CargoRepository;
import org.springframework.stereotype.Service;

import java.util.UUID;
/**
 * Application Service class for the Cargo Booking Commands
 */

@Service
public class CargoBookingCommandService {

    private CargoRepository cargoRepository;
    private ExternalCargoRoutingService externalCargoRoutingService;

    public CargoBookingCommandService(CargoRepository cargoRepository){

        this.cargoRepository = cargoRepository;
        this.externalCargoRoutingService = externalCargoRoutingService;
    }

    /**
     * Service Command method to book a new Cargo
     * @return BookingId of the Cargo
     */

    public BookingId bookCargo(BookCargoCommand bookCargoCommand){

        String random = UUID.randomUUID().toString().toUpperCase();
        bookCargoCommand.setBookingId(random);
        Cargo cargo = new Cargo(bookCargoCommand);
        cargoRepository.save(cargo);
        return new BookingId(random);
    }

    /**
     * Service Command method to assign a route to a Cargo
     * @param routeCargoCommand
     */

    public void assignRouteToCargo(RouteCargoCommand routeCargoCommand){ 

        Cargo cargo = cargoRepository.findByBookingId(routeCargoCommand.getCargoBookingId());
        CargoItinerary cargoItinerary = externalCargoRoutingService.fetchRouteForSpecification(new RouteSpecification(
                new Location(routeCargoCommand.getOriginLocation()),
                new Location(routeCargoCommand.getDestinationLocation()),
                routeCargoCommand.getArrivalDeadline()
        ));
        routeCargoCommand.setCargoItinerary(cargoItinerary);
        cargo.assignToRoute(routeCargoCommand);
        cargoRepository.save(cargo);

    }

}

Listing 5-25.CargoBookingCommand Application services class implementation

清单 5-26 展示了 货物预订查询应用服务类 的实现,它服务于与预订相关的所有查询:

package com.practicalddd.cargotracker.bookingms.application.internal.queryservices;

import com.practicalddd.cargotracker.bookingms.domain.model.aggregates.BookingId;
import com.practicalddd.cargotracker.bookingms.domain.model.aggregates.Cargo;
import com.practicalddd.cargotracker.bookingms.infrastructure.repositories.CargoRepository;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * Application Service which caters to all queries related to the Booking Bounded Context
 */
@Service
public class CargoBookingQueryService {

    private CargoRepository cargoRepository; // Inject Dependencies

    /**
     * Find all Cargos
     * @return List<Cargo>
     */

    public List<Cargo> findAll(){
        return cargoRepository.findAll();
    }

    /**
     * List All Booking Identifiers
     * @return List<BookingId>
     */
   public List<BookingId> getAllBookingIds(){

       return cargoRepository.findAllBookingIds();
   }

    /**
     * Find a specific Cargo based on its Booking Id
     * @param bookingId
     * @return Cargo
     */
    public Cargo find(String bookingId){
        return cargoRepository.findByBookingId(bookingId);
    }
}

Listing 5-26.CargoBookingQuery Application services implementation

图 5-32 说明了我们实现的类图。

img/473795_1_En_5_Fig32_HTML.jpg

图 5-32

我们的应用服务命令/查询委托的类图

我们所有负责命令/查询委托的应用服务实现都遵循相同的方法,如图 5-33 所示。

img/473795_1_En_5_Fig33_HTML.jpg

图 5-33

应用服务实施流程摘要

  1. 对命令/查询操作的请求通常来自入站服务层,到达有界上下文的应用服务。应用服务类 被实现为 Spring Managed bean,带有 @Service 标记注释,它们所有的 依赖项都通过构造函数注入。

  2. 应用服务依靠领域模型中定义的 命令处理程序/查询处理程序 来设置/查询聚合状态。

  3. 应用服务利用 出站服务 (例如,存储库)来保持集合的状态或在集合上执行查询。

出站服务

正如我们在前面的应用服务实现中看到的,在处理命令/查询期间,应用服务可能需要与 外部服务 进行通信,如下所示:

  • 储存库 用于存储/检索有界上下文的状态

  • 消息经纪人 对有界上下文的状态变化进行交流

  • 其他有界语境

应用服务依靠 出站服务 来帮助进行这种通信。

出站服务提供与 这些外部服务 交互的能力。 外部服务可以是数据存储库 ,我们在其中存储有界上下文的聚合状态,它可以是 消息代理,我们在其中发布聚合状态, 或者它可以是与另一个有界上下文的 交互。

图 5-34 说明了出站服务的职责。作为操作(命令、查询、事件)的一部分,它们接收与外部服务通信的请求。它们使用基于外部服务类型的 API(持久性 API、REST APIs、代理 API)与它们进行交互。

img/473795_1_En_5_Fig34_HTML.jpg

图 5-34

出站服务

让我们看看这些出站服务类型的实现。

出站服务:存储库类

数据库访问的出站服务实现为存储库 "类。 存储库类是围绕一个特定的集合构建的,处理该集合的所有数据库操作,包括:

*** 新聚集及其关联的持久性

  • 更新聚合及其关联

  • 查询聚合及其关联

Spring Data JPA 帮助我们轻松实现 JPA 存储库类。让我们看一个仓库类的例子, 货物仓库类, ,它处理与 货物集合: 相关的所有数据库操作

  • 货物存储库被实现为扩展 JpaRepository 接口的接口。

  • Spring Data JPA 自动实现货物集合所需的默认 CRUD 操作。

  • 我们只是添加任何类型的定制查询所需的方法,这些方法被映射到在货物集合中定义的相应的命名查询。

清单 5-27 演示了 Cargo Repository 类的实现:

package com.practicalddd.cargotracker.bookingms.infrastructure.repositories;

import com.practicalddd.cargotracker.bookingms.domain.model.aggregates.BookingId;
import com.practicalddd.cargotracker.bookingms.domain.model.aggregates.Cargo;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;
/**
 * Repository class for the Cargo Aggregate
 */
public interface CargoRepository extends JpaRepository<Cargo, Long> {

     Cargo findByBookingId(String BookingId);

     List<BookingId> findAllBookingIds();

     List<Cargo> findAll();

}

Listing 5-27.CargoRepository JPA interface

图 5-35 说明了我们实现的类图。

img/473795_1_En_5_Fig35_HTML.jpg

图 5-35

出站服务–存储库实施

我们所有的存储库实现都遵循相同的方法。

出站服务:Rest API

使用 REST API 作为微服务之间的通信模式是一个很常见的需求。虽然我们已经将事件编排视为实现这一点的一种机制,但有时有界上下文之间的直接调用也可能是一种需求。

让我们通过一个例子来说明这一点。作为货物预订流程的一部分,我们需要根据路线规范为货物分配一个行程。生成最佳路线所需的数据作为维护船只运动、路线和时间表的路线约束上下文的一部分来维护。这需要预订受限上下文的预订服务向路由受限上下文的路由服务发出一个出站调用,路由服务提供一个 REST API 来根据货物的路线规范检索所有可能的路线。

如图 5-36 所示。

img/473795_1_En_5_Fig36_HTML.jpg

图 5-36

两个有界上下文之间的 HTTP 调用

然而,这确实对领域模型提出了挑战。预订有界上下文的货物集合将路线表示为“ ”对象,而路由有界上下文将路线表示为“ ”对象。因此,两个有界上下文之间的调用将需要在它们的域模型之间进行转换。

这种转换通常在反讹误层中完成,反讹误层充当两个有界上下文之间通信的桥梁。

如图 5-37 所示。

img/473795_1_En_5_Fig37_HTML.jpg

图 5-37

两个有界上下文之间的反腐败层

预订绑定上下文依赖于 Spring Web 提供的 Rest 模板功能来调用路由服务的 REST API。

让我们通过完整的实现来更好地理解这个概念:

  • 第一步是实现路由服务 REST API。这是通过使用标准的 Spring Web 功能完成的,我们在前面的章节中已经实现了这些功能。清单 5-28 展示了路由服务 REST API 实现:
package com.practicalddd.cargotracker.routingms.interfaces.rest;

import com.practicalddd.cargotracker.TransitPath;
import com.practicalddd.cargotracker.routingms.application.internal.CargoRoutingService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

@Controller    // This means that this class is a Controller
@RequestMapping("/cargorouting")
public class CargoRoutingController {

    private CargoRoutingService cargoRoutingService; // Application Service Dependency

    /**
     * Provide the dependencies
     * @param cargoRoutingService
     */
    public CargoRoutingController(CargoRoutingService cargoRoutingService){
        this.cargoRoutingService = cargoRoutingService;
    }

    /**

     *
     * @param originUnLocode
     * @param destinationUnLocode
     * @param deadline
     * @return TransitPath - The optimal route for a Route Specification
     */

    @GetMapping(path = "/optimalRoute")
    @ResponseBody
    public TransitPath findOptimalRoute(
             @PathVariable("origin") String originUnLocode,
             @PathVariable("destination") String destinationUnLocode,
             @PathVariable("deadline") String deadline) {

        TransitPath transitPath = cargoRoutingService.findOptimalRoute(originUnLocode,destinationUnLocode,deadline);

        return transitPath;

    }
}

Listing 5-28.CargoRoutingController implementation class

路由服务实现在“/ optimalRoute ”提供了一个 REST API。它接受一组规范——起始位置、目的位置和截止时间。然后,它使用货物路由应用服务类根据这些规范计算最佳路线。路由有界上下文中的域模型根据 运输路径(类似于路线)运输边(类似于路线)来表示最优路由。

清单 5-29 展示了传输路径域模型类的实现:

import java.util.ArrayList; 

import java.util.List;
/**
 * Domain Model representation of the Transit Path
 */
public class TransitPath {
    private List<TransitEdge> transitEdges;
    public TransitPath() {
        this.transitEdges = new ArrayList<>();
    }
    public TransitPath(List<TransitEdge> transitEdges) {
        this.transitEdges = transitEdges;
    }
    public List<TransitEdge> getTransitEdges() {
        return transitEdges;
    }
    public void setTransitEdges(List<TransitEdge> transitEdges) {
        this.transitEdges = transitEdges;
    }
    @Override
    public String toString() {
        return "TransitPath{" + "transitEdges=" + transitEdges + '}';
    }
}

Listing 5-29.TransitPath Domain model class implementation

清单 5-30 演示了 Transit Edge 域模型类的实现:

package com.practicalddd.cargotracker;

import java.io.Serializable;
import java.util.Date;

/**
 * Represents an edge in a path through a graph, describing the route of a
 * cargo.
 */
public class TransitEdge implements Serializable {

    private String voyageNumber;
    private String fromUnLocode; 

    private String toUnLocode;
    private Date fromDate;
    private Date toDate;

    public TransitEdge() {    }

    public TransitEdge(String voyageNumber, String fromUnLocode,
            String toUnLocode, Date fromDate, Date toDate) {
        this.voyageNumber = voyageNumber;
        this.fromUnLocode = fromUnLocode;
        this.toUnLocode = toUnLocode;
        this.fromDate = fromDate;
        this.toDate = toDate;
    }

    public String getVoyageNumber() {
        return voyageNumber;
    }

    public void setVoyageNumber(String voyageNumber) {
        this.voyageNumber = voyageNumber;
    }

    public String getFromUnLocode() {
        return fromUnLocode;
    }

    public void setFromUnLocode(String fromUnLocode) {
        this.fromUnLocode = fromUnLocode;
    }

    public String getToUnLocode() {
        return toUnLocode;
    }

    public void setToUnLocode(String toUnLocode) {
        this.toUnLocode = toUnLocode; 

    }

    public Date getFromDate() {
        return fromDate;
    }

    public void setFromDate(Date fromDate) {
        this.fromDate = fromDate;
    }

    public Date getToDate() {
        return toDate;
    }

    public void setToDate(Date toDate) {
        this.toDate = toDate;
    }

    @Override
    public String toString() {
        return "TransitEdge{" + "voyageNumber=" + voyageNumber
                + ", fromUnLocode=" + fromUnLocode + ", toUnLocode="
                + toUnLocode + ", fromDate=" + fromDate
                + ", toDate=" + toDate + '}';
    }
}

Listing 5-30.TransitEdge Domain model class implementation

图 5-38 展示了实现的类图。

img/473795_1_En_5_Fig38_HTML.jpg

图 5-38

REST API 的类图

  • 下一步是为我们的 Routing Rest 服务实现客户端实现。客户端是CargoBookingCommandService类,负责处理“ 给货物分配路线 ”命令。作为命令处理的一部分,这个服务类将需要调用路由服务 REST API 来获得基于货物路线规范的最佳路线。

    CargoBookingCommandService 使用一个出站服务类–ExternalCargoRoutingService–来调用路由服务 REST API。ExternalCargoRoutingService类还将路由服务的 REST API 提供的数据转换成预订绑定上下文的域模型可识别的格式。

    清单 5-31 演示了CargoBookingCommandService 中的方法“assignRouteToCargo”。 该服务类被注入了ExternalCargoRoutingService依赖项,该依赖项处理调用路由服务的 REST API 的请求,并返回cargo interary对象,该对象随后被分配给 cargo:

@ApplicationScoped
public class CargoBookingCommandService {

    @Inject
    private ExternalCargoRoutingService externalCargoRoutingService;

    /**
     * Service Command method

to assign a route to a Cargo
     * @param routeCargoCommand
     */
    @Transactional
    public void assignRouteToCargo(RouteCargoCommand routeCargoCommand){

        Cargo cargo = cargoRepository.find(new BookingId(routeCargoCommand.getCargoBookingId()));
        CargoItinerary cargoItinerary = externalCargoRoutingService.fetchRouteForSpecification(new RouteSpecification(
                new Location(routeCargoCommand.getOriginLocation()),
                new Location(routeCargoCommand.getDestinationLocation()),
                routeCargoCommand.getArrivalDeadline()
        ));

        cargo.assignToRoute(cargoItinerary);
        cargoRepository.store(cargo);

    }

    // All other implementations of Commands for the Booking Bounded Context

}

Listing 5-31.Dependencies for outbound services

清单 5-32 演示了 ExternalCargoRoutingService 出站服务类。这个类执行两件事:

  • 它利用了 Spring Web 项目提供的 RestTemplate 类来帮助构建 Rest 客户端。

  • 它还将路由服务的 Rest API 提供的数据(transi pathtransi edge)转换为预订有界上下文的域模型(cargo internary/Leg)。

package com.practicalddd.cargotracker.bookingms.application.internal.outboundservices.acl;

import com.practicalddd.cargotracker.bookingms.domain.model.valueobjects.CargoItinerary;
import com.practicalddd.cargotracker.bookingms.domain.model.valueobjects.Leg;
import com.practicalddd.cargotracker.bookingms.domain.model.valueobjects.RouteSpecification;
import com.practicalddd.cargotracker.shareddomain.TransitEdge;
import com.practicalddd.cargotracker.shareddomain.TransitPath;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Anti Corruption Service Class
 */

@Service
public class ExternalCargoRoutingService {

    /**
     * The Booking Bounded

Context makes an external call to the Routing
     * Service of the Routing Bounded Context to fetch the Optimal
     * Itinerary for a Cargo based on the Route Specification
     * @param routeSpecification
     * @return
     */
    public CargoItinerary fetchRouteForSpecification(RouteSpecification routeSpecification){

        RestTemplate restTemplate = new RestTemplate();
        Map<String,Object> params = new HashMap<>();
        params.put("origin",routeSpecification.getOrigin().getUnLocCode());
        params.put("destination",routeSpecification.getDestination().getUnLocCode());
        params.put("arrivalDeadline",routeSpecification.getArrivalDeadline().toString());

        TransitPath transitPath = restTemplate.getForObject("<<ROUTING_SERVICE_URL>>/cargorouting/",
                    TransitPath.class,params);

        List<Leg> legs = new ArrayList<>(transitPath.getTransitEdges().size());
        for (TransitEdge edge : transitPath.getTransitEdges()) {
            legs.add(toLeg(edge));
        }

        return new CargoItinerary(legs);

    }

    /**
     * Anti-corruption

layer conversion method from the routing service's
     * domain model (TransitEdges) to the domain model recognized by the
     * Booking Bounded Context (Legs)
     * @param edge
     * @return
     */
    private Leg toLeg(TransitEdge edge) {
        return new Leg(
                edge.getVoyageNumber(),
                edge.getFromUnLocode(),
                edge.getToUnLocode(),
                edge.getFromDate(),
                edge.getToDate());
        }
}

Listing 5-32.Outbound service implementation class

图 5-39 展示了实现的类图。

img/473795_1_En_5_Fig39_HTML.jpg

图 5-39

出站服务–REST API 实现

我们所有需要与其他有界上下文通信的出站服务实现都遵循相同的方法,如图 5-40 所示。

img/473795_1_En_5_Fig40_HTML.jpg

图 5-40

出站服务(HTTP)实施流程

  1. 应用服务类接收命令/查询/事件。

  2. 作为处理的一部分,如果它需要使用 REST 与另一个有界上下文的 API 进行交互,它会使用出站服务。

  3. 出站服务使用 RestTemplate 类 创建一个 Rest 客户端来调用有界上下文的 API。它还执行从该有界上下文的 API 提供的数据格式到当前有界上下文识别的数据模型的转换。

出站服务:消息代理

出站服务的最终职责是在命令处理期间引发和发布由聚合注册的域事件。

图 5-41 展示了一个有界上下文中事件流的整个机制。

img/473795_1_En_5_Fig41_HTML.jpg

图 5-41

有界上下文中的事件流机制

让我们回顾一下事件的顺序:

  1. 应用服务接收处理特定命令的请求(例如,预订货物、安排货物路线)。

  2. 应用服务将处理委托给集合命令处理器。

  3. 命令处理程序记录需要发布的事件(例如,货物预订、货物发送)。

  4. 应用服务利用出站服务的储存库来保持聚集状态。

  5. 存储库操作触发出站服务内的事件监听器 。事件监听器 收集所有需要发布的未决注册域事件

  6. 事件监听器将域事件发布到同一个事务内的外部消息代理(即 RabbitMQ)

事件监听器的实现将利用 Spring Cloud Stream 提供的功能来完成。我们的消息代理将是 RabbitMQ,所以我们的实现将假设我们已经有了一个 RabbitMQ 实例并正在运行。我们不需要在 RabbitMQ 中创建任何特定的交换、目的地或队列。

我们将继续我们的预订有界上下文的示例,其中我们需要在“ 预订货物命令 ”和“ 路线货物命令 ”的末尾发布“ 货物预订事件 ”和“ 货物路线事件 ”:

  1. 第一步是 实现事件源 。事件源包含事件输出通道的详细信息( 逻辑连接 )。

    清单 5-33 展示了 CargoEventSource 的实现。 我们创建了两个输出消息通道( cargoBookingChannel,cargoRoutingChannel ):

  2. 下一步是 实现事件监听器。 事件源包含输出通道的细节( 逻辑连接 )为我们的事件。

    清单 5-34 展示了CargoEventPublisherService 的实现。 这是由 Cargo Aggregate 注册并发布给消息代理的所有域事件的事件监听器。

    Implementing the event listener involves the following steps:

    • 事件监听器被实现为一个常规的 Spring 托管 bean,带有原型 @ 服务 注释。清单 5-34 演示了这个实现。

    • 我们 使用 @EnableBinding 注释将 事件监听器绑定到我们在第一步中创建的事件源。

    • 对于由 Cargo Aggregate 注册的每个域事件类型,我们在侦听器中有一个相应的处理例程,例如,CargoBookedEvent 将有一个 handleCargoBooked()例程,类似地,CargoRoutedEvent 将有一个 handleCargoRouted()例程。这些例程将注册的事件作为输入参数。

    • 这些例程用@TransactionalEventListener 注释进行标记,以表明它应该是存储库操作的同一个事务的一部分。

    • 最后,在例程中,我们将注册的事件发布到消息代理的相应通道。

package com.practicalddd.cargotracker.bookingms.infrastructure.brokers.rabbitmq;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.messaging.MessageChannel;
/**
 * Interface depicting all output channels
 */
public interface CargoEventSource {
    @Output("cargoBookingChannel")
    MessageChannel cargoBooking();
    @Output("cargoRoutingChannel")
    MessageChannel cargoRouting();
}

Listing 5-33.Event source class implementation

  1. 除了代码实现,我们还需要实现代理配置,比如 代理连接细节和代理通道/交换映射 。清单 5-35 展示了需要在 Spring Boot 应用的 application.properties 文件中实现的配置。代理配置的属性具有 RabbitMQ 在我们第一次安装它时设置的默认值:
package com.practicalddd.cargotracker.bookingms.application.internal.outboundservices;

import com.practicalddd.cargotracker.bookingms.infrastructure.brokers.rabbitmq.CargoEventSource;
import com.practicalddd.cargotracker.shareddomain.events.CargoBookedEvent;
import com.practicalddd.cargotracker.shareddomain.events.CargoRoutedEvent;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.event.TransactionalEventListener;

/**
 * Transactional

Event Listener for all Cargo Aggregate Events
 */
@Service
@EnableBinding(CargoEventSource.class) //Bind to the Event Source
public class CargoEventPublisherService {

    CargoEventSource cargoEventSource;

    public CargoEventPublisherService(CargoEventSource cargoEventSource){
        this.cargoEventSource = cargoEventSource;
    }

    @TransactionalEventListener //Attach it to the transaction of the repository operation
    public void handleCargoBookedEvent(CargoBookedEvent cargoBookedEvent){
        cargoEventSource.cargoBooking().send(MessageBuilder.withPayload(cargoBookedEvent).build()); //Publish the event
    }

    @TransactionalEventListener
    public void handleCargoRoutedEvent(CargoRoutedEvent cargoRoutedEvent){
        cargoEventSource.cargoRouting().send(MessageBuilder.withPayload(cargoRoutedEvent).build());
    }
}

Listing 5-34.Event listener class implementation

spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.cloud.stream.bindings.cargoBookingChannel.destination=cargoBookings
spring.cloud.stream.bindings.cargoRoutingChannel.destination=cargoRoutings

Listing 5-35.RabbitMQ configuration details

所有需要发布域事件的出站服务都遵循前面列出的相同方法。

图 5-42 展示了我们实现的类图。

img/473795_1_En_5_Fig42_HTML.jpg

图 5-42

事件发布器实现的类图

这完成了出站服务、域模型服务和货物跟踪应用的实施,作为利用 DDD 原则和 Spring 平台的微服务应用。

实施摘要

我们现在有了一个完整的货物跟踪微服务应用的 DDD 实现,使用 Spring 平台中可用的相应项目实现了各种 DDD 工件。

实施总结如图 5-43 所示。

img/473795_1_En_5_Fig43_HTML.jpg

图 5-43

使用 Spring Boot 的 DDD 工件实施汇总

摘要

总结我们的章节

  • 我们从建立关于 Spring 平台及其提供的各种功能的细节开始。

  • 我们决定使用 Spring 平台完整产品组合中的一部分项目(Spring Boot、Spring Web、Spring Cloud Stream 和 Spring Data)来帮助构建 Cargo Tracker 作为微服务应用。

  • 我们深入研究了各种 DDD 工件的开发——首先是域模型,然后是使用所选技术的域模型服务。********