实现领域驱动设计
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

六边形架构(端口与适配器)

在六边形架构[2]中,Alistair Cockburn提出了一种具有对称性特征的架构风格[Cockburn]。在这种架构中,不同的客户通过“平等”的方式与系统交互。需要新的客户吗?不是问题。只需要添加一个新的适配器将客户输入转化成能被系统API所理解的参数就行了。同时,系统输出,比如图形界面、持久化和消息等都可以通过不同方式实现,并且是可互换的。这是可能的,因为对于每种特定的输出,都有一个新建的适配器负责完成相应的转化功能。

至此,我们有充足的理由认为,这将是一种具有持久生命力的架构。

现在,很多声称使用分层架构的团队实际上使用的是六边形架构。这是因为很多项目都使用了某种形式的依赖注入。并不是说依赖注入天生就是六边形架构,而是说使用依赖注入的架构自然地具有了端口与适配器风格。我们将对此做详尽的解释。

我们通常将客户与系统交互的地方称为“前端”;同样,我们将系统中获取、存储持久化数据和发送输出数据的地方称为“后端”。但是,六边形架构提倡用一种新的视角来看待整个系统,如图4.4所示。该架构中存在两个区域,分别是“外部区域”和“内部区域”。在外部区域中,不同的客户均可以提交输入;而内部的系统则用于获取持久化数据,并对程序输出进行存储(比如数据库),或者在中途将输出转发到另外的地方(比如消息)。

牛仔的逻辑

AJ:“我的那些马确实很喜欢它们的六边形蓄栏,因为当我带上马鞍要骑它们时,它们有更多的角落可以跑躲。”

在图4.4中,每种类型的客户都有它自己的适配器[Gamma et al.],该适配器用于将客户输入转化为程序内部API所能理解的输入。六边形每条不同的边代表了不同种类型的端口,端口要么处理输入,要么处理输出。图4.4中有3个客户请求均抵达相同的输入端口(适配器A、B和C),另一个客户请求使用了适配器D。可能前3个请求使用了HTTP协议(浏览器、REST和SOAP等),而后一个请求使用了AMQP协议(比如RabbitMQ)。端口并没有明确的定义,它是一个非常灵活的概念。无论采用哪种方式对端口进行划分,当客户请求到达时,都应该有相应的适配器对输入进行转化,然后端口将调用应用程序的某个操作或者向应用程序发送一个事件,控制权由此交给内部区域。

img

图4.4 六边形架构也称为端口与适配器。对于每种外界类型,都有一个适配器与之相对应。外界通过应用层API与内部进行交互。

我们不必自己实现端口

通常来说,我们都不用自己实现端口。我们可以将端口想成是HTTP,而将适配器想成是Java的Servlet或JAX-RS的REST请求处理类。或者,我们可以为NServiceBus或RabbitMQ创建消息监听器,在这种情况下,端口是消息机制,而适配器则是消息监听器,因为消息监听器将负责从消息中提取数据,并将数据转化为应用层API(领域模型的客户)所需的参数。

按照功能需求来设计内部区域中的应用程序

在使用六边形架构时,我们应该根据用例来设计应用程序,而不是根据需要支持的客户数目来设计。任何客户都可能向不同的端口发出请求,但是所有的适配器都将使用相同的API。

应用程序通过公共API接收客户请求。应用程序边界,即内部六边形,也是用例(或用户故事)边界。换句话说,我们应该根据应用程序的功能需求来创建用例,而不是客户数量或输出机制。当应用程序通过API接收到请求时,它将使用领域模型来处理请求,其中便包括对业务逻辑的执行。因此,应用层API通过应用服务的方式展现给外部。再次提醒大家,这里的应用服务是领域模型的直接客户,就像在分层架构中一样。

以下代码表示通过JAX-RS发布的RESTful资源。当请求到达HTTP的输入端口时,相应的适配器将对请求的处理委派给应用服务:

JAX-RS所提供的Java注解构成了适配器的大部分功能,它们负责解析资源路径,并将资源参数转化为String类型参数。ProductService实例是注入进来的,请求便是通过该ProductService将处理委派到应用程序内部的。之后,Product对象将被序列化成XML,然后放在Response中,再由HTTP输出端口发出。

JAX-RS并不是我们的关注点

JAX-RS只是使用应用程序和领域模型的一种方式。在这里,JAX-RS并不重要,我们完全可以使用Restfulie或者Node.js来完成相同的功能。但不管采用哪种方式,不同的适配器都会将输入委派给相同的API。.

对于图4.4中右侧的端口和适配器,我们应该如何看待呢?我们可以将资源库的实现看作是持久化适配器,该适配器用于访问先前存储的聚合实例,或者保存新的聚合实例。正如图中的适配器E、F和G所展示的,我们可以通过不同的方式实现资源库,比如关系型数据库、基于文档的存储、分布式缓存和内存存储等。如果应用程序向外界发送领域事件消息,我们将使用适配器H进行处理。该适配器处理消息输出,而刚才提到的处理AMQP消息的适配器则是处理消息输入的,因此应该使用不同的端口。

六边形架构的一大好处在于,我们可以轻易地开发用于测试的适配器。整个应用程序和领域模型可以在没有客户和存储机制的条件下进行设计开发。在测试时,我们可以方便地对ProductService进行替换,而无须考虑它是应该支持HTTP/REST呢,还是SOAP呢,或者是消息端口。任何测试客户都可以在用户界面还未完成之前进行开发。在选择持久化机制之前,我们可以在测试中采用内存资源库来模拟持久化。更多的内存持久化实现细节,请参考资源库(12)。如此一来,我们可以在核心领域上进行持续开发,而不需要考虑那些支撑性的技术组件。

如果你采用的是严格分层架构,那么你应该考虑推平这种架构,然后开始采用端口与适配器。如果设计得当,内部六边形——也即应用程序和领域模型——是不会泄漏到外部区域的,这样也有助于形成一种清晰的应用程序边界。在外部区域,不同的适配器可以支持自动化测试和真实的客户请求,还有存储、消息和其他输出机制等。

当SaaSOvation的开发团队考虑了六边形架构的优点之后,他们决定从分层架构转向六边形架构。事实上,这并不困难,只是需要从不同的角度来看待和使用Spring框架而已。

六边形架构的功能如此强大,以致于它可以用来支持系统中的其他架构。比如,我们可能采用SOA架构、REST或者事件驱动架构;也有可能采用CQRS;或者数据网织或基于网格的分布式缓存;还有可能采用Map-Reduce这种分布式并行处理方式。以上这些架构我们会在后续章节中讲到。六边形架构为这些架构提供了坚实的支撑基础。当然,能够提供这种基础的不只是六边形架构,但是在本章剩下的内容中,我们都假设使用这种架构。