六边形架构和清洁架构(附示例)

2025-06-04

六边形架构和清洁架构(附示例)

六边形架构和清洁架构

改变我作为软件开发人员的职业生涯并改变我对如何构建软件的看法的事情之一是关于如何构建我的应用程序以及如何以更专业的方式设计我的代码的知识,从而使我的应用程序能够扩展。

顺便说一句,我正在制作一系列关于使用 Typescript 的 Clean Architecture 的视频,在其中我将构建一个真实世界的 API,探索一系列很酷的功能和概念,以及如何使用 Clean Architecture 来实现它们。

如果这听起来很有趣,并且您想使用 MVC 超越简单的 CRUD,您可以查看YouTube 上的播放列表该项目的 GitHub 存储库

我们将在本文的最后一部分介绍一些您可以遵循的后续步骤和示例,但如果您正在寻找如何将 Clean Architecture 应用于 Typescript 项目的示例,您可以查看此存储库

继续阅读以更好地理解这些概念,以及为什么我认为理解清洁架构的最佳方式是首先理解六边形架构:)

通常,当我们开始开发软件时,尤其是在使用某些框架时,我们倾向于使用众所周知的 MVC 模式,将应用程序划分为模型、视图和控制器。
如果您正在构建一些简单的应用程序,例如 MVP,这还不算太糟。但是,当我们需要扩展应用程序、添加更多功能、替换库或执行类似操作时,我们就会面临一些问题。因为随着代码耦合度的增加,如果不修改代码中的许多不同位置,就很难对应用程序进行更改。

这样一来,我们的软件就更容易出现 bug,也打击了所有需要维护代码的开发者的积极性。这种情况非常常见,尤其是在刚起步的开发者身上。我们通常不会学习如何构建应用程序,而是让某个框架替我们做决定,最终把所有业务逻辑都放在控制器里。
我记得我刚开始职业生涯的时候经常这样做,也记得在很多面试中被拒绝过,主要是因为那些公司正在寻找更高级或中级的人才。

话虽如此,我写这篇文章的主要目的是让您更好地理解如何通过将软件分成多个层来构建您的应用程序,以及如何组织和隔离您的业务逻辑,这样您就不会再将所有内容挤在控制器内。

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

在深入研究清洁架构之前,我们先来简单了解一下六边形架构(或者端口和适配器架构,我认为这是一个更好的名称)。

六边形架构(也称为端口和适配器架构)是一种软件架构,其基于将应用程序分离为松散耦合的组件,从而将核心业务逻辑与外部关注点隔离的理念。

六边形架构

我从Netflix 博客文章中截取了这张图片

假设我们正在构建一个需要从 REST API 获取一些数据的 API。

我们不必将用例类(或服务类)与外部 REST API 紧密耦合,只需创建一个Port即可。Port 定义了用例类需要实现的接口,用于获取数据(与获取数据的方式无关,例如使用 REST API、数据库、GraphQL API 等)。然后,我们可以创建一个Adapter 类,它是一个实现Port接口的类,并将调用委托给外部 REST API(或其他外部服务)。

这样,如果我们需要改变获取数据的方式,我们只需要创建一个实现Port接口的新适配器而不必改变用例实现

在看到如何创建这些端口适配器的一些实际示例之前,我希望您先看一下这篇博文中的这两句话,我相信它们概括了六边形架构的主要思想:

六边形架构的理念是将输入和输出置于设计的边缘。业务逻辑不应该依赖于我们暴露的是 REST 还是 GraphQL API,也不应该依赖于我们从哪里获取数据——数据库、通过 gRPC 或 REST 暴露的微服务 API,还是仅仅是一个简单的 CSV 文件。

该模式使我们能够将应用程序的核心逻辑与外部关注点隔离开来。核心逻辑的隔离意味着我们可以轻松更改数据源细节,而不会对代码库造成重大影响或进行大量代码重写。

值得注意的是,六边形架构先于整洁架构出现,然而,两者的目标都是相同的,即关注点分离
事实上,六边形架构是 Robert C. Martin(Bob 叔叔——《整洁架构》的作者)用来引用整洁架构的架构模式之一。

然而,六边形架构缺少一些实现细节,这就是清洁架构的用武之地,可以填补这些“空白”。

Typescript 示例

让我们使用 TypeScript 来看看六边形架构的实际应用。

创建这些端口适配器的方法之一是使用依赖倒置原则,这是最后一个 SOLID 原则(这对清洁架构来说是一种破坏)。

依赖倒置原则指出

“高级模块不应该从低级模块导入任何东西。两者都应该依赖于抽象(例如接口)。”

“抽象不应该依赖于细节。细节(具体实现)应该依赖于抽象。”

假设我们要创建一个用例类来导出用户。

