Clean Architecture
本文翻译自Robert C. Martin的博文The Clean Architecture
在过去的几年里,我们已经看到了一系列关于系统架构的想法,这些包括:
尽管这些架构在细节上都有所不同,但它们非常相似。它们都有相同的目标,即关注点分离。他们都通过将软件分层来实现这种分离,这些架构里面至少有一个业务层,其他都是接口层。
这些架构都有以下特性:
- 框架独立性,不依赖于特定的软件库框架,这样你可以像工具一样使用这种框架,而不需要让你的系统受到它的约束条件;
- 可测试性,可以在没有 UI、数据库、Web 服务器或任何其他外部依赖的情况下可以测试业务规则;
- UI独立性,UI 可以轻松更改,而无需更改系统的其余部分。例如,可以用控制台 UI 替换 Web UI,而无需更改业务规则;
- 数据库独立性,可以将 Oracle 或 SQL Server 换成 Mongo、BigTable、CouchDB 或其他东西,业务规则未绑定到数据库;
- 外部依赖独立性,事实上业务逻辑代码不应该和外部具体实现打交道;
The Dependency Rule
同心圆代表软件的不同领域,一般来说,越往外圈软件的层次就越高,外圈是机制,内圈是政策。
使这个整洁架构工作的最重要的规则是依赖规则。这个规则说明了源代码依赖只能向内。内圈中结构不需要知道外圈具体实现,特别是,在外圈中声明的事物的名称不能被内圈中的代码提及。这包括函数、类、变量或任何任何实体信息。
Entities
Entity封装了企业范围的业务规则。实体可以是具有方法的对象,也可以是一组数据结构和函数。
如果你不是企业级,只是编写单个应用程序,那么这些实体就是应用程序的业务对象。它们封装了最通用和最高级的规则。当外部事物发生变化时,它们最不可能发生变化。例如,不会期望这些对象会受到页面导航或安全性更改的影响。对任何特定应用程序的操作更改都不应影响实体层。
Use Cases
该层中的软件包含特定于应用程序的业务规则。它封装并实现了系统的所有用例。这些用例协调进出实体的数据流,并指导这些Entity使用其企业范围的业务规则来实现用例(Use cases)的目标。
我们预计这一层的变化不会影响实体。我们也不希望这一层受到外部变化的影响,例如数据库、UI 或任何常见框架。该层与此类问题隔离。
当然我们期望对于应用操作的变化会影响用例而进一步影响到这层的软件。 如果一个用例的细节变化了,那么这层的代码肯定也会被影响。
Interface Adapters
该层中的软件是一组适配器,可将数据从最适合用例和实体的格式转换为最适合某些外部机构(如数据库或 Web)的格式。例如,正是这一层将完全包含 GUI 的 MVC 架构。Presenters、Views 和 Controllers 都属于这里。模型可能只是从控制器传递到用例,然后从用例返回到演示者和视图的数据结构。
类似地,在这一层中,数据从对实体和用例最方便的形式转换为对正在使用的任何持久性框架最方便的形式。即数据库。这个圈子内的任何代码都不应该对数据库有任何了解。如果数据库是 SQL 数据库,那么所有的 SQL 都应该限制在这一层,特别是限制在这一层与数据库有关的部分。
在这一层中还有任何其他适配器,用于将数据从某种外部形式(例如外部服务)转换为用例和实体使用的内部形式。
Frameworks and Drivers.
最外层一般由框架和工具组成,如数据库、Web 框架等。通常在这一层你不会写太多代码,除了与下一个循环往内通信的胶水代码。
这一层是所有细节的所在。网络是一个细节。数据库是一个细节。我们把这些东西放在外面,它们不会造成什么伤害。
Only Four Circles?
不,圆圈是示意性的。你可能会发现需要的不仅仅是这四个。没有规则说你必须总是只有这四个。但是,依赖规则始终适用。源代码依赖项总是指向内部。随着向内移动,抽象级别会增加。最外圈是低层次的具体细节。随着向内移动,软件变得更加抽象,并封装了更高级别的策略。最内圈是最一般的。
Crossing boundaries.
在图的右下方是我们穿越圆圈边界的示例。它展示了Controller和Presenter与下一层的用例进行通信。注意控制流。它从controller出发,穿过用例,然后在presenter里执行。也注意下源码依赖。它们每个都指向内部的用例。
我们通常使用依赖反转原则解决这个明显的问题。在java这样的语言中,我们会整理源码依赖与控制流相反的接口和继承关系,让它们从边界正确的穿过。
例如,用例需要调用presenter。但是,这个调用不能直接进行因为会违反依赖规则。外圈的名字不能被内圈提到。所以我们的用例调用内圈的一个接口(在这个例子里是Use Case Output Port),并让外圈的presenter实现它。
架构里所有的边界穿越都用这个技巧。我们使用动态多态来创建与控制流相反的源码依赖,以便于无论在控制流的任何方向都不会违反依赖规则。
What data crosses the boundaries.
正常来说穿过边界的数据是简单数据结构。你可以使用基本结构或简单的Data Transfer 对象。或者可以方便的进行函数赋值的数据。或者你可以打包进一个hashmap,或者将它组装成一个对象。重要的是穿过边界的是隔离,简单的数据结构。我们不想搞变通传递实体或数据库行数据。我们不想数据结构有任何违反依赖规则的依赖。
例如,很多数据库框架在查询后返回一个方便的数据格式。我们可以叫它RowStructure(行结构)。我们不想将这个行机构通过边界传递给内部的圈。这会导致内部圈需要知道外部圈的内容进而违反依赖规则。
所以当我们在边界传递数据是,要注意其应该是内部圈的格式。
Conclusion
遵守这些简单的规则并不难,并且会省去很多麻烦。通过将软件分层并遵守依赖规则,创建一个本质上可测试的系统,并具有所有暗示的好处。当系统的任何外部部件(如数据库或 Web 框架)过时时,可以轻松替换那些过时的元素。