导航菜单

端口和适配器架构——DDD好帮手

本文来自2018年领域驱动设计中国峰会《领域驱动设计与演进式架构专题》其中一个Sessions,是其博客版

在实际领域中驱动设计时,您可以选择一些相互引用的方法。端口和适配器架构概念简单易懂,适合作为实际领域驱动设计的辅助方法。

大约一个月前,在做2018年现场设计会议预览时,上次会议的主旨发言人肖然提出了这样的担忧,即工具和方法似乎无法解决“难以降落”的挑战。 >

没有一种方法可以绕过这个世界,使用哪一个,就像你需要添加一个属性“这取决于.”。

无论是在原版DDD书籍中,还是在许多后续专家的书籍中,隐含甚至是明确的建筑设计的终极技巧都是“体验” - 经验丰富。

从战略视角的子域划分到战术建模层面的实体和价值对象的选择,最终决策可能不是完全“理性”,而“明智”的经验起着重要作用“

那么,推动领域驱动的设计实践的方向是否应该从介绍性方法转向介绍如何积累经验?

阅读完这篇文章之后,我放弃了之前准备的主题《CQRS和Event Sourcing,从入门到放弃》,因为也许你不会遇到一年内需要通过这两种方法解决的复杂项目。

如何快速获得经验?没有什么是更多的练习,但经过训练讨论和总结,我遇到了这样的对话,我称之为“两个孩子争辩DDD”:

一个:我认为你不应该在这里使用实体,你应该使用值对象

B:我认为你的界面不是域名服务,它实际上是一个应用服务,你不做DDD

A:你的实体不应该调用Repository,你不是在做DDD

B:(看着我)你来判断,谁是对的?

我:我不知道,这取决于.

这种重新分配方法效果不好,我建议你可以跳出DDD并找到相互引用和测试的方法,比如“端口和适配器架构”

应用流行的提问:当我们谈论架构时,我们在谈论什么?在本文中,我们不讨论微服务架构,也不讨论基础架构架构。这里的架构是指:

在单个应用程序(流程)中

如何组织代码以实现端到端用户请求

它与框架无关,无论您使用ORM框架还是JDBC,这都不是架构中的关键区别

一个示例是三层体系结构,其中表示层负责接收用户指令和呈现视图;业务逻辑层负责处理“业务逻辑”;数据层负责处理数据库,保存和读取数据。

2267652-cb4fe23eebcb3c6b.jpg

图像

“经典”三层架构

三层(或多层)架构目前仍然是最常见的架构,但它也有缺点:

架构过于简单。如果解决方案包括发送电子邮件通知,那么代码应放在哪些层中?

虽然它提出了业务逻辑隔离,但没有明确的架构元素来指导我们如何隔离

因此,当实际登陆时,业务逻辑很容易泄漏到显示层,导致应用程序可能需要一种新的使用方式(如开放API),原有的业务逻辑层可能无法快速重用,同样问题也发生在数据层和业务逻辑层之间。

那么还有另一种解决方案吗? Alistair Cockburn是敏捷的早期推动者之一。他在2005年的博客中提出了端口和适配器架构。他对架构的定义是:

'应用程序应该由用户,其他程序,自动化测试或脚本同等驱动,并且可以独立于其最终运行时设备和数据库进行开发和测试

“允许应用程序同样由用户,程序,自动化测试或批处理脚本驱动,并与最终的运行时设备和数据库隔离开发和测试。”

该体系结构由端口和适配器组成,它们是应用程序的入口和出口。在许多语言中,它作为接口存在。例如,在取消订单的情况下,“发送订单取消通知”可以被视为出口端口,订单取消的业务逻辑确定何时调用端口,订单信息确定端口输入,以及port阻止预订过程的通知。交付方式的实施细节。

有两种类型的适配器。主适配器(别名Driving Adapter)表示用户如何使用该应用程序。从技术上讲,它们接收用户输入,调用端口并返回输出。 Rest API是目前使用应用程序的最常用方式。要取消订单,适配器会实现Rest API的端点并调用入口端口CancelOrderService。多个适配器可以调用相同的端口。例如,消息提供程序的驱动适配器也可以调用CancelOrderService来异步取消订单。

辅助适配器(别名驱动适配器)实现应用程序的出口端口并对外部工具执行操作,例如

执行SQL到MySQL,存储订单

使用Elasticsearch的API搜索产品

使用电子邮件/短信发送订单取消通知

如果您想象它,驱动适配器和驱动适配器基于端口在应用程序周围形成左右结构,这与传统的分层图像不同,形成六边形,因此它也将被称为六边形体系结构

2267652-39bf553a840c8b59.jpg

图像

可视端口和适配器架构

如果我在这一点上成功击败了你,请不要担心,我们将通过案例体验这种架构。

咨询公司DDD Cruises报告显示,未来几年,邮轮旅行作为中国旅游形式的比例将大幅上升。在这样的背景下,DDD Cruise,一家中国邮轮公司,正在开发新一代预订系统并尝试在线邮轮预订。

