你可能是通过了解干净架构并做了些研究后找到了这篇文章。但是,你可能仍然不确定如何在实际项目中实现它。本指南旨在通过提供实用的见解和示例来帮助你在开发项目中有效地应用干净架构原则,从而弥补这一差距。
我将通过一个基于Nest.js框架的例子来说明Clean Architecture。不过,这些核心概念也能用在其他框架或编程语言上。这些基本理念在不同的技术中也是一样的。
什么是干净的架构设计?清潔架構,由罗伯特·C·马丁(人称“Uncle Bob”)提出,是一种强调关注点分离和软件组件独立性的软件设计哲学。以下是清潔架構的几个关键原则:
- 框架的独立性:架构不应依赖于某个功能丰富的软件库。这样你就可以把这些框架当作工具来使用,而不是被这些框架所限制。
2. 可测试性:无需依赖用户界面、数据库、Web服务器等外部元素即可测试业务逻辑。
3. 独立的UI:UI可以很容易地更改,而无需改变业务逻辑。例如,一个网页UI可以换成控制台UI。
4. 数据库的独立性:您可以轻松地将 PostgreSQL 替换为其他数据库,如 Mongo、SQLite、Firestore 等。您的业务规则不受特定数据库的约束。
5. 与任何外部实体无关:事实上,你的业务规则对外界没有任何了解。
干净架构(鲍勃大叔)
该架构通常被描绘为一系列同心圆,最内层的圆代表最抽象和最高层次的政策,而外层的圆则包含更具体化和较低层次的细节信息。核心理念是依赖关系只能指向内部,内圈的代码不应了解外圈中的函数或类。例如,最内层的代码不应知道外层的函数或类的存在。
干净架构的主要层次主要包括:
• 实体规则:适用于整个企业的业务指导方针。
• 应用场景:适用于特定应用程序的业务规则。
• 适配器:将用例和实体中的数据转换成最适合框架使用的格式。
• 框架和驱动:如UI、数据库和Web框架等外部组件。
这种架构允许业务逻辑独立于用户界面、数据库和其他外部组件进行测试,从而使得系统更加灵活和易于维护。
通常,实体层和用例层被表示为一个——业务逻辑层,这也将是我们的情况——这也将是我们的案例。
一个示例项目这个项目是一个简单的“图书馆”应用——一个提供书籍和作者的创建、读取、更新和删除操作的 REST API(表述性状态转移的应用程序接口)。目标是保持简单,同时提供一个实现 Clean Architecture 的清晰示例。这种方法避免了不必要的复杂情况,使这个项目成为一个很好的学习资源。我想你已经厌倦了那些 TODO 应用了。
你可以在GitHub上找到它。
项目的结构是这样安排的,应用程序代码位于_app_目录下。根目录下提供了一个_docker-compose_文件,以便更容易设置项目所用的PostgreSQL数据库。
安装指南
- 环境设置:
▪ 复制 .env.template 文件并将其改名为 .env。
确保按照本文件中提到的,为相应的环境变量提供正确的值。
2. 外部服务整合 :
该项目包含与第三方服务 Resend 的集成示例。这是通过环境变量 RESEND_API_KEY 来展示的。如果您有 Resend 账户,可以在 .env 文件中设置您的 API 密钥。不过,运行项目时这并不是必须的。
3. Docker运行环境
该项目包含一个 docker-compose 文件,该文件中有一个 PostgreSQL。
这个项目重构以实现干净架构是基于一个简单的三层应用结构。这种结构包括两个主要模块:BooksModule(书籍模块)和AuthorsModule(作者模块)。
每个模块都包括:
• 这些控制器:提供REST API的接口,处理传入的HTTP请求并作出相应的HTTP响应。
• 服务:它们包含业务逻辑,作为控制器和数据层之间的桥梁。
• 仓库(Repositories):这些被注入到服务内,负责数据的访问和处理,使用外部库 TypeORM。
这样的基础结构为实现“清洁架构”原则提供了一个简单的起点,确保了关注点分离的明确,提高了可维护性。
组件和模块图
该基础应用的代码在这里可以找到,点击链接可以查看: https://github.com/peterkracik/nestjs-clean-architecture/tree/1.0
向干净架构重构 文件夹结构介绍CLEAN架构(Clean Architecture)并没有规定特定的文件夹结构。相反,它是一种编码方法论,可以根据编程语言、框架和项目规模进行调整。开发者可以根据项目需求选择最合适的命名约定和结构。在这个例子中,文件夹结构如下所示。
- domain : 该目录包含核心业务逻辑,包括实体和用例。它代表应用程序的核心部分,独立于外部框架和技术。
- gateway : 该文件夹包含表现层 — 接口适配器。尽管“接口”可能是常见的命名,但在这里故意避免使用该名称以防止与 TypeScript 接口文件混淆。
- Frameworks : 该部分包含与框架、驱动程序和第三方模块相关的实现细节。这是外部依赖与核心业务逻辑集成的层。
- main.ts 和 AppModule 是应用程序的入口点。
- ProvidersModule — 作为提供抽象框架模块给应用程序的模块。在将应用程序逻辑与具体框架实现解耦方面起着关键作用,我们将在文章后面进一步探讨其功能。
我也喜欢通过在_tsconfig.json_文件中定义路径别名来实现这种分离。这种方法简化了整个项目的导入,使代码库更加整洁和易于维护。
在 tsconfig.json 中定义的路径
控制者从逻辑上讲,我们应该首先开发业务逻辑,然后通过控制器将其暴露出来。然而,这使得测试现有功能变得困难。因此,我们从控制器开始入手。
在_gateways/controllers_目录下,我们创建了一个名为_ControllerModule_的Nest.js模块,并为_books_和_authors_设置了文件夹。
- 在这些文件夹中,我们将原应用中的控制器和DTO复制过来。目前,我们把这些方法中的依赖和逻辑暂时移除,如下所示:(控制器:Controller,DTO:数据传输对象):https://github.com/peterkracik/nestjs-clean-architecture/commit/59198ae9ca48e50471a69bc68731dcd4056120e0
3. 在ControllersModule 的 controllers 属性中定义新的控制器。
4. 在 AppModule 中移除原有的 BooksModule 和 AuthorsModule 的导入项。用新的 ControllersModule 替换。
此时此刻,端点应像以前那样可访问,但不再包含实际数据。
当前应用程序的状态
可以在这里查看源代码:https://github.com/peterkracik/nestjs-clean-architecture/commits/1.1
使用案例为了先从设置必要的模块和接口开始,我们可以遵循干净架构的原则来实现业务逻辑。
- 首先,我们将创建一个空的 UseCasesModule 模块,它将包含我们的用例。
- 定义一个 BaseUseCase 接口。此接口将由所有用例实现。
- 创建用于表示领域实体的 Book 和 Author 接口。
- 我们将把 UseCasesModule 导入到 ControllersModule 中。这使得控制器可以与用例进行交互。
如《干净架构》所规定,内部圈层(例如用例)不知道外部圈层(例如网关和控制器)。然而,外部圈层可以依赖于内部圈层。如果我们的应用程序使用了不同的接口,比如事件处理器或终端应用程序,这些端点也会位于网关中。它们将被定义得像控制器一样,并同样导入UseCasesModule以与业务逻辑互动。
为了遵循 Clean Architecture 的原则实现这些用例,我们将为原 BooksService 和 AuthorsService 中的每个功能分别创建可注入的服务,每个服务代表一个用例:findAll、findById 和 create。每个服务将封装单一的用例。这种做法增强了模块化,使得测试和维护代码更加容易。
每个用例类都将实现之前创建的BaseUseCase接口。我们可以让这些类为空或者填充一些模拟数据来测试应用程序是否仍然正常运行,如下所示:此处。
// src/domain/usecases/books/get-all-books.usecase.ts
import { Injectable } from '@nestjs/common';
import { BaseUseCase } from '@domain/use-cases/base-use-case.interface';
import { Book } from '@domain/interfaces/book';
/**
* 获取所有书籍的用例
*/
@Injectable()
export class GetAllBooksUseCase implements BaseUseCase {
constructor() {}
/**
* 执行获取所有书籍的操作
*/
async execute(): Promise<Book[]> {
return [];
}
}
所有使用案例都必须定义为模块中的_提供者_并导出,这样_ControllersModule_可以将它们注入到控制器中:
// src/domain/usecases/use-cases.module.ts
import { Module } from '@nestjs/common'; // 导入模块
import { CreateBookUseCase } from './books/create-book.usecase'; // 导入创建书籍用例
import { GetBookByIdUseCase } from './books/get-book-by-id.usecase'; // 导入通过ID获取书籍用例
import { GetAllBooksUseCase } from './books/get-all-books.usecase'; // 导入获取所有书籍用例
import { CreateAuthorUseCase } from './authors/create-author.usecase'; // 导入创建作者用例
import { GetAuthorByIdUseCase } from './authors/get-author-by-id.usecase'; // 导入通过ID获取作者用例
import { GetAllAuthorsUseCase } from './authors/get-all-authors.usecase'; // 导入获取所有作者用例
const useCases = [ // 定义用例数组
GetAllAuthorsUseCase, // 获取所有作者用例
GetAuthorByIdUseCase, // 通过ID获取作者用例
CreateAuthorUseCase, // 创建作者用例
GetAllBooksUseCase, // 获取所有书籍用例
GetBookByIdUseCase, // 通过ID获取书籍用例
CreateBookUseCase, // 创建书籍用例
];
@Module({ // 定义模块
imports: [], // 导入空数组
providers: [...useCases], // 导入用例数组
exports: [...useCases], // 导出用例数组
})
export class UseCasesModule {} // 导出用例模块类
为了在初始化方法中提供这些参数并将其注入控制器,我们按如下方式操作:
// src/gateways/controllers/books/books.controller.ts
import { CreateBookUseCase } from '@domain/use-cases/books/create-book.usecase';
import { GetBookByIdUseCase } from '@domain/use-cases/books/get-book-by-id.usecase';
import { GetAllBooksUseCase } from '@domain/use-cases/books/get-all-books.usecase';
@Controller('books')
export class BooksController {
constructor(
private readonly createBookUseCase: CreateBookUseCase,
private readonly getBookByIdUseCase: GetBookByIdUseCase,
private readonly getAllBooksUseCase: GetAllBooksUseCase,
) {}
@Get()
@ApiOkResponse({ type: Array<BookDto> })
findAll() {
return this.getAllBooksUseCase.execute();
}
...
一些框架支持直接将注入作为方法参数,例如,Laravel 或 Symfony。
实现了UseCasesModule和用例场景
在这里可以找到源代码:https://github.com/peterkracik/nestjs-clean-architecture/tree/1.2/app/src
特定领域的接口和存储库为了提供框架或驱动的功能性,同时遵循干净架构的原则——即内层并不知道外层的存在——我们使用依赖倒置的概念。我们通过接口定义所需的功能性,即我们定义的接口要求,然后框架或驱动中的一个类来实现这些接口。
依赖倒置原则在实现数据仓库时的应用
在我们的应用程序中,我们需要两个接口来表示实际对象:IBook 和 IAuthor。此外,我们还需要两个接口来定义存储库:IBooksRepository 和 IAuthorsRepository。所有这些接口都非常直接,旨在为应用程序架构建立清晰的合约。
// src/domain/interfaces/author接口.ts
export interface IAuthor {
id: number;
firstName: string;
lastName: string;
books?: IBook[];
}
// src/domain/repositories/authors-repository接口.ts
export interface IAuthorsRepository {
findAll(): Promise<Array<IAuthor>>; // 查找所有
findById(id: number): Promise<IAuthor>; // 通过ID查找
add(payload: DeepPartial<IAuthor>): Promise<IAuthor>; // 添加
}
框架
(例如常用的框架有...)
现在我们已经准备好业务逻辑,我们需要将其连接到实际的外部数据源。这个来源可以是SQL数据库、NoSQL数据库、文件存储、内存数据库或外部API。在我们的例子中,我们将连接到现有的PostgreSQL数据库,该数据库原先服务于原应用。
在 src/frameworks/database 文件夹里,我们创建了一些文件。
- DatabaseModule —一个导入外部包 TypeORM 并注册和导出仓库的模块。它被定义为一个动态模块,允许我们定义所需属性。(这些内容超出了本文的范围,我不会深入探讨其创建和模块定义,更多信息在这里)
- 实体AuthorEntity 和 BookEntity 分别实现了领域中的相应接口。
- 仓库BooksRepository 和 AuthorRepository 分别实现了领域中的相应接口。
数据库模块的实现图示
Nest.js 的神奇之处 — 服务提供者提供者 是 Nest 中一个核心的概念。许多基本的 Nest 类都可以当作提供者来使用——服务、仓库、工厂、辅助工具等。主要思想是,它能够被注入作为依赖;这意味着对象之间可以建立各种关系,而这些对象的‘连接’工作则可以很大程度上交给 Nest 运行时系统来处理。
此功能使我们能够注入数据库仓库或服务,或其他由我们的框架和驱动程序提供的服务。通过利用动态模块功能,我们能够更高效地配置和管理依赖项。
供应商模块我们已经提到过这个模块了,现在我将解释它的作用。它作为一个连接器,提供领域层所需的关键注入。这样可以确保领域层能够访问所需的数据和服务,而不必直接依赖于底层框架和驱动程序。
在这个模块中,我们引入所有需要注入其他层的驱动和框架。在“providers”块中,我们定义需要赋值的类、函数或值,以及该提供者的标识名称。
然后我们导出提供者而非服务,确保应用程序其他部分可以访问这些必要的依赖,而不直接依赖底层实现。
不过,在这个例子中,我使用了一个固定的字符串,更好的做法是将它定义为常量如这里。
然后我们将这个模块添加到AppModule的引入里。
// src/providers.module.ts
@Global() // 必须定义为全局的
@Module({
imports: [
// 导入 DatabaseModule 并配置所需的配置文件
DatabaseModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'postgres',
password: 'postgres',
database: 'postgres',
}),
],
providers: [
{
// 提供 BooksRepository 作为 BOOKS_REPOSITORY
provide: 'BOOKS_REPOSITORY',
useExisting: BooksRepository,
},
{
provide: 'AUTHORS_REPOSITORY',
useExisting: AuthorsRepository,
}
],
// 导出提供者如下
exports: [
'BOOKS_REPOSITORY',
'AUTHORS_REPOSITORY',
],
})
export class ProvidersModule {}
注入提供器
使用前面定义的提供者很简单。我们可以使用NestJS提供的@Inject装饰器。因为DatabaseModule中的BookRepository实现了IBooksRepository接口,我们很容易将它注入到我们的场景中。
这使得用例能够访问必要的数据操作而不必直接依赖于数据仓库的具体实现。
// src/domain/usecases/books/get-all-books.usecase.ts
import { Inject, Injectable } from '@nestjs/common';
@Injectable()
export class GetAllBooksUseCase implements 基础用例 {
constructor(
@Inject('书籍仓库')
private readonly 书籍仓库: IBooksRepository,
) {}
async 执行(): Promise<书籍[]> {
return this.书籍仓库.查找所有();
}
}
源代码https://github.com/peterkracik/nestjs-clean-architecture/tree/1.3/app/src/domain/use-cases
展现它的光彩我们来创建这个模块,作为模拟数据库模块(MockDatabaseModule)来使用。
// 模拟数据库模块,用于提供书籍和作者的模拟数据
// src/frameworks/mock-database/mock-database.module.ts
import { booksMock } from './mocks/books.mock';
import { authorsMock } from './mocks/authors.mock';
@Module({
providers: [
BooksRepository,
AuthorsRepository,
{
provide: 'BOOKS_MOCK',
useValue: booksMock,
},
{
provide: 'AUTHORS_MOCK',
useValue: authorsMock,
},
],
exports: [BooksRepository, AuthorsRepository],
})
export class MockDatabaseModule (模拟数据库模块) {}
我们在这个模块中使用提供者来提供两个简单的变量,用于导出作者和书的模拟值。
// 模拟数据文件
export const booksMock: IBook[] = [
{
id: 1,
title: '霍比特人(The Hobbit)',
author: {
id: 1,
firstName: 'J.R.R.',
lastName: '托尔金',
名: 'J.R.R.',
姓: '托尔金',
},
},
...
]
实现 IBookRepository 接口的仓库,并注入定义的模拟对象。
// src/frameworks/mock-database/repositories/books.repository.ts
@Injectable()
export class BooksRepository implements IBooksRepository {
constructor(@Inject('BOOKS_MOCK') private books: IBook[]) {}
findAll(): Promise<Array<IBook>> {
return Promise.resolve(this.books);
}
add(payload: DeepPartial<IBook>): Promise<IBook> {
payload.id = this.books.length + 1;
this.books.push(payload as IBook);
return Promise.resolve(payload as IBook);
}
findById(id: number): Promise<IBook> {
return Promise.resolve(this.books.find((w) => w.id === id));
}
}
// the above is TypeScript code, which implements the IBooksRepository interface and provides methods for finding all books, adding a new book, and finding a book by ID.
在我们项目中的 ProvidersModule 内,我们可以通过设置一个简单的条件(比如环境变量)来定义使用哪个数据库模块。这种方法在进行端到端(e2e)测试时特别有用,你可以轻松切换不同的数据库配置。并且这种方式在应用的其他部分无需做任何改动。
超简单的魔法! 💫
// src/提供者模块.ts
@全局()
@模块({
导入: [
DatabaseModule.forRoot({...}), // 数据库模块初始化
MockDatabaseModule, // 模拟数据库模块
],
提供者: [
{
提供: BOOKS_REPOSITORY, // 书籍存储库
使用现有: process.env.MOCK ? MockBooksRepository : BooksRepository, // 根据环境变量决定使用模拟存储库还是实际存储库
},
{
提供: AUTHORS_REPOSITORY, // 作者存储库
使用现有: process.env.MOCK ? MockAuthorsRepository : AuthorsRepository, // 根据环境变量决定使用模拟存储库还是实际存储库
},
],
导出: [BOOKS_REPOSITORY, AUTHORS_REPOSITORY], // 导出书籍和作者存储库
})
代码: https://github.com/peterkracik/nestjs-clean-architecture/blob/1.5/app/src/providers.module.ts
整些额外框架的例子 发送通知为了展示除数据库之外的其他类型的服务,我实现了两个通知模块:ResendEmailsModule,利用SaaS服务Resend来发送电子邮件,以及MockNotifications,只是将请求的通知信息记录到控制台。因为这两个模块都实现了相同的INotificationsService接口,因此它们可以互换,就像之前的DatabaseModule和MockDatabaseModule一样。
身份验证模块到目前为止,我们讨论了在领域层实现框架,但还没有提到 presenter 层。不过,它的运作方式是一样的。我创建了一个简单的 AuthModule,用于验证请求的 Bearer 令牌。
// src/frameworks/auth/auth.module.ts
/**
* @Module 注解用于定义模块,providers 表示模块中的服务提供者,
* exports 表示模块中要导出的服务。
*/
@Module({
providers: [AuthService], // 注入服务
exports: [AuthService], // 导出服务
})
export class AuthModule {} // 定义 AuthModule 类
// src/frameworks/auth/auth.service.ts
@Injectable()
export class AuthService implements IAuthService {
async 验证令牌(token: string): Promise<boolean> {
return '123' === token;
}
}
// src/providers.module.ts
@Global() // 全局模块
@Module({
imports: [
...
AuthModule, // 导入身份验证模块
],
providers: [
...
{
provide: AUTH_SERVICE, // 提供服务
useExisting: AuthService, // 使用现有服务
},
],
exports: [
...
AUTH_SERVICE, // 导出服务
],
})
export class ProvidersModule {} // 导出提供者模块
在Presenter层中,可以在guards文件夹内创建一个名为IAuthService的接口。
// 验证令牌的接口定义
export interface IAuthService {
validate(token: string): Promise<boolean>;
}
我们要创建一个守护。
// src/gateways/guards/auth.guard.ts
import { AUTH_SERVICE } from '@/constants';
import { IAuthService } from './auth-service.interface';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
// 通过提供 AUTH_SERVICE 注入服务
@Inject(AUTH_SERVICE)
private readonly authService: IAuthService,
) {}
canActivate(
context: ExecutionContext, // 请求上下文
): boolean | Promise<boolean> | Observable<boolean> {
// 返回一个布尔值,表示是否通过验证
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
return false; // 如果没有令牌,则返回 false
}
// 调用 AuthService 的验证方法
return this.authService.validate(token);
}
/**
* 用于从请求头中提取令牌的私有方法
*/
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers['authorization']?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined; // 从请求头中提取令牌
}
}
然后我们将守卫添加到特定端点或在整个_ControllersModule_中。
三层图示
源代码: https://github.com/peterkracik/nestjs-clean-architecture/tree/1.7/app/src/gateways/guards
更多关于 guards(守卫)的详情(在 NestJS 安全认证文档中):https://docs.nestjs.com/security/authentication
实体模型层
在小型应用程序中,特别是在我们经常基于接口而不是类来处理通用对象时的JavaScript项目中,实体层并不总是必要的。这可能会增加不必要的复杂性。不过,为了实现完整功能,我们还是继续创建并实现实体层。
如果你按照我提供的标签来跟踪源代码,可能会有点混乱。最初,我将书和作者的定义命名为Book和Author。然而,在实现这些类时,由于名称相同,它们之间产生了冲突。为了解决这个问题,我将它们改名为IBook和IAuthor。这样,相应的实体类就可以命名为Book和Author了。
我在 src/domain/entities 文件夹内创建了两个类 — Book 和 Author — 这两个类都继承自 BaseEntity,以继承一个通用方法,该方法可以将对象转换为类的实例,并在实体层中展示了这种“业务逻辑”方法的示例。我知道这并不是真正的业务逻辑方法,只是一个示例方法,但它可以很容易地变成类似 createSlug 这样的方法。
然后用例会稍微做些修改,以便在 CreateAuthorUseCase 中初始化一个 作者 实例。
// src/domin/usecases/authors/create-author.usecase.ts
@Injectable()
export class CreateAuthorUseCase implements BaseUseCase {
constructor(
@Inject(AUTHORS_REPOSITORY)
private readonly authorsRepository: IAuthorsRepository,
@Inject(NOTIFICATIONS_SERVICE)
private readonly notificationService: INotificationsService,
) {}
async execute(payload: CreateAuthorUseCasePayload): Promise<IAuthor> {
// 如果需要对数据应用特定的业务逻辑,这一步是必要的
const author = new Author();
author.fromDao(payload);
// 在数据库中创建该作者
const created = await this.authorsRepository.add({
firstName: payload.firstName,
lastName: payload.lastName,
});
if (!created) {
throw new Error('作者未被创建');
}
// 将创建的数据与作者实体合并
author.fromDao(created);
await this.notificationService.sendNotification(
`作者 ${author.firstName} ${author.lastName} 已使用 ID ${author.id} 成功创建`,
'新作者已成功创建',
);
return author;
}
}
带有实体层的整个图示如下:
“你把框架库引入了领域层”import { Module } from '@nestjs/common';
...
@Module({
...
})
export class 用例模块类 {}
那是真的。在这个例子中,我们将 Module 装饰器导入领域层的一个文件中。我认为这样做是 “次佳选择”,因为虽然可以解决,这样做会引入不必要的复杂性。
我们可以在业务层创建一个自定义的模块装饰器,并在框架层创建一个名为NestModule的新模块,在那里我们就可以为这个模块提供一个具体的实现方式。这种方法可以确保装饰器不依赖于特定的框架。
然而,由于 NestJS 提供了许多装饰器,严格遵循清洁架构的原则会要求我们将所有这些装饰器按照这种方式实现。这可能会引入不成比例的复杂度,尤其是在较小的项目中。因此,通常更实际的做法是接受这种“次佳选择”,直接使用框架的装饰器,在架构的纯粹性和实用开发之间找到平衡。
结论: 这样做对于小型应用有意义吗?不,但是可以!
为什么不?
实现干净架构需要编写大量的样板代码和相对复杂的代码,这意味着它需要更多的时间来创建,并且新加入项目的开发人员也需要更多时间来上手。
为什么呀?
- 如果你不理解这一点,也不习惯在小应用中这样写,那么在中型或大型应用中你需要它时就会遇到困难。
- 代码更易于维护,而且客户可能也不完全清楚他们想要什么,因此在开发过程中可能会产生需求变更。
- 如之前所展示的,调整数据库或通知服务根据环境非常容易——你可以在开发机器上、CI/CD流水线中或生产环境中使用不同的配置。
- 不同的开发人员可以独立地工作在应用的不同部分,而不会出现问题。这是因为清晰的架构提倡明确的职责分离,使得团队可以独立地开发和维护不同的层。
- 你可以更改底层框架或第三方库的功能,而不会对应用产生显著影响。这种灵活性是使用清晰架构的关键优势之一,因为它将业务逻辑与外部依赖隔离。
不,你不太可能从 Nest.js 换成 Express.js,或从 Laravel 换成 Symfony,而不完全重做应用程序。不过,框架的重要版本更新可能会产生重大影响。如果框架的使用仅限于框架本身,这会简化更新的过程。
虽然你可能无法在运行的应用程序中改变框架结构,但你可能希望在不同的框架体系中重用业务逻辑层的代码。代码对特定框架或第三方库的依赖越少,就越容易重用。
GitHub - peterkracik/nestjs-clean-architecture: 一个遵循清洁架构原则的 Nest.JS 示例项目 - 示例 repo 简单明了的英文解释 🚀感谢你加入我们的In Plain English大家庭!在你离开前:
- 记得给作者点赞并关注👏
- 关注我们:X | LinkedIn | YouTube | Discord | Newsletter | 播客节目
- 在Differ上免费创建一个AI驱动的博客。
- 更多内容请访问 PlainEnglish.io
共同学习,写下你的评论
评论加载中...
作者其他优质文章