理解干净的架构

干净的架构概述

"干净的架构"(Clean Architecture)是一种软件设计理念,由著名的软件工程师罗伯特·C·马丁(Robert C. Martin,常被称为Uncle Bob)提出。这种架构模式的目的是创建一个易于维护、测试、扩展和部署的系统。它强调关注点分离(Separation of Concerns)的重要性,并鼓励将软件分解成互不依赖的模块。

干净的架构包含以下关键特性:

  1. 独立于框架(Framework-Independent) :系统的业务规则不应该依赖于存在的任何一个框架,这样可以避免业务逻辑被框架所限制。
  2. 可测试性(Testable) :业务规则可以在没有UI、数据库、Web服务器或任何外部元素的情况下进行测试。
  3. UI独立性(UI-Independent) :UI可以轻松地更改,而不会影响系统的其余部分。
  4. 数据库独立性(Database-Independent) :业务规则不依赖于数据库,所以可以轻松更换数据库。
  5. 外部代理独立性(External Agency Independence) :业务规则不知道外部世界(比如:Web或其他应用程序)。

干净的架构通常包括以下四个层次,从内到外分别是:

  • 实体(Entities) :包含企业的业务规则。实体可以是一个带有方法的对象或一组数据结构和函数。
  • 用例(Use Cases) :包含应用程序的业务规则。它封装了与实体的交互。
  • 接口适配器(Interface Adapters) :将数据在用例和实体之间以及数据库和外部代理(比如Web)之间转换。
  • 框架和驱动器(Frameworks and Drivers) :包括UI、数据库、Web等。

这个架构的一个关键特点是控制流的方向,它总是指向内部。最外层(框架和驱动器)依赖于内层(用例和实体),但内层完全不依赖于外层。这种依赖规则确保了业务逻辑的独立性和可重用性。

关键特性

  1. 独立于框架(Framework-Independent)

    • 在干净的架构中,系统的业务规则不依赖于任何特定的框架。这意味着框架可以作为工具来使用,而不是系统设计的基础。
    • 优势在于当新的、更好的框架出现时,或者当前框架不再适用时,可以更容易地进行替换,而不影响业务逻辑的核心部分。
    • 这也意味着业务逻辑应该足够抽象,从而可以在不同的环境中运行,不论是桌面应用程序、Web应用程序还是移动应用程序。
  2. 可测试性(Testable)

    • 干净的架构强调业务规则应该能够独立于UI、数据库、网络等外部因素进行测试。
    • 这通常通过使用依赖注入、接口抽象和模拟对象等技术来实现。这样可以在不同的测试环境下对核心逻辑进行测试,而不需要复杂的设置。
    • 可测试性是维护和扩展系统时的关键因素,因为它确保了对代码更改的影响可以迅速和准确地评估。
  3. UI独立性(UI-Independent)

    • 干净的架构允许UI独立于业务逻辑。这意味着UI可以随着时间的推移而变化,而不影响系统的核心部分。
    • 这种分离使得可以实现多种不同的用户界面(例如,Web界面、移动应用界面、桌面应用界面),而核心业务逻辑保持不变。
    • 这也简化了UI的设计和测试,因为它可以独立于业务规则进行开发和修改。
  4. 数据库独立性(Database-Independent)

    • 在干净的架构中,业务规则与数据库技术解耦。这意味着可以更换底层数据库而不影响业务逻辑。
    • 数据访问逻辑通常通过抽象层实现,例如仓储模式(Repository Pattern),从而使应用程序与特定的数据库实现细节隔离。
    • 这种独立性有助于处理不同的存储需求,例如迁移至新的数据库平台或使用不同类型的数据库(如关系型数据库与NoSQL数据库)。
  5. 外部代理独立性(External Agency Independence)

    • 这意味着业务逻辑不直接与外部世界交互,例如网络服务、文件系统或第三方API。
    • 外部交互通过接口抽象和中间层进行,这样业务逻辑就不会受到外部变化的直接影响。
    • 这种设计有助于创建更稳定、更容易维护的系统,因为外部系统的变化不会直接影响到核心业务逻辑。

总体而言,干净的架构旨在通过这些关键特性实现高度解耦和模块化的系统,使其更加灵活、可维护和可扩展。

四个层级