如果您不熟悉用例的概念,请不要担心,我们将在清洁架构部分更详细地介绍它。

但现在,只需记住,用例类(在领域驱动设计术语中称为服务类)是负责特定用户操作(意图)的类。例如获取订单、创建产品、更新用户、删除文章等。
通常,控制器类(负责处理用户请求)会调用用例类来执行用户操作。

最常见的方法是将用例类(甚至控制器)与导出逻辑紧密耦合。

但是我们不这样做,我们可以创建一个接口来抽象导出逻辑(Port):



type User = {
    name: string,
    email: string,
    dateOfBirth: Date
};

// Port interface
interface ExportUser {
    export(user: User);
}


Enter fullscreen mode Exit fullscreen mode

然后创建该接口(Adapter)的一个(或多个)实现:



// CSV Adapter implementation
class ExportUserToCSV implements ExportUser {
    export(user) {
        // Export user to a CSV file
    }
}

// PDF Adapter implementation
class ExportUserToPDF implements ExportUser {
    export(user) {
        // Export user to a PDF file
    }
}


Enter fullscreen mode Exit fullscreen mode

这样,用户导出的实现将依赖于接口(抽象),正如依赖倒置原则所述。
用例类将不再依赖于任何具体的实现



// Use case class
class ExportUserUseCase {
    constructor(private exportUser: ExportUser) {}

    execute(user: User) {
        this.exportUser.export(user);
    }
}


Enter fullscreen mode Exit fullscreen mode

最后,我们可以在用户导出的不同实现之间切换,而不必更改用例类的实现:



// Export user to a CSV file
const exportUserToCSV = new ExportUserToCSV();
const exportUserUseCase = new ExportUserUseCase(exportUserToCSV);

exportUserUseCase.execute({
    name: 'John Doe',
    email: 'john.doe@mail.com',
    dateOfBirth: new Date()
});


Enter fullscreen mode Exit fullscreen mode


// Export user to a PDF file
const exportUserToPDF = new ExportUserToPDF();
const exportUserUseCase = new ExportUserUseCase(exportUserToPDF);

exportUserUseCase.execute({
    name: 'John Doe',
    email: 'john.doe@mail.com',
    dateOfBirth: new Date()
});


Enter fullscreen mode Exit fullscreen mode

清洁架构

现在,在了解了六边形架构之后,我们可以开始更好地了解清洁架构的含义。

六边形架构和许多其他架构模式都拥有相同的目标:关注点分离。它们都通过将应用程序划分为多个层来实现这一目标。
“整洁架构”只是尝试将所有这些架构整合到一个单一的理念中

值得一提的是,清晰架构不仅仅是一个可以复制粘贴到项目中的文件夹结构。
它的核心思想是将应用程序划分为多个层,并遵循依赖规则,从而创建一个具有以下特点的系统:

  • 独立于框架
  • 可测试
  • 独立于 UI
  • 独立于数据库
  • 独立于任何外部机构

首先,让我们了解一下清洁架构的各个层次:

清洁架构

实体(领域层)

这一层负责应用程序的业务逻辑。
它是最稳定的层,基本上是应用程序的核心。

在这里我们可以应用一些领域驱动设计策略,例如聚合、值对象、实体、领域服务等。
顺便说一下,2004 年引入领域驱动设计概念的书名是“领域驱动设计:解决软件核心的复杂性”,作者是 Eric Evans。

该层与应用程序的其余部分(外层关注)隔离。

用例(应用层)

这一层包含特定于应用程序的业务规则。它实现了应用程序的所有用例,并使用了领域类,但它与外部层(例如数据库、适配器等)的细节和实现隔离。
这一层仅包含与外界交互的接口。

正如我们之前所见,用例是用户操作,或者说用户意图,例如获取订单、创建产品等等。
并且每个用例必须独立于其他用例,以符合单一职责原则(SOLID 的第一条原则)。

例子:



import { GetPostByIdUseCase } from '@application/interfaces/use-cases/posts/GetPostByIdUseCase';
import { GetPostByIdRepository } from '@application/interfaces/repositories/posts/GetPostByIdRepository';
import { PostNotFoundError } from '@application/errors/PostNotFoundError';

export class GetPostById implements GetPostByIdUseCase {
    constructor(
        private readonly getPostByIdRepository: GetPostByIdRepository,
    ) {}

    async execute(postId: GetPostByIdUseCase.Input): Promise<GetPostByIdUseCase.Output> {
        const post = await this.getPostByIdRepository.getPostById(postId);
        if (!post) {
            return new PostNotFoundError();
        }
        return post;
    }
}


Enter fullscreen mode Exit fullscreen mode

在这个例子中,我们有一个接口(GetPostByIdUseCase),它包含用例InputOutput,即DTO(数据传输对象),它们与其他接口(如GetPostByIdRepository接口)一起使用来与外界交互。

