原文链接:How to Structure Code

软件设计最佳实践,逐层发现包、按功能发现包以及六角形架构/端口和适配器。

在这篇博文中,我将探讨如何构建我们的代码并讨论最佳实践,涵盖三种不同的方法:按层打包、按功能打包以及六边形架构/端口和适配器及其优缺点。

在探索构建代码的不同方法之前,我们需要了解基本的软件设计原则:

  • 内聚力:指模块内各类之间的相互关联程度。
  • 耦合度:指不同模块之间的依赖程度。

img

  • 模块化:指软件系统被划分为独立模块的程度。每个模块都封装了一组特定的功能,并设计为独立工作,同时通过明确定义的接口相互交互。
  • 抽象:隐藏实现细节并仅通过接口公开必要的功能。
  • 关注点分离:设有不同的部分,每个部分解决一个特定的关注点。
  • 封装:将数据和方法捆绑到单个模块或类中以隐藏内部细节。

让我们仔细看看内聚力和耦合力?

内聚力描述了软件的集中程度。它与单一责任原则密切相关。

  • 高内聚力意味着模块内的类紧密相关并具有共同的、明确定义的目的。
  • 低内聚力意味着模块内的类之间关系松散,缺乏明确的目的且职责不相关。

要遵循的最佳实践是实现模块之间的高内聚和松散耦合

img

松耦合被认为是结构良好和设计良好的计算机系统的标志,与高内聚力相结合,可带来较高的可读性和可维护性。

img

现在,让我们探索构建代码的不同方式。首先,我将逐层介绍包,然后逐功能介绍包,并比较两者。之后,我们将探索端口和适配器模式。

img

层封装

它代表一个项目结构,其中类被组织成多个层,每个层负责一组特定的功能。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
src
├── main
│   ├── java
│   │   └── com
│   │       └── app
│   │           ├── service 
│   │           │   └── UserService.java
│   │           │   └── OrderService.java
│   │           │   └── ProductService.java
│   │           ├── domain   
│   │           │   └── User.java
│   │           │   └── Order.java
│   │           │   └── Product.java 
│   │           ├── repository   
│   │           │   └── UserRepository.java
│   │           │   └── OrderRepository.java
│   │           │   └── ProductRepository.java
│   │           ├── controller     
│   │           │   └── UserController.java
│   │           │   └── OrderController.java
│   │           │   └── ProductController.java

典型的层次包括:

  1. 表示层:此层负责处理用户交互并向用户呈现信息。它通常包括与用户界面、控制器和视图相关的组件。
  2. 服务层:该层包含业务逻辑并提供表示层所需的数据。
  3. 领域包:此包包含领域实体。
  4. 数据访问层:此层负责处理数据库的数据持久化和检索。
  5. 基础设施包:此包提供支持应用程序运行的服务。它可能包括日志记录、配置、安全和其他跨切关注点的组件。

以下是使用逐层打包的一些缺点:

  • 低内聚力:不相关的类被分组到同一个包中。
  • 高耦合
  • 封装性差:大多数类都是公共的,因此我们不能将类作为包私有的,因为其他层需要它们。
  • 模块化程度低:由于每个包都包含与特定层相关的类,因此以后很难将代码分解为微服务。
  • 可维护性差:由于类分散在各个包中,因此很难找到所需的类。
  • 它提倡数据库驱动设计而不是领域驱动设计。

按功能打包

它代表一种基于特性或功能而非层来组织代码的结构。在这种方法中,每个包代表一个独特且独立的特性。

目标是将与特定功能相关的所有组件(例如控制器,服务,存储库和域类)组合到一个包中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
src
├── main
│   ├── java
│   │   └── com
│   │       └── app
│   │           ├── user
│   │           │   ├── UserController.java
│   │           │   ├── UserService.java
│   │           │   └── UserRepository.java
│   │           ├── order
│   │           │   ├── OrderController.java
│   │           │   ├── OrderService.java
│   │           │   └── OrderRepository.java
│   │           ├── product
│   │           │   ├── ProductController.java
│   │           │   ├── ProductService.java
│   │           │   └── ProductRepository.java

使用这种结构的一些好处:

  • 高内聚力
  • 低耦合
  • 强封装:允许某些类将其访问修饰符设置为包私有(package-private)而不是公共(public)。
  • 高模块化:由于每个包都包含与特定功能相关的类,因此以后很容易将代码分解为微服务。
  • 可维护性:由于功能所需的所有类都在同一个包中,因此减少了在包之间导航的需要。
  • 促进领域驱动设计

端口和适配器模式(六边形架构)

六边形架构,也称为端口和适配器,是 Alistair Cockburn 博士在 2005 年撰写的一篇文章中引入的一种软件架构模式。

六边形架构

创建无需 UI 或数据库即可运行的应用程序,以便您可以针对…运行自动回归测试。

阿利斯泰尔·科克伯恩

该模式通过保持核心业务逻辑独立于外部细节并且不与数据库、用户界面或外部服务等外部依赖项紧密耦合来促进关注点的隔离/分离。

这使得测试、维护和发展系统变得更加容易。

img

在此模式下:

  1. 域/核心:表示应用程序的业务逻辑或域(应用程序的核心)。
  2. 端口:端口是核心定义的允许与外部组件交互的接口。这些接口可以包括服务、存储库或任何外部依赖项的接口。
  3. 适配器:适配器是端口的实现。它们将核心应用程序连接到外部组件,例如数据库、用户界面和外部服务。适配器可以特定于不同的技术或协议。
  4. 主要参与者:系统用户,例如 webhook、UI 请求或测试脚本。
  5. 次要参与者:由应用程序使用,这些服务要么是存储库(例如数据库),要么是接收者(例如消息队列)。

img

六边形: 六边形象征着核心应用程序位于中心,周围环绕着适配器。此形状表示核心与其外部依赖项之间的明确分离。

顶层包结构应如下所示:

1
2
3
4
5
6
7
8
9
src/main/
  java
    mina
      dev
        <servicename>
          adapters
          config
          core
          <ServiceApplication>.java

根包应该只包含以下包:

  • core包含服务的所有领域逻辑。它可能包含子包。
  • 端口应该位于核心包中:端口只是核心声明的、供适配器调用或实现的接口。
  • adapters软件包包含所有适配器实现代码。它可能包含子软件包,用于按单个适配器或按技术组织适配器代码。
  • config包包含用于将不同组件连接在一起的配置类。

包依赖规则:

  • 根包可能依赖于所有其他包。
  • 软件包config可能依赖于coreadapters
  • 可能adapters取决于core但不取决于config
  • Core 可能不依赖于任何其他包。

您可以在我的 github 存储库中找到更多详细信息和示例:https://github.com/minarashidi/transfer-service-hexagonal-architecture/tree/main/src/main/java/com/rashidi/transferservice

我希望这篇文章能帮助您更好地理解不同的代码结构。

我非常希望听到您的想法和评论。欢迎分享您的见解或提出任何问题。感谢您的阅读!