软件架构架构师

11.端口和适配器架构(译)

2018-09-14  本文已影响58人  qinyu

原文:https://herbertograca.com/2017/09/14/ports-adapters-architecture/

这篇文章是软件架构编年史()的一部分,这部编年史由一系列关于软件架构的文章()组成。在这一系列文章中,我将写下我对软件架构的学习和思考,以及我是如何运用这些知识的。如果你阅读了这个系列中之前的文章,本篇文章的的内容将更有意义。

2005年,Alistair Cockburn构思了端口和适配器架构 (又称六边形架构)并记录在他的博客中。下面这句话就是他对该架构的目标的定义:

让用户、程序、自动化测试和批处理脚本可以平等地驱动应用,让应用的开发和测试可以独立于其最终运行的设备和数据库。——Alistair Cockburn 2005,端口和适配器

有许多文章在谈及端口和适配器架构时会花很多篇幅在分层上。然而, 我并没有在 Alistair Cockburn 的原文中找到关于分层的只言片语。

其思想使将我们的应用看作是一个系统的中心产物,输入和输出都是通过一个端口出入应用,这些端口将应用和外部的工具、技术以及传达机制隔离开来。应用不应该关心是谁在发送输入或接收输出。这就是为了保护产品免受技术和业务需求演进的影响。由于技术/供应商锁定,这些演进可能导致产品刚开发没多久就被废弃。

我将在本文中剖析以下主题:

传统架构方式的问题

传统的架构方式在前端和后端都可能给我们带来问题。

在前端,业务逻辑最终可能会渗透到 UI(例如,我们把用例的逻辑放到控制器或视图里,导致这些逻辑不能在其它 UI 界面中重用), 甚至 UI 会渗透到业务逻辑中(例如,我们会为了模板中需要的业务逻辑在实体中创建对应的方法)。

而在后端,我们可能会在自己的业务逻辑里使用外部类的类型提示、继承或者实例化它们,这会导致对这些外部的库和技术直接引用,最后任由它们渗透到业务逻辑中。

分层架构的演化

EBIDDD()的福, 2005 年我们已经知道了“系统中真正重要的是中间的层次”。业务逻辑(应该)存在于这些层次之中,它们才是我们和竞品的真正区别。这才是真正“应用”。


但是,Alistair Cockburn 意识到 顶部和底部的层次从另一方面来说,就是应用的入口/出口。尽管实际上他们确实不一样,它们却有着十分相似的目标,在设计中也是对称的。而且,如果我们想要隔离出应用中间的层次,这些入口和出口能以另一种相似的方式使用。

区别于典型的分层架构图,我们将它们画在系统的左右两侧,而不是上下两边。

虽然我们识别出了系统中对称的两侧,但两侧都可能有若干入口/出口。例如, API和UI就是位于应用左侧的两个不同的入口/出口。为了表示应用有若干个入口/出口,我们把应用改成了多边形。多边形的可以有任意多条边,但最终六边形获得了青睐。这也是“六边形架构”的由来。

端口和适配器架构使用了实现为端口和适配器的抽象层次,解决了传统架构方式带来的问题。

什么是端口?

端口是对其消费者无感知的进入/离开应用的入口和出口。在许多编程语言里,端口就是接口。例如,在搜索引擎里它可能是可以执行搜索的接口。在应用中,我们把这个接口当成入口/出口使用,而不用去关心它的具体实现,实际上这些实现会在所有使用接口定义类型提示的地方被注入。

什么是适配器?

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

例如,一个适配器实现了接口 A 并被注入了接口 B。当这个适配器被实例化时,一个实现了接口B的对象将通过它的构造方法被注入进来。这个对象会被注入到需要接口A的地方,然后接收方法请求,将其转换并代理给实现了接口B的内部对象。

如果我说的不够明白,别慌,后面我会给出一个更具体的例子。

适配器的两种不同类型

左侧代表 UI 的适配器被称为主适配器或者主动适配器,因为使它们发起了应用上的一些动作。而右侧表示和后端工具链接的适配器,被称为从适配器或者被动适配器,因为它们只会对主适配器的动作作出响应。

端口/适配器的用法也有一点区别:

端口和适配器架构有哪些优势?

