Clean Architecture:依赖反转与用例驱动设计

FreeGuideOnline 最新 2026-06-12

整洁架构与 SOLID 原则:从依赖反转到用例驱动设计

整洁架构(Clean Architecture)不是某种特定的框架或库,而是一套构建可维护、可测试、与外部技术解耦的软件系统的设计方法。它由 Robert C. Martin 提出,核心思想是让业务逻辑处于系统中心,所有外部依赖(数据库、框架、UI)都成为可替换的插件。为了实现这一点,整洁架构极度依赖 SOLID 原则中的依赖反转原则(DIP),并采用用例驱动设计的方式来组织核心层。

本教程将带你从 SOLID 入手,逐步理解依赖反转如何帮助实现分层解耦,最终掌握如何以用例为中心来设计应用程序。


SOLID 原则回顾与依赖反转原则

整洁架构是 SOLID 原则在系统层面的体现,尤其是其中三个原则至关重要:单一职责原则(SRP)、开闭原则(OCP)和依赖反转原则(DIP)。我们先快速聚焦于 DIP,因为它是整个架构依赖规则的理论基石。

什么是依赖反转原则

依赖反转原则(Dependency Inversion Principle)包含两层含义:

  1. 高层模块不应该依赖低层模块,两者都应该依赖抽象。
  2. 抽象不应该依赖细节,细节应该依赖抽象。

传统分层架构中,业务逻辑层(高层)会直接调用数据访问层(低层),导致高层模块依赖于低层模块。任何对数据库、网络请求的修改,都可能传导到业务代码,带来不可预知的错误。

DIP 提倡我们将这种依赖关系反转:让业务逻辑定义自己需要的接口,而数据访问层、展示层等去实现这些接口。这样,源代码依赖方向就与执行流向相反了。

// 高层模块不直接依赖低层模块,而是依赖抽象
interface UserRepository {
  save(user: User): void;
}

class RegisterUserUseCase {
  constructor(private userRepo: UserRepository) {}
  execute(userData: UserData): User {
    const user = new User(userData);
    this.userRepo.save(user);
    return user;
  }
}

上面的 RegisterUserUseCase 是高层业务逻辑,它只依赖于 UserRepository 接口。实际保存用户的实现可以是数据库、文件系统或内存,只要实现了该接口,就能被注入。这就是依赖反转的直接应用。

DIP 在整洁架构中的角色

整洁架构的“同心圆”分层模型完全建立在 DIP 之上。圆心是业务实体和用例,它是系统中最稳定的部分。从圆心向外,每一层都较少稳定,较多依赖具体技术。依赖规则规定:源代码依赖只能指向圆心方向,外层可以依赖内层,但内层绝不能知道外层的存在。

这就确保了核心业务逻辑永远不会受到框架升级、数据库迁移或 UI 变更的影响。DIP 是实现这一规则的关键技术,因为内外层交互就必须通过接口抽象,而这些抽象由内层定义。


整洁架构的分层与依赖规则

整洁架构将软件划分为多个同心层,每一层都有明确的职责边界。

实体、用例、接口适配器、框架与驱动层

  • 实体(Entities) :包含最核心的业务对象与规则,比如 UserOrder 等,可以拥有验证自身状态的方法。它们完全不依赖任何外部内容。
  • 用例(Use Cases) :某一特定业务场景的自动化编排。例如“用户注册”、“下订单”。用例定义输入输出端口(接口),并协调实体完成业务。它只依赖实体和外部接口的抽象。
  • 接口适配器(Interface Adapters) :负责内外数据的转换。比如控制器、展示器、仓库的具体实现。它们将外部格式的数据(HTTP 请求、数据库行)转换为用例需要的输入,并把用例的输出转换成适合展现或存储的格式。
  • 框架与驱动层(Frameworks & Drivers) :最外层,包括 Web 框架、数据库驱动、UI 库等。它们通过适配器与内层沟通。

依赖方向:由外向内,抽象在中心

依赖只能从外层指向内层。举例:Web 控制器调用用例,用例调用实体,这是执行方向。但源代码依赖上,控制器知道用例接口,用例不知道控制器;用例知道仓库接口,仓库实现知道用例定义的接口,但用例不知道具体实现。这就是依赖反转的威力。

这种布局带来的直接好处是:当你需要更换数据库时,只需新增一个实现了仓库接口的适配器,核心层代码无需任何修改。同样,换成命令行 UI 或测试框架,也只需新增对应的控制器/驱动。


用例驱动设计:从业务逻辑出发

传统开发中,我们经常从数据库表结构或 API 端点开始设计。整洁架构反其道而行,要求先明确系统要做什么,即从用例开始。

用例是什么

用例描述了系统在特定业务场景下的行为,它接受输入,执行业务规则,产生输出。用例是应用程序特有的,它不关心数据如何持久化或如何呈现给用户。一个用例对应一个确定性的业务动作,比如创建文章、查询订单历史等。

用例与输入输出端口

整洁架构中,用例通过输入端口输出端口与外界交互。输入端口是一个接口,定义了用例的执行方法;输出端口也是由用例定义的接口,供它调用以获取或推送数据。

// 输入端口
interface CreatePostInput {
  title: string;
  content: string;
  authorId: string;
}

// 输出端口(由用例定义,外层实现)
interface PostRepository {
  save(post: Post): void;
}

// 用例本身
class CreatePostUseCase {
  constructor(private postRepo: PostRepository) {}

  execute(input: CreatePostInput): Post {
    const post = new Post(input.title, input.content, input.authorId);
    // 可加入业务规则,如标题长度校验
    this.postRepo.save(post);
    return post;
  }
}