干净架构中的四个层级是其设计的核心,这些层级从内向外分别是:

  1. 实体(Entities)

    • 实体代表了企业级的业务规则。它们可以是具有方法的对象(面向对象编程中的实体)或一组数据结构和函数(在更功能性的编程风格中)。
    • 这些实体封装了最通用和高级的规则。例如,在一个电商系统中,实体可能包括产品、订单、客户等核心概念及其相关的业务规则。
    • 实体应该是独立于任何特定用例的,这意味着它们的实现不应该被UI、框架、数据库等方面所影响。
  2. 用例(Use Cases)

    • 用例层包含应用程序特定的业务规则。它封装了系统应如何使用实体来执行特定的业务操作。
    • 这一层处理应用程序的逻辑,例如用户创建订单、修改个人信息等操作的具体细节。
    • 用例层位于实体层的外部,直接与实体层交互,但通过接口与外层交互,保持与特定的UI、数据库和外部接口的独立。
  3. 接口适配器(Interface Adapters)

    • 接口适配器层作为连接内部层(实体和用例)和外部层(框架和驱动器)的桥梁。
    • 这一层包括了格式化数据以供内部层使用(例如,将HTTP请求数据转换为用例层可以使用的形式),以及将数据从用例层格式化以供外部层使用(例如,将数据转换为HTTP响应)。
    • 接口适配器层通常包括控制器、视图、呈现器和数据访问对象等。它们将信息从外部世界转换为内部层所需的格式,反之亦然。
  4. 框架和驱动器(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  # 示例内存数据库

层级关系和相互作用

  1. 实体层 (entities/) :

    • User.js: 定义了用户的数据结构和基础业务逻辑。例如,它可以定义用户的属性(如姓名、电子邮件)和一些基本的方法(如验证电子邮件格式)。
  2. 用例层 (use_cases/) :

    • AddUser.jsListUsers.js: 实现了添加用户和获取用户列表的业务逻辑。这些用例直接操作实体(例如,创建新的用户实体)并通过接口适配器层与数据存储进行交互。
  3. 接口适配器层 (interface_adapters/) :

    • controllers/UserController.js: 接收来自外部的请求(如HTTP请求),使用用例层处理这些请求,然后返回响应。
    • repositories/UserRepository.js: 提供了与数据源交互的方法(如保存用户、获取用户列表)。它抽象了数据存储的细节,用例层通过它与数据存储交互。
  4. 框架和驱动器层 (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实体层变化,用例层产生的数据也会变化。

外层受控于内层,而内层不受外层影响。

繁杂--->"抽离职责"---->职责分离---->干净的架构

题外话

在遵循干净架构的源代码中,你可能会遇到 domainpresentation 这两个目录。它们分别对应干净架构中的特定层级:

  1. domain 目录

    • 这个目录通常对应于干净架构中的实体层(Entities)。
    • domain 包含定义业务逻辑和业务规则的类或数据结构。这些是应用程序的核心,代表了业务领域的概念,如用户、订单、产品等。
    • 这些实体应该是独立于任何特定技术实现的,即它们不应该依赖于数据库、框架或UI的实现细节。
    • domain 目录中的代码通常包括实体类(Entities)、领域服务(Domain Services)、值对象(Value Objects)、聚合根(Aggregate Roots)等概念。
  2. presentation 目录

    • 这个目录对应于干净架构中的框架和驱动器层(Frameworks and Drivers),尤其是与用户界面(UI)或API表现层相关的部分。
    • presentation 层的责任是处理所有与用户接口相关的逻辑。这包括接收用户的输入(如Web请求)、调用适当的用例(Use Cases)、并向用户展示结果(如渲染Web页面或返回API响应)。
    • 这一层通常包括控制器(Controllers)、视图模型(View Models)、API端点和其他与展示数据相关的逻辑。

在干净架构中,不同的层级应该相互隔离。domain 层(实体层)包含的业务逻辑不应该依赖于 presentation 层的实现,而 presentation 层则负责将 domain 层的数据和逻辑以用户友好的方式呈现。这种结构的好处是,如果需要更改UI技术或框架,可以只修改 presentation 层,而不影响业务逻辑的核心部分。同样,业务规则的变更只会影响 domain 层,而不会影响到展示层。

相关推荐
ai小鬼头2 小时前
AIStarter如何助力用户与创作者?Stable Diffusion一键管理教程!
后端·架构·github
掘金-我是哪吒5 小时前
分布式微服务系统架构第156集:JavaPlus技术文档平台日更-Java线程池使用指南
java·分布式·微服务·云原生·架构
国服第二切图仔5 小时前
文心开源大模型ERNIE-4.5-0.3B-Paddle私有化部署保姆级教程及技术架构探索
百度·架构·开源·文心大模型·paddle·gitcode
SelectDB6 小时前
SelectDB 在 AWS Graviton ARM 架构下相比 x86 实现 36% 性价比提升
大数据·架构·aws
weixin_437398217 小时前
转Go学习笔记(2)进阶
服务器·笔记·后端·学习·架构·golang
liulilittle7 小时前
SNIProxy 轻量级匿名CDN代理架构与实现
开发语言·网络·c++·网关·架构·cdn·通信
喷火龙8号8 小时前
深入理解MSC架构:现代前后端分离项目的最佳实践
后端·架构
Codebee8 小时前
“自举开发“范式:OneCode如何用低代码重构自身工具链
java·人工智能·架构
掘金-我是哪吒8 小时前
分布式微服务系统架构第158集:JavaPlus技术文档平台日更-JVM基础知识
jvm·分布式·微服务·架构·系统架构
JohnYan9 小时前
模板+数据的文档生成技术方案设计和实现
javascript·后端·架构