目前该计划中有两个联系人申请:

微信小程序 - 提供邮轮搜索和邮轮预订的核心体验

中国的官方网站 - 这是一个包含多个HTML页面的遗留应用程序。这次我希望提供巡航搜索的功能。值得注意的是,有些游轮与旅行社签约出售,也需要在网站上展示。营销推广

2267652-51469be250567d98.jpg

图像

C4模型 - 系统上下文图

在这两个联系人的背后,这是主角,预订引擎1.0,计划从一个应用程序开始,为联系应用程序提供API,启用邮轮搜索,邮轮预订。 Cruises有多个数据源,一些来自传统预订系统,另一些来自业务部门的Excel电子表格,存储在AWS S3对象库中。最后,还有一个小型无头CMS,为营销人员提供了一个引人注目的游轮描述。

现在让我们进入端口和适配器:

2267652-39a6fcfc74287354.jpg

图像

在“Routine”上,一个驱动适配器,两个端口,两个驱动适配器,一个小连接

API控制器是一个典型的驱动适配器,它实现了Rest API的端点并调用门户端口CruiseSearch

CruiseSearch是应用程序的入口点,可以屏蔽来自Driving Adapter的巡航搜索的实施。

另一方面,导出端口CruiseSource要求返回全部Cruise数据,隐藏应用程序外部数据源的集成方案:从旧预订系统或AWS S3上的文件中提取Cruise

推行单一责任原则

然后我们继续基于这种架构的轮廓设计。这些组件自然分为三个部分:

2267652-43690d11cc462b15.jpg

图像

摘要设计类图

绿色是驱动适配器,如果你在Java-Spring技术堆栈上,你可以从命名中找出他是一个RestController

黄色是Cruise Search的实施。这里的概念仅与游轮有关。你不应该在这里看到技术术语。

粉红色部分是驱动适配器,除了处理从数据源获取Cruise的适配器外,我们还需要

一个。 CompositeCruiseSource,它不直接与数据源交互,但它负责合并多个数据源并根据规则删除重复的Cruises

湾CachingCruiseSource不直接处理数据源,负责缓存Cruise

从架构的角度来看,这些组件很简单。请注意,简单并不意味着简单。简单来说,它只是一件事(或一件事),但很容易做一件事,例如,如果你使用Spring MVC来实现Driving。适配器可以用几行代码实现。由于这些组件要么实现业务逻辑,要么实现对符合单一责任原则的技术的调整,您可以更有效地控制某个范围内的更改,并更自信地对更改做出响应。

澄清测试策略

处理变更的另一个有效工具是自动化测试。测试金字塔是最常用的测试策略。它表明自动化测试集应该基于大量的单元测试。它们易于编写,速度快,应该只包含少量使用。 UI驱动测试,由于需要处理测试数据冲突,外部依赖准备,它们难以编写和运行速度较慢。但是中级服务/集成测试的测试目标是什么,它们与单元测试有什么区别?

2267652-d5a4b428f30e3d09.jpg

图像

测试金字塔 - 端口和适配器版

如果您感到困惑,可能希望根据端口和适配器架构重新解释它。金字塔应包含大量Driving Adatper测试,业务逻辑测试和Driven Adapter测试。

驱动适配器测试,目标是验证API是否正确解析输入,按预期调用入口端口,并生成输出。由于驱动适配器不关心入口端口的实现,因此在测试中,模拟可以很容易地构建测试场景,并且可以提高测试速度。

2267652-9542fa6f78fd7dfe.jpg

图像

在过去两年中流行的合同测试也可以被视为驾驶适配器测试的扩展

业务逻辑测试,通过Mock出口端口,也可以轻松构建测试数据,这里应该是Plain Object,测试可以完全在内存中运行,速度最快

2267652-318318c3feda4205.jpg

图像

传统的单元测试

驱动适配器测试,目标是验证外部工具,下游服务和数据库是否按预期运行。传统上,涉及这些外部依赖项的测试很难编写并且运行缓慢,但如果导出端口和驱动适配器设计得当,它们不涉及业务逻辑,这需要通过引入内存数据库来显着减少测试用例,Stub Servers等技术,构建测试场景的难度将得到改善,总体执行时间也会相应减少

2267652-c8bf0ef636fb8e4f.jpg

图像

单功能驱动适配器也降低了测试难度,但测试速度仍然相对较慢

应该注意的是,上述测试在技术上测试组件是否符合预期。您可以考虑添加E2E测试以验证这些组件是否已集成且可用。业务符合预期,并且通常可以使用涵盖关键功能的Happy Path场景。

促进增量发展

端口和适配器架构也可以为我们实现增量开发提供一些灵感。我们来看看这个用户故事分解示例:

由于旅行社出售的游轮全部来自Excel电子表格,只要确定了表格字段的含义,我们就可以开始整合。我们选择这张卡来建造脚手架:

2267652-fa891b26058a12c1.jpg

图像

如果业务优先级允许,选择技术来实现最简单的卡建筑脚手架

