【译】The Clean Architecture

【译】The Clean Architecture
最新回答
亡命天涯不如早点回家

2026-06-25 00:34:09

《整洁架构》

作者:Robert C. Martin (Uncle Bob)

原文链接:

The Clean Architecture

译者:时序

在过去几年中,我们见证了许多关于系统架构的构想,其中包括:

  • Alistair Cockburn 的六边形架构(也称为端口与适配器),被 Steve Freeman 和 Nat Pryce 在其著作《Growing Object Oriented Software》(
    购买链接
    )中采用。
  • Jeffrey Palermo 的洋葱架构 (
    相关链接
    )。
  • 去年某篇博客中提到的尖叫架构 (
    相关链接
    )。
  • James Coplien 与 Trygve Reenskaug 的 DCI 架构 (
    购买链接
    )。
  • Ivar Jacobson 的著作《Object Oriented Software Engineering: A Use-Case Driven Approach》中的 BCE 架构 (
    购买链接
    )。

尽管这些架构在细节上存在差异,但它们在本质上是相似的。它们都旨在隔离关注点,并通过将软件分层来实现这一目标。每个架构都至少包含一层业务规则和另一层作为接口。

这些架构所构建的系统具有以下特点:

  • 独立于框架:架构不依赖于特定类库的特性,使得框架可以像工具一样被使用,而不会对系统造成约束。
  • 可测试性:业务规则可以脱离用户界面(UI)、数据库、Web 服务器或其他外部元素进行测试。
  • 独立的用户界面(UI):UI 可以轻松更换,而无需修改系统的其他部分。例如,Web UI 可以替换为控制台 UI,而业务规则保持不变。
  • 独立的数据库:可以轻松更换数据库,如从 Oracle 或 SQL Server 切换到 Mongo、BigTable、CouchDB 等。业务规则不与特定数据库绑定。
  • 独立的外部代理:业务规则对外部世界一无所知。

本文试图将上述所有架构整合为一个可行的整体构想。

依赖规则

同心圆代表软件的不同部分。一般来说,越往外部,软件的级别越低,主要是机制;越往内部,软件的级别越高,主要是策略。

使这个架构运作的核心规则是依赖规则,它规定源代码依赖只能向内指向。内部圆不应了解任何外部圆的信息。在实践中,这意味着外部圆中声明的名称(如函数、类、变量等软件实体)不应被内部圆的代码提及。

同样,外部圆使用的数据格式不应被内部圆使用,特别是当这些格式是由外部圆使用的框架生成时。我们应避免外部圆的影响渗透到内部圆。

实体

实体封装了企业域范围内的业务规则。它们可以是带有方法的对象,也可以是一组数据结构和函数。只要企业内的不同应用能够使用这些实体即可。

如果你不是在企业级开发,而是构建一个单体应用,那么这些实体就是应用的业务对象。它们封装了最通用和高层的规则,且当外部发生变化时,它们基本保持不变。例如,你不会期望这些对象因页面导航或安全方面的变化而改变。任何特定应用的操作都不应影响实体层。

用例

这一层的软件包含特定应用的业务规则。它封装并实现了系统的所有用例,组织实体中的数据流,并指挥这些实体利用它们的企业域业务规则来实现用例的目标。

我们不期望这一层影响实体,也不希望它在数据库、UI 或其他常用框架等外部变化时受到影响。这一层隔离了上述关注点。

当然,我们期望应用操作的变化会影响用例,进而影响这一层的软件。如果用例的细节发生变化,那么这一层的代码也会受到影响。

接口适配器

这一层的软件由一组适配器组成,它们将数据从用例和实体最合适的格式转换为适合外部设施(如数据库或网站)的格式。例如,这一层包含 GUI 的 MVC 架构,其中 Presenters、Views 和 Controllers 都属于这一层。模型基本上是从 Controllers 传递到用例的数据结构,并从用例返回到 Presenters 和 Views。

类似地,数据在这一层被转换,从适合实体和用例的结构变为适合持久层框架使用的结构。这圈内的代码不应了解数据库的具体实现。如果数据库是 SQL 数据库,那么所有 SQL 语句都应在这一层内,特别是与数据库相关的部分。

这一层的其他适配器还需要将数据从外部服务的结构转换为用例和实体使用的内部结构。

框架与驱动

最外层主要包含数据库、网络框架等框架和工具。在这一层,你除了编写一些与内层通信的胶水代码外,基本不会有其他代码。

这一层是所有细节存在的地方。网络是细节,数据库是细节。我们将这些东西放在外部,以确保它们不会影响其他部分。

只有四个圈?

实际上,圆圈的数量并不固定为四个,它们只是示意。你可能需要根据实际需求增加或减少圆圈的数量。依赖规则始终适用:源代码依赖始终指向内部。当你向内部移动时,抽象层次增加。最外部的圆代表低层具体细节,而向内移动时,软件变得更加抽象,并封装了更高一级的策略规则。最内部的圆代表最普遍的抽象层次。

跨越边界

图的右下方展示了穿越圆圈边界的示例。它展示了 Controller 和 Presenter 如何与下一层的用例进行通信。注意控制流:它从 Controller 出发,穿过用例,然后在 Presenter 中执行。同时,注意源代码依赖:它们都指向内部的用例。

我们通常使用依赖反转原则来解决这个明显的问题。在 Java 这样的语言中,我们会整理源代码依赖与控制流相反的接口和继承关系,以确保它们从边界正确穿越。

例如,用例需要调用 Presenter,但这个调用不能直接进行,因为这会违反依赖规则(外圈的名称不能被内圈提及)。因此,我们的用例调用内圈的一个接口(在这个例子中是 Use Case Output Port),并让外圈的 Presenter 实现它。

架构里的所有边界穿越都使用这个技巧。我们使用动态多态来创建与控制流相反的源代码依赖,以确保无论控制流的方向如何,都不会违反依赖规则。

什么样的数据会穿越边界

通常,穿越边界的数据是简单数据结构,如基本结构、简单的数据传输对象(DTO)、可以方便进行函数赋值的数据,或者打包进哈希表或组装成对象的数据。重要的是,穿越边界的是隔离、简单的数据结构。我们应避免传递实体或数据库行数据等可能违反依赖规则的数据结构。

例如,许多数据库框架在查询后返回一个方便的数据格式(我们可以称之为 RowStructure)。我们不应将这个行结构通过边界传递给内部的圈,因为这会导致内部圈需要了解外部圈的内容,进而违反依赖规则。

因此,当我们在边界传递数据时,应确保其格式适合内部圈。

结论

遵从这些简单规则并不难,并且能帮助你减少未来可能出现的问题。通过将软件隔离分层并遵从依赖规则,你可以构建一个真正可测试的系统,并享受上述所有好处。当系统的任何外部部分(如数据库或 Web 框架)过时时,你可以轻松地替换这些过时的元素。