使用这种应用位于系统中心的端口/适配器设计,让我们可以保持应用和实现细节之间的隔离,这些实现细节包括昙花一现的技术、工具和传达机制。它还让可重用的概念验证创建起来更容易更快速。

实现及技术的隔离

上下文

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

传统架构方式

传统架构方式下,我们会直接在我们的代码中使用库(SOLR)里的类,作为类型提示,或者实例化和/或作为我们实现的基类。

端口和适配器架构方式

如果采用端口和适配器架构的话,我们会创建一个接口,比如叫做 UserSearchInterface,在代码中用这个接口作为类型提示。我们还会为 SOLR 创建一个实现该接口的适配器,比如叫做 UserSearchSolrAdapter。这个实现是 SOLR 的包装,SOLR 会被注入其中并用来实现接口指定的方法。

问题

不久之后,我们想用Elasticsearch换掉SOLR。甚至,对于同样搜索,我们希望有些时候使用SOLR,有些时候使用Elasticsearch,在运行时决定就好。

如果我们采用传统架构,我们需要查找所有使用SOLR的代码并替换成Elasticsearch。然而,这可不是简单的查找替换:两个引擎的用法不同,方法、输入、输出也不尽相同,替换并不是一件轻松的任务。而在运行时决定使用那个引擎甚至是不可能的。

然而,假设我们使用了端口和适配器架构,我们只需要创建一个新的适配器,比如就叫UserSearchElasticsearchAdapter,注入时使用它换掉SOLR的适配器,也许改一下DCI中的配置就可以做到。我们完全可以使用工厂来决定注入那个适配器,在运行时注入不同的实现。

传达机制的隔离

和上面这个例子,假设我们的应用需要 Web GUI,CLI 和 Web API。我们想在全部三种 UI 中提供某个功能,比如叫做UserProfileUpdate的功能。

使用端口和适配器架构的话,我们会在一个应用服务的方法中实现这个功能并作为一个用例。服务会实现一个接口,该接口指定了方法、输入以及输出。

每个版本的 UI 都有各自的控制器(或控制台命令)来通过这个接口触发期望的逻辑,应用服务接口的具体实现会被注入到 UI 中。这种情况下,适配器实际上就是控制器(或 CLI 命令)。

之后我们可以修改 UI,因为我们知道这些修改不会影响业务逻辑。

测试

上面两个例子中,使用端口和适配器架构会让测试更加容易。第一个例子中,我们用接口(端口)的 Mock 就可以测试应用,而不需要使用 SOLR 或 Elasticsearch 。

第二个例子中,所有的 UI 都可以独立于应用进行测试。我们的用例也可以独立于 UI 进行测试,传给服务一些输入断言结果就好。

总结

在我看来,端口和适配器架构只有一个目标:将业务逻辑和系统使用的传达机制以及工具隔离。为此,它使用了常见的编程语言结构:接口

UI侧(主动适配器),我们创建使用应用接口的适配器,比如控制器。
基础设施侧(被动适配器),我们创建实现应用接口的适配器,比如资源库。

这就是全部!

然而,我惊讶的发现早在 13 年前同样的思想就已经公开发表了,尽管它没有刻意地强调要将工具和传达机制从应用核心中隔离出来。

系统和角色的任何交互都要通过边界对象。按照 Jacobson 的描述,角色可以是客户或者管理员(操作员)这样的人类用户,也可以是定时器或者打印机这样的非人类“用户”,它们分别对应着端口和适配器架构中的主动适配器被动适配器

引用来源

1992 – Ivar Jacobson – Object-Oriented Software Engineering: A use case driven approach
200? – Alistair Cockburn – Hexagonal Architecture
2005 – Alistair Cockburn – Ports and Adapters
2012 – Benjamin Eberlei – OOP Business Applications: Entity, Boundary, Interactor
2014 – Fideloper – Hexagonal Architecture
2014 – Philip Brown – What is Hexagonal Architecture?
2014 – Jan Stenberg – Exploring the Hexagonal Architecture
2017 – Grzegorz Ziemoński – Hexagonal Architecture Is Powerful
2017 – Shamik Mitra – Hello, Hexagonal Architecture

上一篇下一篇

猜你喜欢

热点阅读