接下来,在InMemoryCruiseSearch中实现过滤:

2267652-359367e2995ece91.jpg

图像

实施过滤功能

介绍LegacyBookingCruiseSource和CompositeCruiseSource

2267652-0e2a402fe730a8aa.jpg

图像

实施

最后,您可以介绍一张技术卡:

2267652-8a1899aeaa8c8bba.jpg

图像

添加CachingCruiseSource以提高Cruise读取的速度

在这里,我们总结一下:

2267652-043a15492387175f.jpg

图像

端口和适配器架构的组件及其好处

由于该概念简单易懂,因此端口和适配器架构非常适合作为DDD的入门工具,许多域驱动设计方法可以补充端口和适配器架构中的空白。

2267652-77af7ae52538179b.jpg

图像

端口和适配器架构以及域驱动设计之间的协同作用

验证“通用语言”

通用语言是域驱动设计的核心,建议所有各方(无论是域专家和开发人员)使用相同的词汇表来实现相同的功能。这可以防止各方在通信领域产生误解,并在没有不同专业背景的情况下制定解决方案,最终促进对正确问题的识别和采用正确的解决方案。甚至有一种激进的观点认为“领域模式本身就是通用语言”。

虽然端口和适配器不直接帮助我们找到域模型或通用语言,但它有助于我们快速消除常用语言的技术概念:应排除用于实现适配器的所有技术细节。让我们回到DDD Cruise示例:

2267652-a269a61c1a15377f.jpg

图像

对话片段,请注意绿色字体与驱动适配器相关,它们应该被公共语言排除

脚手架作为“DDD战术设计”

域驱动设计诞生于2004年。一年后,提出了端口和适配器。在战术设计层面,我们可以发现许多相似之处并相互呼应。以体系结构为例,DDD原始工作中提出的体系结构非常有趣:乍一看,它被认为是传统的分层体系结构,但它强调了每层的Infrastructure的实现。

2267652-8ac94461f97cc560.jpg

图像

如果我们进行职责分析,您会发现这不是端口和适配器吗?

端口和适配器的优势在于分层不是重点,技术实现隔离是关键,因此您不必再担心组件是否允许跨层调用。 DDD原始体系结构的优点是通过Application和Domain进一步阐明业务逻辑的模糊概念。让我们把它合二为一:

2267652-a77cc8d8f6e2e44c.jpg

图像

值得一提的是,Application和Domain甚至可以是声明性的,因为端口存在,例如,DDD构建块中的ApplicationService是典型的入口端口,而Repository是典型的出口端口。

让我们回到DDD和域模型过滤方法之间的映射工作,这样它就完全负责步骤协调

2267652-3bc0b8858da614e3.jpg

图像

将应用程序服务和域模型替换为Cruise Search

让“DDD战略设计”指南孤立实施

在实施战略设计时,一个重要的实践是识别有限的背景。当存在多个边界上下文时,很可能需要进行集成。防腐层是一种常见的集成工具。看看这个:服务A是左边界上下文暴露的接口,以及通过适配器调用右边界上下文的接口。

2267652-621482275431fca0.jpg

图像

“防腐层”

这个熟悉吗?这不是端口和驱动适配器吗?您可以将它们视为专门的防腐层。然后,当单个应用程序中存在多个边界上下文时,它们也应该是端口隔离的并与适配器集成。如果使用微服务来隔离边界上下文,则端口和适配器体系结构适用于这些服务中的每一个。

回到DDD Cruise,请记住我们需要集成Headless CMS,因为在目前阶段,我们在单个应用程序中工作,CruiseSearch API需要返回包含巡航描述的信息。

2267652-67b81d6906847e42.jpg

图像

虽然引入了端口和驱动适配器,但是无法识别边界上下文,这是不理想的

一种解决方案是将这些描述性信息添加到域模型中。由于现有数据源都不能提供此信息,因此我们引入了另一个导出端口及其驱动适配器来填充此信息。但这个解决方案并不理想:

在巡航搜索的筛选实施中,描述信息没有实际效果,域模型变得更加臃肿甚至造成干扰。

在巡航搜索测试中,我们不关心描述,但我们可能需要构造一些虚拟数据以避免可能的空指针误报

2267652-3bec87c2ec8eaa89.jpg

图像

将边界上下文引入DDD Cruise

在有界上下文概念的指导下的另一种解决方案引入了入口端口和出口端口,以保持巡航搜索上下文不受干扰。该方案的优点在于,假设巡航搜索引擎执行微服务转换,很可能将描述信息填充的责任分离到单独的服务中。在这种情况下,您只需提供一个输入并输出驱动适配器,而无需描述信息。

2267652-105eff08c41b078b.jpg

图像

在边界上下文的指导下找到一个更稳定的端口

我们介绍了端口和适配器架构,它易于掌握并与域驱动设计协调,希望它能帮助您快速积累DDD体验!

文/ThoughtWorks周玉刚

如需更多精彩见解,请关注微信公众号:ThoughtWorks见解