简介
本文将介绍一种实现域驱动设计(DDD)的软件架构,该架构由埃里克·埃文在《领域驱动设计:处理软件核心复杂性》(蓝皮书)中介绍。
当我们将DDD和架构联系起来时,许多人会想到微服务。但这不是DDD概念的唯一实现方式。
六边形架构也是实现DDD的一个好选择,也是Robert C. Martin在他的书《Clean Architecture》中描述的一个非常好的架构。
此外,在微服务中,我们需要保持良好的实践和良好的架构。尽管六边形架构最初是为单体应用程序设计的,但我们也可以应用它来构建我们的微服务。
先决条件
- JDK 11或更早版本
- Maven 3+
- 任何IDE或文本编辑器
什么是DDD?
来自Domain Driven Design Implementation Guide | Documentation Center | ABP.IO的DDD表示
正如上面已经解释的那样,域驱动设计是由埃里克·埃文于2003年推出的。它是一种软件工程方法,将领域放在我们的核心软件集的中心。整个解决方案必须围绕业务规则及其术语展开。
我们需要在所有股东(领域团队、技术团队)之间创建一种普适语言
。此语言中的每个术语都应在应用程序中找到。例如,如果我们正在构建一个跟踪飞机航班时间表的应用程序,则我们会在应用程序中找到实体,例如“Airplane”或“Travel”,以及值对象,例如“Destination”或“Arrival”。
有关DDD的更多信息,我建议你阅读这本免费电子书https://www.infoq.com/minibooks/domain-driven-design-quickly/
六边形架构
当你将DDD原则转化为架构图时,最合适的方法是微服务架构。因此,每个边界上下文都需要建立在边界上下文中。例如,当你构建电子商务网站或应用程序时,你可能会为帐户管理创建一个微服务,为支付功能创建另一个微服务,为库存管理创建另一个微服务。它们每个人都只有一个目的,这被转化为域边界上下文。这是微服务成为DDD实现的一个很好的选择的原因。
但这不是唯一的选择。正如马克·理查兹(Mark Richards)在他的书《软件架构基础》中所说的关于架构决策:“这总是一个权衡”。的确,微服务不是唯一可能的实现方式。对于这种类型的架构,你需要拥有管理生态系统的工具(例如Kubernetes或OpenShift等容器管理工具),以及巨大的可扩展性的需求。每个架构决策都伴随着这些缺点。对于微服务而言,可靠性和性能可能是一项挑战。
即使你选择了这种方式,每个微服务也需要拥有自己的内部架构。
那么,什么是六边形架构?
它是由Alistair Cockburn于2005年介绍的一种软件架构。它也被称为“端口和适配器架构”。其目的是在你的域代码(在单个项目或模块中)和支持代码(例如数据库访问、API调用或Java中的Spring框架)之间具有明确的分离。
这样,你可以更改任何技术细节的任何部分,而不会影响域。
DDD的目的是保持对域实现的关注,而不会太早地担心技术细节。
如下图所示,我们首先有一个应用程序模块(cf: app),它将通过其接口使用域。域使用这些服务实现了公开的接口,如果需要使用外部关注点(例如数据库访问),则将使用其他接口,称为端口(出站)。
要使用域的模块必须实现此接口(适配器)并在其与域之间映射数据。
实现
我们将创建一个新的Spring Boot应用程序,其中包含一个非常简单的案例,以说明这种架构。
你可以在Github存储库中找到源代码。
这是一个从外部公共API检索电影信息的应用程序。
我们将创建一个Maven项目,以movies
为根,以下有3个子模块。
movies
├───movies-app
├───movies-domain
└───movies-infra-api
如你所见,我们有一个名为movies-app
的主模块,一个名为movies-domain
的域模块和一个名为movies-infra-api
的基础结构模块。每个基础结构都代表有关基础结构的详细信息,例如数据库访问和外部API调用。
由于域仅包含具有业务规则的类,因此它不能单独运行。它需要一些配置和一个主要方法或其他技术细节才能运行。
六边形架构的第一条规则很清楚:
- 域不依赖任何内容。一切都依赖于它。
第二个是:
- 你不会直接使用基础结构模块,而是必须只通过域模块传递。
因此,两个基础结构模块之间没有直接依赖关系。没有人知道其他人的存在
那么,如果域不依赖于它们,域如何能够使用基础结构API?
依赖规则
pom.xml配置将防止我们意外耦合模块。
为了遵守六边形架构的依赖规则,你需要像下面的movies-domain
的pom.xml配置一样:
...
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
</dependencies>
...
正如你所看到的,没有关于Spring的内容,也没有关于持久性或外部API的内容。这是一个不可变的规则,域不依赖任何内容。只是需要编译(例如Lombok)或运行测试的内容。我们如何使用其他模块?
领域层将公开接口,每个基础设施模块都将实现它们。因此,领域层只关心使用它的接口,不关心它们将由谁以及如何实现。
对于所有基础设施的依赖,我们将使用提供范围为 provided
的领域模块,如下所示:
...
<dependencies>
...
<dependency>
<groupId>com.architecture.hexagonal.example</groupId>
<artifactId>movies-domain</artifactId>
<scope>provided</scope>
</dependency>
...
</dependencies>
...
对于应用程序模块 movies-app
,我们需要像下面这样导入领域和所有其他模块:
...
<dependencies>
<dependency>
<groupId>com.architecture.hexagonal.example</groupId>
<artifactId>movies-domain</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.architecture.hexagonal.example</groupId>
<artifactId>movies-infra-api</artifactId>
<scope>runtime</scope>
</dependency>
...
</dependencies>
...
对于所有基础设施模块,我们需要使用 runtime
范围。这可以防止开发人员意外地在其他基础设施模块中直接使用模块。
有时基础设施模块需要另一个基础设施模块。为此,它必须通过领域层而不是直接使用它。你不应该在其他基础设施模块中找到基础设施的依赖关系。正是‘app’将协调所有基础设施依赖项以建立链接。
领域
因此,我们将从这个项目的主要重要部分——‘领域’开始。在开始编码之前,我们需要停下来一分钟,思考一下我们领域的‘通用语言’。在现实世界中,这部分可能需要花费很长时间,从项目开始时就可以开始,并且可以在项目的整个生命周期中发展。
你的应用程序需要做什么?为什么创建它?
答案是:电影
我们想要检索电影信息,如标题、描述、评级、导演、演员等等。
因此,让我们从创建我们必须在应用程序中找到的通用语言开始。
通用语言
由于我们的业务领域非常简单,因此很容易快速创建我们的通用语言,但在实际情况下,可能需要很长时间。在找到它之前可能需要几天或几个月,每个利益相关者都要批准并使用它。
在我们的案例中,它将是这样的:
- 电影:表示电影列表
- 标题:电影标题
- 简介:具有简短简介的电影解释
- 评级:电影评级
- 导演:创建电影的人
- 演员:在电影中扮演角色的人
- 发布日期:电影发布日期
评级需要在0到5之间
电影 API
为此,我们将使用开放 API TheMovieDB,以及由 Holger Brandl 在这个存储库中提供的库。
我已经为这篇文章创建了一个虚假账户。令牌放在项目的 application-local.yml
中。如果令牌关闭,你可以创建一个账户并获取一个新令牌:https://www.themoviedb.org/signup
一切就绪后,你可以直接运行 Spring Boot 应用程序并测试它。
暴露我们自己的 API
我们的 API 公开了3个 API,这些 API 与 Movie DB API 相映成趣。我们将数据与我们自己的数据模型进行映射。
- GET http://localhost:8080/api/v1/movies/populars
- GET http://localhost:8080/api/v1/movies/upcoming
- GET http://localhost:8080/api/v1/movies/{{id}}
第一个给你一个按流行程度排序的电影列表,第二个给你即将上映的电影列表,最后一个给你带有路径参数的电影详细信息。
架构
对于架构,我们显然使用六边形架构,使用Apache Maven创建模块并构建我们的应用程序。
如上所述,POM 的配置遵守六边形依赖关系的规则。
在 movie-app
模块中,我们有用于运行 Spring Boot 应用程序和管理我们公开的小 API 的主类。下图表示架构,以便更好地阅读项目结构。
我们将在下面更详细地解释每个模块。
领域模块
在领域模块中,我们创建了一个名为 movies
的包,该包允许我们在同一有界上下文中创建不同的领域或子领域部分。对于这个上下文,你需要关注它,以确保不混合不允许一起发展的不同领域。事实上,在同一个领域包中,你不应该找到例如制作咖啡的领域和观看电视的领域。在这种情况下,你需要创建两个不同的领域工件(Java 的 .jar)。
在此领域模块中,我们将找到子包来管理模块,例如,错误包用于异常处理,模型包用于表示业务模型。在这个模板包中,你需要找到通用语言。
你还有一个服务包,用于表示所有业务逻辑。你的应用程序需要公开的所有功能都需要驻留在这个包中。最后,我们有一个 outbound
包。该包包含所有由 service
包使用的接口。它表示领域未涵盖的所有功能,例如数据库访问、API 调用...这些接口将由外部基础设施模块实现。
应用程序模块
在应用程序模块中,我们将找到运行应用程序所需的所有内容。这是一个编排所有其他模块的模块。在它的 POM 配置中,我们需要找到直接提供的领域模块和声明为仅在运行时使用的所有基础设施模块。与领域模块一样,我们为错误管理(声明和处理错误)找到了一个包。配置包收集了所有 Spring Boot 配置,在我们的情况下,我们只有一个 Bean 配置,用于声明领域模块服务(请记住,领域不知道 Spring 上下文)。我们还找到了用于公开 API 的控制器包。显然,我们有一个名为 DemoApplication
的主类,用于运行我们的 Spring Boot 应用程序。
基础设施模块
最后,我们有了基础设施模块,它组合了所有的配置和服务,用于外部API调用。在配置包下,我们有有关Spring和用于调用TheMovieDB API的库的所有配置。还有一个通常的错误管理包和一个服务包,它们代表了领域所需的功能。类TheMovieDBService
实现了在outbound
领域包中找到的领域接口MoviesProvider
。
结论
我希望本文能帮助读者了解干净架构的基础,并实现DDD:六边形架构。正如本文已经解释的那样,架构选择总是一种权衡,开发人员或架构师不需要一直遵循这种实现方式。
但是,的确,这种架构是非耦合模块的良好演示,以实现更好的可维护性。我们可以更改应用程序的任何部分,而不用担心其他部分的回归。领域驱动设计比仅仅遵循一种新的时尚模式更好,它是整个软件开发范式的一种观点。开发团队需要专注于领域问题,而不是被每天发展的所有新软件新工具所吸引。当软件团队理解了这一点,他们就可以应对任何业务规则挑战。
Github代码库:https://github.com/KevinDupeyrat/Hexagonal_Architecture_Article/tree/master/movies
译自:https://towardsdev.com/hexagonal-architecture-and-domain-driven-design-bc2525dbc05f
评论(0)