Clean Architecture
整洁架构
Clean Architecture(整洁架构)(也称为洋葱架构或六边形架构或垂直切片架构)
一系列关于系统架构的想法:
- Hexagonal(六边形) Architecture(又名端口和适配器)来自 Alistair Cockburn 的并被 Steve Freeman 和 Nat Pryce 在他们的精彩著作《Growing Object Oriented Software 》中采用
- Onion Architecture(洋葱建筑) - Jeffrey Palermo
- DCI 来自 James Coplien 和 Trygve Reenskaug
- BCE 来自 Ivar Jacobson 的《面向对象的软件工程:用例驱动的方法》一书
尽管这些架构在细节上都有所不同,但它们非常相似。它们都有相同的目标,即关注点分离。他们都通过将软件分层来实现这种分离。每个都至少有一层用于业务规则,另一层用于接口。
这些架构中的每一个都产生以下系统:
- 独立于框架。该架构不依赖于某些功能丰富的软件库的存在。这使您可以将此类框架用作工具,而不必将系统塞入其有限的约束中。
- 可测试。可以在没有 UI、数据库、Web 服务器或任何其他外部元素的情况下测试业务规则。
- 独立于用户界面。UI 可以轻松更改,而无需更改系统的其余部分。例如,可以用控制台 UI 替换 Web UI,而无需更改业务规则。
- 独立于数据库。您可以将 Oracle 或 SQL Server 换成 Mongo、BigTable、CouchDB 或其他东西。您的业务规则未绑定到数据库。
- 独立于任何外部机构。事实上,您的业务规则根本不了解外部世界。
依赖规则 The Dependency Rule
同心圆代表软件的不同领域。一般来说,你走得越远,软件的层次就越高。外圈是机制。内圈是政策。
使这个架构工作的最重要的规则是依赖规则。这条规则说源代码依赖只能指向内部。内圈中的任何人都无法对外圈中的事物一无所知。特别是,在外圈中声明的事物的名称不能被内圈中的代码提及。这包括函数、类。变量或任何其他命名的软件实体。
同样,在外圈中使用的数据格式不应该被内圈使用,特别是如果这些格式是由外圈中的框架生成的。我们不希望外圈的任何东西影响内圈。
实体 Entities
实体封装了企业范围的业务规则。实体可以是具有方法的对象,也可以是一组数据结构和函数。只要实体可以被企业中的许多不同应用程序使用,这并不重要。
如果您没有企业,并且只是编写单个应用程序,那么这些实体就是应用程序的业务对象。它们封装了最通用和最高级的规则。当外部事物发生变化时,它们最不可能发生变化。例如,您不会期望这些对象会受到页面导航或安全性更改的影响。对任何特定应用程序的操作更改都不应影响实体层。
用例 Use Cases
该层中的软件包含特定于应用程序的业务规则。它封装并实现了系统的所有用例。这些用例协调进出实体的数据流,并指导这些实体使用其企业范围的业务规则来实现用例的目标。
我们预计这一层的变化不会影响实体。我们也不希望这一层受到外部变化的影响,例如数据库、UI 或任何常见框架。该层与此类问题隔离。
但是,我们确实希望对应用程序操作的更改会影响用例,从而影响这一层中的软件。如果用例的细节发生变化,那么这一层的一些代码肯定会受到影响。
接口适配器 Interface Adapters
该层中的软件是一组适配器,可将数据从对用例和实体最方便的格式转换为对某些外部机构(如数据库或 Web)最方便的格式。例如,正是这一层将完全包含 GUI 的 MVC 架构。Presenters、Views 和 Controllers 都属于这里。模型可能只是从控制器传递到用例,然后从用例返回到演示者和视图的数据结构。
类似地,在这一层中,数据从对实体和用例最方便的形式转换为对正在使用的任何持久性框架最方便的形式。即数据库。这个圈子内的任何代码都不应该对数据库有任何了解。如果数据库是 SQL 数据库,那么所有的 SQL 都应该限制在这一层,特别是限制在这一层与数据库有关的部分。
在这一层中还有任何其他适配器,用于将数据从某种外部形式(例如外部服务)转换为用例和实体使用的内部形式。
框架和驱动程序 Frameworks and Drivers.
最外层一般由框架和工具组成,如数据库、Web 框架等。通常在这一层你不会写太多代码,除了与下一个循环往内通信的胶水代码。
这一层是所有细节的所在。网络是一个细节。数据库是一个细节。我们把这些东西放在外面,它们不会造成什么伤害。
只有四个圈?
不,圆圈是示意性的。您可能会发现您需要的不仅仅是这四个。没有规则说你必须总是只有这四个。但是,依赖规则始终适用。源代码依赖项总是指向内部。随着您向内移动,抽象级别会增加。最外圈是低层次的具体细节。随着您向内移动,软件变得更加抽象,并封装了更高级别的策略。最内圈是最一般的。
跨越界限 Crossing boundaries.
图的右下角是我们如何跨越圆圈边界的示例。它显示了控制器和演示者与下一层的用例进行通信。注意控制流程。它从控制器开始,遍历用例,然后在演示者中执行。还要注意源代码依赖项。它们中的每一个都指向用例。
我们通常通过使用依赖倒置原则来解决这个明显的矛盾。例如,在像 Java 这样的语言中,我们会安排接口和继承关系,以便源代码依赖关系在跨越边界的正确点反对控制流。
例如,考虑用例需要调用演示者。但是,这个调用不能是直接的,因为这会违反依赖规则:内圈不能提及外圈中的名称。所以我们在内圈中让用例调用一个接口(这里显示为用例输出端口),并让外圈中的演示者实现它。
相同的技术用于跨越架构中的所有边界。我们利用动态多态性来创建与控制流相反的源代码依赖关系,这样无论控制流向哪个方向,我们都可以遵守依赖规则。
图的右下角是代码控制流的走向。是 按照Controller -> Use Cases -> Presenter这个路径走的。这里面有两个向右的箭头,表示的是代码的依赖方向。表示Controller和Presenter都依赖User Case层的代码。 你会发现一个问题,就是实际的代码运行的控制流程和代码的依赖方向是冲突的,这里就是通过接口实现了依赖反转。 图中的 <I>
表示接口,空三角箭头代表实现了接口,实线的箭头表示实际的依赖/调用关系。 举个例子,右侧的Use Case层的Use Case Interactor需要调用外层的Presenter,但是不能直接调用,因为这违反了依赖关系。所以,在内层(Use Case层) 加了一个接口Use Case Output Port,Use Case Interactor直接调用这个接口,而外层的Presenter实现了这个接口,这里就是依赖反转。
解释干净架构
有一个普遍的误解,认为该图总结了架构模式的所有要点。单独使用这个图,它的简单实现是创建一个与高级层匹配的文件夹结构。事实上,一些在线教程介绍了以下简单的文件夹结构作为如何实现清洁架构。
但是,这是一个错误,原因有两个:
- 对员工入职用例的更改将导致对四个包进行更改。这意味着代码违反了具有高内聚性的规则。
- 该架构并没有“尖叫它的目的”。 干净架构中包的预期布局是 Robert Martin 所说的“尖叫架构”。这里的想法是,您的包结构在不查看单个类或代码行的情况下就可以突出应用程序的意图。
更正确的结构如下所示,每个业务领域都可以从顶层轻松识别:
但是,仅这两个图像也不完整。
如果您只从前两个图像开始工作,您可能会使类紧密耦合,并且经常对如何保持层分离感到困惑。这是因为这两个图中没有足够的信息来正确理解层间端口和适配器的作用和目的。业务域之间的内部数据流并没有想象的那么清晰。在犯了紧密耦合类的错误,以及不恰当地分离跨界通信之后,许多人放弃了架构,认为它太复杂,同时也没有提供足够的好处。
这是因为我们有一个非常高和非常低的抽象级别,没有复杂的粘合剂将它们组合在一起。完成图片需要第三张图,这是信息/数据流图。
Robert Martin 将这些图表呈现为:
我们在这里用更具体的例子和我们虚构的贫乏的“工资单应用程序”重新创建了它:
在此图中,绿色箭头表示“输入”,红色箭头表示“输出”。请注意第一张和第三张图中的箭头是如何朝相反方向移动的。从系统外部,数据从视图进入,通过控制器和数据库进入用例,然后到达系统的中心,即实体。
然而,在第三种观点中,所有实体都偏向一边。实体由存储库填充,这些实体被注入到所有使用它们的控制器和用例中。注意所有红色箭头是如何向一个方向移动的。这称为依赖倒置。
请注意,在一个区域内,输入(绿色箭头)和输出(红色箭头)向任何方向流动。但是,一旦达到边界,绿色和红色箭头只会沿一个方向流动。输入进入一个区域,输出来自一个区域。将存储库作为 generatePaySlip UseCase 的输入,并且直接获取数据是非常容易和诱人的。但相反,我们确保我们的 Entity 类是进入 UseCase 的唯一数据源,是它们自己的输出。
所以总结一下:
- 第一个图像是一个非常高级的概念抽象,说明您应该如何考虑应用程序或服务的各个层。它介绍了 Repository、UseCase 和 Presenter 等类概念。
- 第二个图像是我们如何将文件和包组织到业务域中,以便开发人员在进行更改时确切地知道去哪里。这些包没有参考层概念;这些仅显示在单个文件中。
- 第三幅图是我们如何构建类和域之间的通信。请注意,数据在包和层之间自由移动。然而,当您更改一个包中的代码时,它不会影响任何其他包中的代码。
如果以 Java 语言来实现,遵循整洁架构的设计思想,则所有领域模型对象都应该是 POJO(Plain Ordinary Java Object)。整洁架构的 Entities 层对应于领域驱动设计的领域层。