天天看点

软件架构(11)-端口和适配器/六边形架构,高阶进阶之路

作者:架构师狂飙

这篇文章是 软件架构编年史的一部分,这是一系列关于软件架构的文章。在其中,我写下了我在软件架构方面学到的知识、我对它的看法以及我如何使用这些知识。如果您阅读了本系列的前几篇文章,这篇文章的内容可能会更有意义。

端口和适配器架构(又名六边形架构)由 Alistair Cockburn 于 2005 年提出并写在他的博客上。他用一句话定义其目标:

允许应用程序同样由用户、程序、自动测试或批处理脚本驱动,并在独立于其最终运行时设备和数据库的情况下进行开发和测试。
Alistair Cockburn 2005, 端口和适配器

我看过一些关于端口和适配器架构的文章,其中对层进行了很多阐述。但是,我还没有在 Alistair Cockburn 的原帖中读到任何关于层的内容。

这个想法是将我们的应用程序视为系统的中心人工制品,其中所有输入和输出都通过一个端口到达/离开应用程序,该端口将应用程序与外部工具、技术和交付机制隔离开来。应用程序应该不知道谁/什么正在发送输入或接收其输出。这是为了针对技术和业务需求的发展提供一些保护,由于技术/供应商锁定,这可能会使产品在开发后不久就过时。

在这篇文章中,我将深入探讨以下主题:

  • 传统方法的问题
  • 从分层架构演进
  • 什么是端口?什么是适配器?两种不同类型的适配器
  • 有什么好处?
  • 实施和技术隔离交付机制隔离测试
  • 结论

传统方法的问题

传统的方式很可能会给我们带来前端和后端两个方面的问题。

在前端,我们最终将业务逻辑泄漏到 UI 中(即,当我们将用例逻辑放在控制器或视图中时,使其无法在其他 UI 屏幕中重用)甚至将 UI 泄漏到业务逻辑中(即,当我们由于模板中需要的某些逻辑而在我们的实体中创建方法时)。

软件架构(11)-端口和适配器/六边形架构,高阶进阶之路

在后端,我们可能会将外部库和技术泄漏到业务逻辑中,因为我们最终可能会通过类型提示、子类化甚至实例化业务逻辑中的库类来直接引用它们。

从分层架构演进

到 2005 年,由于EBI和DDD,我们已经知道系统中真正相关的是内层。这些层是所有业务逻辑存在(或应该存在)的层,它们是我们与竞争对手的真正区别。这才是真正的“应用”。

软件架构(11)-端口和适配器/六边形架构,高阶进阶之路

但在某些时候,Alistair Cockburn 意识到,另一方面,顶层和底层只是应用程序的入口/出口点。尽管它们实际上不同,但它们的目标非常相似,并且在设计中存在对称性。此外,如果我们想要隔离我们的应用程序内层,我们可以以类似的方式使用这些入口/出口点来实现。

软件架构(11)-端口和适配器/六边形架构,高阶进阶之路

为了摆脱典型的分层图,我们将系统的这两个方面表示为左右,而不是顶部和底部。

虽然我们可以识别应用程序的两个对称边,但每一边都可以有多个入口/出口点。例如,API 和 UI 是我们应用程序左侧的两个不同的入口/出口点,而 ORM 和搜索引擎是我们应用程序右侧的两个不同的入口/出口点。为了表示我们的应用程序有多个入口/出口点,我们将绘制具有多个面的应用程序图。该图可能是有多个边的任何多边形,但最终选择的是六边形。因此得名“六边形架构”。

软件架构(11)-端口和适配器/六边形架构,高阶进阶之路

Ports & Adapters Architecture通过使用一个抽象层解决了前面发现的问题,实现为一个端口和一个适配器。

什么是端口?

端口是消费者不可知的进出应用程序的入口点和出口点。在许多语言中,它将是一个接口。例如,它可以是用于在搜索引擎中执行搜索的界面。在我们的应用程序中,我们将使用此接口作为入口和/或出口点,而不知道在接口定义为类型提示的情况下实际注入的具体实现。

什么是适配器?

适配器是将接口转换(适配)到另一个接口的类。

例如,适配器实现接口 A 并注入接口 B。当适配器被实例化时,它会在其构造函数中注入一个实现接口 B 的对象。然后在需要接口 A 的任何地方注入该适配器并接收它的方法请求转换并代理到实现接口 B 的内部对象。

如果我让你感到困惑,不用担心,我在下面给出了一个更具体的例子。

两种不同类型的适配器

