【译】The Clean Architecture
目录
引子
基于 NodeJS and Good Practices 想尝试下分层,在做的时候发现另外一个类似的分层,继续翻译记录。
正文
在过去的几年里,我们看到了一系列关于系统架构的想法。其中包括:
- Alistair Cockburn 的 Hexagonal Architecture(又称 Ports and Adapters),并在 Steve Freeman 和 Nat Pryce 的精彩著作 Growing Object Oriented Software 中采用
- Jeffrey Palermo 的 Onion Architecture
- 我去年的一篇文章 Screaming Architecture
- 出自 James Coplien 和 Trygve Reenskaug 的 DCI
- 出自 Ivar Jacobson 著作 Object Oriented Software Engineering: A Use-Case Driven Approach 的[BCE][url9]
尽管这些架构在细节上都有所不同,但它们非常相似。它们都有相同的目的,即关注点分离。它们都通过将软件划分为不同的层来实现这种分离。每个至少有一层用于业务规则,另一层用于接口。
这些体系结构中的每一个都产生了以下体系:
- 独立于框架。架构并不依赖于某个功能丰富的软件库。这允许你将这些框架当作工具,而不必将系统塞进它们有限的约束中。
- 可测试的。业务规则可以在没有 UI 、数据库、Web 服务器或任何其它外部元素的情况下进行测试。
- 独立于 UI 。UI 可以很容易地更改,而无需更改系统的其余部分。例如,可以用控制台 UI 替换 Web UI ,而无需更改业务规则。
- 独立于数据库。你可以将 Oracle 或 SQL Server 换成 Mongo、BigTable、CouchDB 或其它东西。你的业务规则没有跟数据库绑定。
- 独立于任何外部机构。事实上,你的业务规则根本就不知道外部世界的任何事情。
本文顶部的图示尝试将所有这些架构集成到一个可操作的想法中。
依赖规则(The Dependency Rule)
同心环代表软件的不同区域。一般来说,你走得越深入,软件的层次就越高。The outer circles are mechanisms. The inner circles are policies.
使这个架构运行的最重要规则是 依赖规则 。这个规则是 源代码依赖 只能指向 内部 。内环的任何东西都不可能知道外环的任何东西。特别是,在外环中声明某些事物的名称不能被内环中的代码提及。这些包括函数、类、变量或任何其它命名的软件实体。
同样,外环中使用的数据格式不应该被内环使用,特别是如果这些格式是由外环中的框架生成的。我们不希望外环的任何东西对内环产生影响。
实体(Entities)
实体封装了 规划范围(enterprise wide) 的业务规则。实体可以是具有方法的对象,也可以是一组数据结构和函数。只要实体可以被规划中的许多不同应用程序使用,这就无关紧要了。
如果你没有规划(enterprise),只是编写单个应用程序,那么这些实体就是应用程序的业务对象。它们封装了最一般和最高级的规则。当外部事物发生变化时,它们最不可能发生变化。例如,你不希望这些对象受到页面导航或安全性更改的影响。对任何特定应用程序的操作更改都不应影响实体层。
用例(Use Cases)
这层中的软件包含 特定于应用程序 的业务规则。它封装并实现了系统的所有用例。这些用例编排了进出实体的数据流,并指导这些实体使用其 规划范围 的业务规则来实现用例的目的。
我们不希望此层中的更改影响实体。我们也不希望这一层受到外部因素(如数据库、UI 或任何公共框架)变化的影响。这一层与这些问题无关。
但是,我们确实希望对应用程序操作的更改 会 影响用例,从而影响该层中的软件。如果用例的细节发生了变化,那么这层中的一些代码肯定会受到影响。
接口适配器(Interface Adapters)
这层中的软件是一组适配器,用于将最方便适用于用例和实体的数据格式,转换为最方便适用于数据库或 Web 等外部机构的格式。例如,正是这一层将完全包含 GUI 的 MVC 架构。Presenters、Views、Controllers 都属于这里。这个模型就像是从 Controllers 传递到用例的数据结构,然后从用例返回到 Presenters 和 Views 。
类似地,在这层中,数据会转换,从对于实体和用例最方便的形式转换为对于持久化框架(例如数据库)最方便的形式。这个环里的任何代码都不应该知道数据库的任何信息。如果数据库是 SQL 数据库,那么所有 SQL 都应该限制在这一层,特别是这一层中与数据库有关的部分。
这层也是用于将数据从一些外部表单(如外部服务)转换为用例和实体使用的内部表单的适配器。
框架和驱动因素(Frameworks and Drivers)
最外层通常由框架和工具组成,如数据库、Web 框架等。一般来说,除了与向内的下一个环通信的粘合代码外,你不会在这一层编写太多代码。
这一层是所有细节的所在。Web 是一个细节。数据库是一个细节。我们把这些东西放在外面,这样它们尽量小的造成伤害。
只有 4 个环?
不是的,圆环只是示意图。你可能会发现你需要的不仅仅是这四个。没有规则说你必须只有这四个。但是,依赖规则 始终适用。源代码依赖总是向内。当你向内移动时,抽象的层次会增加。最外面的圆环是低层次的具体细节。当你向内移动时,软件变得更加抽象,并且封装了更高级别的策略。最里面的圆是最通用的。
跨越边界
在图示的右下角是一个如何跨越边界的例子。它展示了 Controllers 和 Presenters 与下一层的用例进行通信。注意控制流(flow of control)。它从 Controllers 开始,穿过用例,然后在 Presenters 中执行。还要注意源代码的依赖关系。它们中的每一个都向内指向用例。
我们通常使用 Dependency Inversion Principle 来解决这个明显的矛盾。例如,在像 Java 这样的语言中,我们会安排接口和继承关系,使得源代码依赖关系在边界的正确点上与控制流相反。
例如,考虑用例需要调用 presenter 。但是,这个不能是直接调用,因为这将违反 依赖规则 :外环中的任何名称都不能被内环提及。因此,我们让用例在内环调用一个接口(这里就是图示中的 Use Case Output Port),并让外环的 presenter 实现它。
同样的技术用于跨越架构中的所有边界。我们利用动态多态性来创建与控制流相反的源代码依赖关系,这样无论控制流朝哪个方向,我们都可以遵循 依赖规则 。
什么数据跨越边界
通常,跨越边界的数据是简单的数据结构。如果你愿意,可以使用基本结构或简单的数据传输对象。或者数据可以只是函数调用中的参数。或者你可以把放入一个散列表(hashmap),或者把它构造成一个对象。重要的是跨边界传递的数据结构是独立的、简单的。我们不想欺骗和传递 实体 或数据库行。我们不希望数据结构有任何违反 依赖规则 的依赖关系。
例如,许多数据库框架返回一种方便的数据格式来响应查询。我们可以称之为行结构(RowStructure)。我们不想让行结构越过边界向内传递。这将违反 依赖规则 ,因为这将迫使内环知道外环的一些事情。
因此,当我们跨越边界传递数据时,它总是以最方便内环的形式传递。
结论
遵守这些简单的规则并不难,而且会为你今后省去很多麻烦。通过将软件分为多个层,并遵循 依赖规则 ,你将创建一个本质上可测试的系统,其中包含所有的好处。当系统的任何外部部分(如数据库或 web 框架)淘汰时,你能以最小代价替换这些淘汰的元素。