理解干净的架构

干净的架构概述

"干净的架构"(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 层,而不会影响到展示层。

相关推荐
梦中的天之酒壶1 天前
多级缓存架构
缓存·架构
眠りたいです1 天前
基于脚手架微服务的视频点播系统-数据管理与网络通信部分的预备工作
c++·qt·ui·微服务·云原生·架构·媒体
虫小宝1 天前
返利软件的分布式缓存架构:Redis集群在高并发场景下的优化策略
分布式·缓存·架构
一水鉴天1 天前
整体设计 之 绪 思维导图引擎 之 引 认知系统 之 引 认知系统 之 序 认知元架构 之6 拼句 之1 (豆包助手 之8)
架构·认知科学
纪元A梦1 天前
Redis最佳实践——安全与稳定性保障之高可用架构详解
redis·安全·架构
Dontla1 天前
流行的前端架构与后端架构介绍(Architecture)
前端·架构
熊文豪1 天前
KingbaseES读写分离集群架构解析
数据库·架构·kingbasees·金仓数据库·电科金仓
往事随风去1 天前
别再纠结了!IM场景下WebSocket和MQTT的正确选择姿势,一文讲透!
后端·websocket·架构
爱读源码的大都督1 天前
为什么Spring 6中要把synchronized替换为ReentrantLock?
java·后端·架构
一水鉴天1 天前
整体设计 之 绪 思维导图引擎 之 引 认知系统 之 引 认知系统 之 序 认知元架构 之 元宇宙:三种“即是”逻辑与数据安全措施的适配(豆包助手 之10)
架构·认知科学