干净的架构概述
"干净的架构"(Clean Architecture)是一种软件设计理念,由著名的软件工程师罗伯特·C·马丁(Robert C. Martin,常被称为Uncle Bob)提出。这种架构模式的目的是创建一个易于维护、测试、扩展和部署的系统。它强调关注点分离(Separation of Concerns)的重要性,并鼓励将软件分解成互不依赖的模块。
干净的架构包含以下关键特性:
- 独立于框架(Framework-Independent) :系统的业务规则不应该依赖于存在的任何一个框架,这样可以避免业务逻辑被框架所限制。
- 可测试性(Testable) :业务规则可以在没有UI、数据库、Web服务器或任何外部元素的情况下进行测试。
- UI独立性(UI-Independent) :UI可以轻松地更改,而不会影响系统的其余部分。
- 数据库独立性(Database-Independent) :业务规则不依赖于数据库,所以可以轻松更换数据库。
- 外部代理独立性(External Agency Independence) :业务规则不知道外部世界(比如:Web或其他应用程序)。
干净的架构通常包括以下四个层次,从内到外分别是:
- 实体(Entities) :包含企业的业务规则。实体可以是一个带有方法的对象或一组数据结构和函数。
- 用例(Use Cases) :包含应用程序的业务规则。它封装了与实体的交互。
- 接口适配器(Interface Adapters) :将数据在用例和实体之间以及数据库和外部代理(比如Web)之间转换。
- 框架和驱动器(Frameworks and Drivers) :包括UI、数据库、Web等。
这个架构的一个关键特点是控制流的方向,它总是指向内部。最外层(框架和驱动器)依赖于内层(用例和实体),但内层完全不依赖于外层。这种依赖规则确保了业务逻辑的独立性和可重用性。
关键特性
-
独立于框架(Framework-Independent) :
- 在干净的架构中,系统的业务规则不依赖于任何特定的框架。这意味着框架可以作为工具来使用,而不是系统设计的基础。
- 优势在于当新的、更好的框架出现时,或者当前框架不再适用时,可以更容易地进行替换,而不影响业务逻辑的核心部分。
- 这也意味着业务逻辑应该足够抽象,从而可以在不同的环境中运行,不论是桌面应用程序、Web应用程序还是移动应用程序。
-
可测试性(Testable) :
- 干净的架构强调业务规则应该能够独立于UI、数据库、网络等外部因素进行测试。
- 这通常通过使用依赖注入、接口抽象和模拟对象等技术来实现。这样可以在不同的测试环境下对核心逻辑进行测试,而不需要复杂的设置。
- 可测试性是维护和扩展系统时的关键因素,因为它确保了对代码更改的影响可以迅速和准确地评估。
-
UI独立性(UI-Independent) :
- 干净的架构允许UI独立于业务逻辑。这意味着UI可以随着时间的推移而变化,而不影响系统的核心部分。
- 这种分离使得可以实现多种不同的用户界面(例如,Web界面、移动应用界面、桌面应用界面),而核心业务逻辑保持不变。
- 这也简化了UI的设计和测试,因为它可以独立于业务规则进行开发和修改。
-
数据库独立性(Database-Independent) :
- 在干净的架构中,业务规则与数据库技术解耦。这意味着可以更换底层数据库而不影响业务逻辑。
- 数据访问逻辑通常通过抽象层实现,例如仓储模式(Repository Pattern),从而使应用程序与特定的数据库实现细节隔离。
- 这种独立性有助于处理不同的存储需求,例如迁移至新的数据库平台或使用不同类型的数据库(如关系型数据库与NoSQL数据库)。
-
外部代理独立性(External Agency Independence) :
- 这意味着业务逻辑不直接与外部世界交互,例如网络服务、文件系统或第三方API。
- 外部交互通过接口抽象和中间层进行,这样业务逻辑就不会受到外部变化的直接影响。
- 这种设计有助于创建更稳定、更容易维护的系统,因为外部系统的变化不会直接影响到核心业务逻辑。
总体而言,干净的架构旨在通过这些关键特性实现高度解耦和模块化的系统,使其更加灵活、可维护和可扩展。
四个层级
干净架构中的四个层级是其设计的核心,这些层级从内向外分别是:
-
实体(Entities) :
- 实体代表了企业级的业务规则。它们可以是具有方法的对象(面向对象编程中的实体)或一组数据结构和函数(在更功能性的编程风格中)。
- 这些实体封装了最通用和高级的规则。例如,在一个电商系统中,实体可能包括产品、订单、客户等核心概念及其相关的业务规则。
- 实体应该是独立于任何特定用例的,这意味着它们的实现不应该被UI、框架、数据库等方面所影响。
-
用例(Use Cases) :
- 用例层包含应用程序特定的业务规则。它封装了系统应如何使用实体来执行特定的业务操作。
- 这一层处理应用程序的逻辑,例如用户创建订单、修改个人信息等操作的具体细节。
- 用例层位于实体层的外部,直接与实体层交互,但通过接口与外层交互,保持与特定的UI、数据库和外部接口的独立。
-
接口适配器(Interface Adapters) :
- 接口适配器层作为连接内部层(实体和用例)和外部层(框架和驱动器)的桥梁。
- 这一层包括了格式化数据以供内部层使用(例如,将HTTP请求数据转换为用例层可以使用的形式),以及将数据从用例层格式化以供外部层使用(例如,将数据转换为HTTP响应)。
- 接口适配器层通常包括控制器、视图、呈现器和数据访问对象等。它们将信息从外部世界转换为内部层所需的格式,反之亦然。
-
框架和驱动器(Frameworks and Drivers) :
- 这是架构的最外层,包括具体的框架、工具和驱动器,比如Web框架、数据库、UI框架等。
- 这一层的职责是处理所有与外部世界的交互。例如,它可以接受用户输入,并将其传递给内部层处理,或者从内部层获取数据并在UI上展示。
- 尽管这一层对于应用程序是必不可少的,但在干净的架构中,它被视为一种外部机制,不应该影响到内部层的业务逻辑。
在干净的架构中,每个层级都只与紧邻的内外层交互。这种分层确保了系统的不同部分可以独立变化和发展,提高了系统的灵活性、可维护性和可扩展性。例如,你可以更换UI框架而不影响业务逻辑,或者修改业务规则而不影响数据库。这种设计也使得系统更易于测试和维护,因为每个层级都可以相对独立地开发和测试。
代码示例
目录结构
以下是该应用程序的建议目录结构,以及每个部分所属的层级:
lua
src/
|-- entities/ # 实体层
| `-- User.js # 用户实体
|-- use_cases/ # 用例层
| |-- AddUser.js # 添加用户的用例
| `-- ListUsers.js # 列出用户的用例
|-- interface_adapters/ # 接口适配器层
| |-- controllers/ # 控制器
| | `-- UserController.js
| `-- repositories/ # 数据访问对象
| `-- UserRepository.js
`-- frameworks_drivers/ # 框架和驱动器层
|-- webserver/ # Web服务器(例如,Express)
| `-- server.js # 服务器设置
`-- database/ # 数据库接口
`-- InMemoryDB.js # 示例内存数据库
当然,我可以提供一个基于Node.js的简单后端应用程序的示例,这个应用程序遵循干净架构的原则。假设我们正在构建一个简单的用户管理系统,包括添加用户和获取用户列表的功能。
目录结构
以下是该应用程序的建议目录结构,以及每个部分所属的层级:
lua
luaCopy code
src/
|-- entities/ # 实体层
| `-- User.js # 用户实体
|-- use_cases/ # 用例层
| |-- AddUser.js # 添加用户的用例
| `-- ListUsers.js # 列出用户的用例
|-- interface_adapters/ # 接口适配器层
| |-- controllers/ # 控制器
| | `-- UserController.js
| `-- repositories/ # 数据访问对象
| `-- UserRepository.js
`-- frameworks_drivers/ # 框架和驱动器层
|-- webserver/ # Web服务器(例如,Express)
| `-- server.js # 服务器设置
`-- database/ # 数据库接口
`-- InMemoryDB.js # 示例内存数据库
层级关系和相互作用
-
实体层 (
entities/
) :User.js
: 定义了用户的数据结构和基础业务逻辑。例如,它可以定义用户的属性(如姓名、电子邮件)和一些基本的方法(如验证电子邮件格式)。
-
用例层 (
use_cases/
) :AddUser.js
和ListUsers.js
: 实现了添加用户和获取用户列表的业务逻辑。这些用例直接操作实体(例如,创建新的用户实体)并通过接口适配器层与数据存储进行交互。
-
接口适配器层 (
interface_adapters/
) :controllers/UserController.js
: 接收来自外部的请求(如HTTP请求),使用用例层处理这些请求,然后返回响应。repositories/UserRepository.js
: 提供了与数据源交互的方法(如保存用户、获取用户列表)。它抽象了数据存储的细节,用例层通过它与数据存储交互。
-
框架和驱动器层 (
frameworks_drivers/
) :webserver/server.js
: 设置和运行Web服务器(例如,使用Express.js)。它连接控制器以处理传入的HTTP请求。database/InMemoryDB.js
: 一个简单的内存数据库实现,用于存储用户数据。这仅作为示例,实际应用中可能使用真实的数据库系统。
实际代码
这里是一些简化的代码示例,展示了各个层次是如何实现和相互作用的: 实体层 (User.js
) :
javascript
// entities/User.js
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
validateEmail() {
// 简单的邮箱验证逻辑
return /\S+@\S+\.\S+/.test(this.email);
}
}
module.exports = User;
用例层 (AddUser.js
) :
javascript
// use_cases/AddUser.js
const User = require('../entities/User');
class AddUser {
constructor(userRepository) {
this.userRepository = userRepository;
}
execute(userData) {
const user = new User(userData.name, userData.email);
if (!user.validateEmail()) {
throw new Error('Invalid email');
}
return this.userRepository.add(user);
}
}
module.exports = AddUser;
接口适配器层 (UserController.js
) :
javascript
// interface_adapters/controllers/UserController.js
const AddUser = require('../../use_cases/AddUser');
const UserRepository = require('../repositories/UserRepository');
class UserController {
static async addUser(req, res) {
try {
const addUser = new AddUser(new UserRepository());
const user = await addUser.execute(req.body);
res.json(user);
} catch (err) {
res.status(400).json({ error: err.message });
}
}
}
module.exports = UserController;
框架和驱动器层 (server.js
) :
ini
// frameworks_drivers/webserver/server.js
const express = require('express');
const UserController = require('../../interface_adapters/controllers/UserController');
const app = express();
app.use(express.json());
app.post('/users', UserController.addUser);
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
这些代码仅提供了一个基本的框架。在实际应用中,你需要添加错误处理、日志记录、配置管理等其他关键方面。此外,随着应用程序变得更加复杂,你可能需要更精细的层次划分和更多的抽象。
总
在干净的架构中,"抽离"这个词很重要,职责分离,洋葱式结构,只能外部引用内部,内部不能依赖外部。这样外部变化,内部依然不受影响。
实体层:规定user的数据格式
用例层:引用实体层规则,并按照自己的职责,创建多个 user; 当用例层规则变化,user实例层并不会受影响;而user实体层变化,用例层产生的数据也会变化。
外层受控于内层,而内层不受外层影响。
繁杂--->"抽离职责"---->职责分离---->干净的架构
题外话
在遵循干净架构的源代码中,你可能会遇到 domain
和 presentation
这两个目录。它们分别对应干净架构中的特定层级:
-
domain
目录:- 这个目录通常对应于干净架构中的实体层(Entities)。
domain
包含定义业务逻辑和业务规则的类或数据结构。这些是应用程序的核心,代表了业务领域的概念,如用户、订单、产品等。- 这些实体应该是独立于任何特定技术实现的,即它们不应该依赖于数据库、框架或UI的实现细节。
domain
目录中的代码通常包括实体类(Entities)、领域服务(Domain Services)、值对象(Value Objects)、聚合根(Aggregate Roots)等概念。
-
presentation
目录:- 这个目录对应于干净架构中的框架和驱动器层(Frameworks and Drivers),尤其是与用户界面(UI)或API表现层相关的部分。
presentation
层的责任是处理所有与用户接口相关的逻辑。这包括接收用户的输入(如Web请求)、调用适当的用例(Use Cases)、并向用户展示结果(如渲染Web页面或返回API响应)。- 这一层通常包括控制器(Controllers)、视图模型(View Models)、API端点和其他与展示数据相关的逻辑。
在干净架构中,不同的层级应该相互隔离。domain
层(实体层)包含的业务逻辑不应该依赖于 presentation
层的实现,而 presentation
层则负责将 domain
层的数据和逻辑以用户友好的方式呈现。这种结构的好处是,如果需要更改UI技术或框架,可以只修改 presentation
层,而不影响业务逻辑的核心部分。同样,业务规则的变更只会影响 domain
层,而不会影响到展示层。