这样做的好处是,用例完全不关心 PostRepository 是连接 MySQL 还是调用 REST API,它只关心自己需要的业务抽象。

依赖反转实践:让高层模块定义接口

注意在上述代码中,PostRepository 接口是由用例所在的层定义的,而不是由数据库实现层定义。这正是 DIP 的体现:高层模块不依赖低层模块,而是低层模块实现高层定义的接口。这样,依赖关系自然向内,确保了用例的独立性和可测试性。你可以在测试中用一个内存实现来替代真实的仓库,而不必连接任何外部服务。


结合其他 SOLID 原则构建可维护架构

整洁架构充分利用 SOLID 原则,让每一部分都职责清晰、易于扩展。

单一职责原则与用例边界

单一职责原则(SRP)经常被误读为“一个类只做一件事”。在架构层面,SRP 指的是一个模块应该有且只有一个引起它变化的原因。用例恰好天然符合这一原则:每个用例封装了“一个理由”的变化。比如,排版规则改变只会影响“排版文章”用例,而不会波及“发布文章”用例。通过用例来划分组件,系统更容易维护。

开闭原则与插件式架构

OCP 要求软件实体对扩展开放,对修改关闭。整洁架构通过稳定的核心抽象(实体、用例接口)和可替换的外层适配器实现了插件式结构。新增支付方式时,你只需要添加一个新的支付网关适配器并注入用例,核心业务代码原封不动。这正是 OCP 的最佳实践。

里氏替换与接口隔离

里氏替换原则(LSP)保证子类型可以透明地替换父类型。在用例依赖接口时,任何实现了仓库接口的适配器(无论是数据库、API 还是内存)都应该能无缝替代,这对测试至关重要。

接口隔离原则(ISP)要求不强制客户端依赖它们不用的接口。在用例设计中,输出端口应该按需定义,而不是一个大而全的 Repository:一个“查询文章列表”用例只需要 findAll 方法,而“保存文章”用例只需要 save 方法。通过将接口分离,我们避免了不必要耦合。


实战:构建一个博客文章的用例

让我们通过一个完整的“创建博客文章”例子,把这些概念串联起来。

定义实体与用例接口

首先定义核心实体 Post 和它的简单业务规则:

class Post {
  constructor(
    public readonly title: string,
    public readonly content: string,
    public readonly authorId: string,
    public readonly id?: string, // 由数据库生成
    public readonly createdAt?: Date
  ) {
    if (title.length < 5) {
      throw new Error('标题至少需要5个字符');
    }
  }
}

接着定义用例需要的输入、输出端口:

// 输入数据结构
interface CreatePostRequest {
  title: string;
  content: string;
  authorId: string;
}

// 用例依赖的仓库接口(由用例层定义)
interface PostRepository {
  save(post: Post): Promise<Post>;
}

// 用例本身
class CreatePostUseCase {
  constructor(private postRepo: PostRepository) {}

  async execute(request: CreatePostRequest): Promise<Post> {
    const post = new Post(request.title, request.content, request.authorId);
    return this.postRepo.save(post);
  }
}

至此,核心业务层结束,它不依赖任何框架或数据库。

实现用例与数据仓储接口

外层适配器将负责实现 PostRepository。假设我们使用 TypeORM 的 PostgreSQL 实现:

class PostgresPostRepository implements PostRepository {
  async save(post: Post): Promise<Post> {
    // 将业务实体转换为数据库实体并保存
    const dbEntity = await db.save({
      title: post.title,
      content: post.content,
      authorId: post.authorId,
    });
    return new Post(dbEntity.title, dbEntity.content, dbEntity.authorId, dbEntity.id, dbEntity.createdAt);
  }
}

注意,这里数据库实现依赖于用例层定义的接口,依赖方向正确。

控制器与展示器:适配层的依赖注入

在接口适配器层,我们编写一个 HTTP 控制器,它接受请求,构造输入对象,并调用用例:

class CreatePostController {
  constructor(private useCase: CreatePostUseCase) {}

  async handle(httpRequest: { body: any }): Promise<{ status: number; body: any }> {
    try {
      const post = await this.useCase.execute(httpRequest.body);
      return { status: 201, body: post };
    } catch (error) {
      return { status: 400, body: { message: error.message } };
    }
  }
}

最后在应用启动时,我们将依赖组装起来:

const postRepo = new PostgresPostRepository();
const useCase = new CreatePostUseCase(postRepo);
const controller = new CreatePostController(useCase);

// 框架路由挂载 controller.handle

通过这种组装,所有依赖都向内指向核心用例,框架和数据库彻底变成了可替换的插件。


总结与最佳实践

整洁架构与 SOLID 原则的结合使得软件具备以下关键能力:

  • 业务逻辑独立于框架,测试成本极低,无需启动服务器或数据库。
  • 极高的可维护性,每个用例都是一个独立的变更单元,边界清晰。
  • 技术选型后置与可更换,架构允许你先集中精力在业务上,后续轻松切换技术栈。

实践中需要注意:

  • 不要过度设计:简单的 CRUD 应用可能不需要完整的用例分层,但如果业务复杂且会持续演化,整洁架构的价值会逐步显现。
  • 始终让用例定义接口,而不是让接口适应技术实现。
  • 确保跨层通信只通过 DTO 和接口,不泄漏任何数据库结构或框架细节到内层。
  • 测试优先:利用依赖注入和内层纯逻辑,为每个用编写单元测试。

整理架构不是一个一次性活动的设计,而是一门在系统演进中时刻遵守的原则。掌握依赖反转与用例驱动的思想,你将有能力让代码与业务价值保持同频,避免陷入技术债务的泥潭。