左侧的适配器代表 UI,称为主适配器或驱动适配器 ,因为它们是在应用程序上启动某些操作的适配器,而右侧的适配器代表与后端工具的连接,称为次要或驱动适配器,因为它们总是对主要适配器的操作做出反应。

端口/适配器的使用方式也有所不同:

  • 在左侧,适配器依赖于端口并被注入端口的具体实现,其中包含用例。在这方面,端口及其具体实现(用例)都属于应用程序内部;
  • 在右侧,适配器是端口的具体实现,虽然我们的业务逻辑只知道接口,但它被注入到我们的业务逻辑中。在这方面,端口属于应用程序内部,但它的具体实现属于应用程序外部,它环绕着一些外部工具。
软件架构(11)-端口和适配器/六边形架构,高阶进阶之路

有什么好处?

使用这种端口/适配器设计,将我们的应用程序置于系统的中心,使我们能够将应用程序与临时技术、工具和交付机制等实施细节隔离开来,从而使测试和创建可重用证明变得更加容易和快速的概念。

实施和技术隔离

语境

我们有一个使用 SOLR 作为搜索引擎的应用程序,我们使用一个开源库连接到它并执行搜索。

传统方法

使用传统方法,我们将直接在我们的代码库中使用该库类,作为我们实现的类型提示、实例和/或超类。

端口和适配器方法

使用端口和适配器,我们将创建一个接口,我们称它为 UserSearchInterface,我们将在需要时将其用作类型提示。我们还将为 SOLR 创建适配器,它将实现该接口,我们将其命名为 UserSearchSolrAdapter。此实现是 SOLR 库的包装器,因此它会注入库并使用它来实现接口中指定的方法。

问题

在某些时候,我们想从 SOLR 切换到 Elasticsearch。此外,对于相同的搜索,有时我们想使用 SOLR,而其他时候我们想使用 Elasticsearch,并在运行时做出决定。

如果我们使用传统的方法,我们将不得不为 Elasticsearch 库搜索和替换 SOLR 库的用法。然而,这不是简单的搜索和替换:库有不同的使用方式,不同的方法有不同的输入和输出,所以替换库不是一件容易的事。并且在运行时使用一个库而不是另一个库是不可能的。

但是,如果我们使用 Ports & Adapters,我们只需要创建一个新的适配器,我们将其命名为 UserSearchElasticsearchAdapter,然后注入它而不是 SOLR 适配器,也许只需更改 DIC 中的配置即可。要在运行时注入不同的实现,我们可以使用工厂来决定注入哪个适配器。

交付机制隔离

以与前面示例类似的方式,假设我们有一个需要 Web GUI、CLI 和 Web API 的应用程序。我们还希望在所有三个 UI 中提供一些功能,我们称该功能为 UserProfileUpdate。

使用端口和适配器,我们将在应用程序服务方法中实现此功能并将其视为用例。该服务将实现一个指定方法、输入和输出的接口。

然后,每个 UI 版本都会有一个控制器(或控制台命令),该控制器将使用该界面来触发所需的逻辑,并将注入服务的具体实现。在这里,Adapter 实际上是控制器(或 CLI Command)。

然后我们可以完全改变 UI,因为我们知道我们不会影响业务逻辑。

测试

在前面的两个示例中,使用端口和适配器架构进行测试变得更加容易。在第一个示例中,我们可以模拟或存根接口(端口)并在不使用 SOLR 或 Elasticsearch 的情况下测试我们的应用程序。

在第二个示例中,我们可以通过简单地为我们的服务提供一些输入并断言结果来测试与我们的应用程序隔离的所有 UI,以及与 UI 隔离的用例。

结论

在我看来,端口和适配器架构只有一个目标:将业务逻辑与系统使用的交付机制和工具隔离开来。它通过使用一种通用的编程语言结构来实现:接口。

在UI端(驱动适配器),我们创建使用我们的应用程序接口的适配器,即。控制器。

在基础设施方面(驱动适配器),我们创建了实现我们的应用程序接口的适配器,即。存储库。

仅此而已!

不过,奇怪的是,同样的想法早在 13 年前就已发表,尽管没有明确强调将工具和交付机制与应用程序核心隔离开来的目标。

软件架构(11)-端口和适配器/六边形架构,高阶进阶之路

伊瓦尔·雅各布森 1992 年,第 171 页

系统与参与者的任何交互都通过边界对象。正如 Jacobson 所描述的,Actor 可以是人类用户,如客户或管理员(操作员),但也可能是非人类“用户”,如警报器或打印机,对应于 Driving Adapters和Driven Adapters端口和适配器架构。

参考资料:https://herbertograca.com/2017/09/14/ports-adapters-architecture/

继续阅读