请注意,此用例仅负责通过 ID 获取帖子。如果需要删除帖子,则需要创建另一个用例。
例如,在使用 MVC 模式时,使用一个庞大的控制器类来处理所有用户请求是很常见的。然而,这违背了单一职责原则。

接口适配器(基础设施层)

基础设施层包含应用程序的所有具体实现,如存储库、适配器、数据库连接等。

框架和驱动程序

这一层由数据库、Web框架等框架和工具组成,
通常我们不会在这一层写太多的代码。

只有四层?

没有规定说你必须始终只有这四层。但是,依赖规则始终适用。

依赖规则

依赖规则规定

“源代码依赖关系只能指向内部。”

“内部圈子里的任何人都不可能知道外部圈子里的事情。”

基本上,外层循环中声明的任何名称都不能被内层循环中的代码提及。这包括函数、类、变量或任何其他命名的软件实体。
我们不希望外层循环中的任何内容影响内层循环。

例如,在领域层,我们不能提及应用层或基础设施层中的任何内容。同样,在应用层,我们不能提及基础设施层中的任何内容,等等。

但显然,我们可以使用应用层中的领域层内定义的实体。

最后的想法和下一步

如果您想设计一个大型应用程序(并且超越 MVC 模式的简单 CRUD 功能),清洁架构 (Clean Architecture) 是一个很好的架构模式。
通过将应用程序划分为多个层,它将帮助您编写干净、灵活、可测试且易于维护的代码。

此外,如果你想更好地组织业务逻辑,并对应用程序的领域进行建模,还可以利用领域驱动设计。清晰架构与领域驱动设计相结合,是一个非常强大的工具!

如果您想了解更多关于 Clean Architecture 的信息,还可以查阅《Clean Architecture》一书或这篇Clean Architecture 博客文章
有趣的是,您需要了解的关于 Clean Architecture 的所有内容都已包含在这篇博客文章中,书中所有提到 Clean Architecture 的页面也都包含在这篇博客文章中。
当然,如果您有机会阅读这本书,我强烈推荐它,但重要的是要知道,所有概念都已在这篇博客文章中进行了解释。

如果您想通过代码示例了解如何在项目中使用 Clean Architecture,也可以查看这个仓库:简单博客应用程序:后端挑战
我在其中创建了一个使用 TypeScript 和 MongoDB 构建的简单博客 API,使用了 TDD、Clean Architecture、SOLID 原则、设计模式和一些 DDD 模式。
它是后端编码挑战的一部分,因此它还包含一些其他有趣的功能,例如使用 JWT 进行身份验证、使用 GitHub Actions 的 CI 工作流、使用 Docker 和 Docker Compose 的本地环境以及文档。

这是一个很好的例子,可以帮助你理解如何使用 Clean Architecture,但请记住,Clean Architecture 并非指文件夹结构,你可以将其用作参考,但每个项目都有其特定的实现需求。
在这个项目中,我为文件夹内的每个层创建了一个目录src。除了领域层、应用层和基础架构层之外,我还创建了一个额外的层作为应用程序的入口点,它包含 Express 服务器,并且所有路由都定义在此层中。在此层中,我还组合了所有控制器、中间件和用例,并注入了所需的依赖项,因为我没有使用任何依赖注入容器。你可以在项目文件
中获取更多信息。README.md

最后但同样重要的是,我正在制作一系列关于使用 Typescript 构建清晰架构的视频,在这些视频中,我将构建一个真实的 API,探索一系列很酷的功能和概念,以及如何使用清晰架构来实现它们。
我们将讨论以下主题:

  • 使用 Typescript 的 OOP
  • 清洁架构
  • SOLID 原则
  • 设计模式
  • DDD 模式
  • REST / GraphQL
  • Git
    • 常规提交
  • JWT 身份验证
  • 角色和权限
  • 集成
    • Google API
    • AWS
    • 电子邮件服务
  • 高级 MongoDB
    • MongoDB索引
    • MongoDB 模式
    • MongoDB 事务
    • 迁移
  • Docker 和 Docker Compose
  • 使用 Jest 进行 TDD
    • 单元
    • 一体化
    • 端到端
  • 使用 GitHub Actions 的 CI 工作流程
  • 使用 Swagger 的 API 文档
  • 部署

还有更多!

如果这听起来很有趣,并且您想使用 MVC 超越简单的 CRUD,您可以查看YouTube 上的播放列表该项目的 GitHub 存储库

谢谢你!

文章来源:https://dev.to/dyarleniber/hexagonal-architecture-and-clean-architecture-with-examples-48oi
PREV
使用 Github + Pipedream 从 Git repo 发布 DEV 文章
NEXT
衡量开发者关系