网关

封装对外部系统或资源的访问的对象

2021年8月10日

188bet足球充值

有趣的软件很少孤立存在。团队编写的软件通常必须与外部系统交互,这些系统可能是库、对外部服务的远程调用、与数据库的交互,或者与文件的交互。通常会有某种形式的API用于外部系统,但从我们的软件环境来看,这个API通常会显得很笨拙。API可能使用不同的类型,需要奇怪的参数,以在我们的上下文中没有意义的方式组合字段。处理这样的API在使用时可能会导致不匹配。

一个入口作为一个单独的点来面对这个外国人。我们系统中的任何代码都与网关的接口进行交互,网关的设计是按照我们系统使用的术语工作的。bet188足球然后网关将这个方便的API转换为外来者提供的API。

虽然这种模式被广泛使用(但应该更加普遍),但“网关”这个名称并没有流行起来。因此,尽管您应该期望经常看到这种模式,但它并没有广泛使用的名称。

它是如何工作的

网关通常是一个简单的包装器。我们看看我们的代码需要对外部系统做些什么,然后构造一个清晰而直接地支持它的接口。然后我们实现网关,将该交互转换为外部系统的术语。这通常涉及到将一个熟悉的函数调用转换为外部API所需的内容,并根据需要调整参数以使其工作。得到结果后,我们将其转换为代码中易于使用的形式。随着我们代码的增长,对外部系统提出了新的要求,我们加强了网关,以继续支持其不同的需求。

网关应该只包含支持国内和国外概念转换的逻辑。建立在此基础上的任何逻辑都应该在网关的客户端中。

将连接对象添加到网关的基本结构中通常是很有用的。连接是对外部代码调用的简单包装。网关将其参数转换为外部签名,并调用与该签名的连接。然后连接调用外部API并返回其结果。入口将结果转化为更易于消化的形式。这种连接可以在两方面发挥作用。首先,它可以封装调用外部代码的任何尴尬部分,比如REST API调用所需的操作。其次,它作为插入一个好的点测试双

何时使用

每当我访问一些外部软件时,我都会使用一个网关,而这种外部因素会让我感到尴尬。我没有让这种尴尬传遍我的代码,而是将其包含到网关中的一个地方。

通过允许测试工具终止网关的连接对象,使用网关可以使系统更容易测试。这对于访问远程服务的网关尤其重要,因为它可以消除缓慢的远程调用的需要。对于需要为测试提供固定数据但又不是设计来这样做的外部系统来说,这是至关重要的。bet188足球我将在这里使用网关,即使外部API在其他方面可以使用(在这种情况下,网关将仅是连接对象)。

网关的另一个优点是,如果发生这种情况,可以更容易地将一个外部系统替换为另一个外部系统。类似地,如果外部系统更改了它的API或返回的数据,网关可以使我们更容易地调整代码,因为任何更改都局限于单个位置。但是,尽管这种好处很方便,但它几乎不是使用网关的理由,因为仅仅封装外部API就足够了。

网关的一个关键目的是翻译外来词汇,否则会使主机代码复杂化。但在这之前,我们确实需要考虑我们是否应该只使用外语词汇。我遇到过这样的情况,一个团队将一个广泛理解的外国词汇翻译成一个特定的代码库词汇,因为“他们不喜欢这些名字”。对于这个决定,我没有一个通用的规则可以陈述,团队必须判断他们是应该采用外部词汇表还是开发自己的词汇表。(在领域驱动设计bet188足球模式,这是在墨守成规和反腐败层之间的选择。)

一个特别的例子是,我们在平台上构建,并考虑是否希望将自己与底层平台隔离。在许多情况下,平台的功能非常普遍,不值得进行包装。例如,我不会考虑包装语言的集合类。在这种情况下,我只是接受他们的词汇表是我的软件词汇表的一部分。

进一步的阅读

我最初描述的这种模式P (EAA).当时,我在考虑是否创建一个新的模式名称,而不是引用现有的Gang of Four模式:Facade、Adapter和Mediator。最后,我决定有足够的差异,它值得一个新的名字。

虽然Facade简化了一个更复杂的API,但它通常是由服务的作者为一般用途而完成的。网关是由客户端为其特定用途而编写的。

适配器是最接近网关的GoF模式,因为它改变一个类的接口以匹配另一个。但是适配器是在已经存在的两个接口的上下文中定义的,而对于网关,我在包装外部元素时定义网关的接口。这种区别使我将gateway视为一个单独的模式。随着时间的推移,人们对“适配器”的使用越来越宽松,因此将网关称为适配器并不罕见。

中介将多个对象分开,因此它们不需要知道彼此,它们只知道中介。对于网关,通常只有一个资源被封装在网关后面,而该资源不会知道网关。

门户的概念与有界的情况下领域驱动设计bet188足球.当我在不同的环境中处理一些事情时,我使用网关,网关处理外部环境和我自己的环境之间的翻译。网关是一种实现防贪层的方法。因此,一些团队会使用这个术语,用缩写形式“ACL”来命名他们的网关。

术语“网关”的一个常见用法是API网关.根据我在上面概述的原则,这实际上更像是一个facade,因为它是由服务提供者为一般客户端使用而构建的。

示例:Simple Function (TypeScript)

考虑一个假想的医院应用程序,它监视一系列治疗程序。许多治疗项目都需要预约病人使用骨融合机。为此,应用程序需要与医院的设备预订服务进行交互。应用程序通过一个库与服务交互,该库公开了一个列出某些设备可用预订槽的功能。

equipmentBookingService.ts……

导出函数listAvailableSlots(equipmentCode: string, duration: number, isEmergency: boolean): Slot[]

由于我们的应用程序只使用骨融合机,而且从不在紧急情况下使用,所以简化这个函数调用是有意义的。这里的简单网关可以是一个函数,其命名方式对当前应用程序有意义。

boneFusionGateway.ts……

export function listboneffusionslots (length: Duration) {return ebs. txt;listAvailableSlots("BFSN", length.toMinutes(), false) .map(convertSlot) }

这个网关功能正在做一些有用的事情。首先,它的名称将其与应用程序中的特定用法联系起来,允许许多调用者包含更清晰地阅读的代码。

网关功能封装了设备预订服务的设备代码。只有这个功能需要知道,要得到一个骨融合机,你需要代码“BFSN”。

网关函数执行从应用程序内使用的类型到API使用的类型的转换。在这种情况下,应用程序使用js-joda处理时间-在JavaScript中简化任何类型的日期/时间工作的一个常见而明智的选择。但是,该API使用整数分钟数。网关函数允许调用者在应用程序中使用约定,而不需要考虑如何转换为外部API的约定。

来自应用程序的所有请求都是非紧急的,因此网关不会公开一个总是相同值的参数

最后,使用转换函数从设备预订服务的上下文中转换API的返回值。

设备预订服务返回像这样的槽对象

equipmentBookingService.ts……

Slot {duration: number, equipmentCode: string, date: string, time: string, equipmentID: string, emergencyOnly: boolean,}

但是调用应用程序发现这样的插槽更有用

treatmentPlanningAppointment.ts……

Slot {date: LocalDate, time: LocalTime, duration: duration, model: EquipmentModel}

因此,这段代码执行转换

boneFusionGateway.ts……

函数convertSlot(slot: ets . slot): slot {return {date: LocalDate.parse(slot.date), time: LocalTime.parse(slot.time), duration: duration . ofminutes (slot.duration), model: modelFor(slot. equipmentid),}}

这种转换忽略了对治疗计划应用程序没有意义的字段。它将日期和时间字符串转换为js-joda。治疗计划用户不关心设备id代码,但他们关心槽中可用的设备型号。所以convertSlot从其本地存储中查找设备模型,并使用模型记录丰富插槽数据。

通过这样做,治疗计划应用程序不必处理设备预订服务的语言。它可以假装设备预订服务在治疗计划的世界中无缝地工作。

示例:使用可替换的连接(TypeScript)

网关是通向外部代码的路径,外部代码通常是通向驻留在其他地方的重要数据的路径。这样的外来数据会使测试复杂化。我们不想每次治疗应用程序的开发人员运行我们的测试时都要预订设备插槽。即使该服务提供了一个测试实例,远程调用的缓慢速度经常会削弱快速测试套件的可用性。这时使用a是有意义的测试双

网关是插入这样一个测试double的自然点,但是有两种不同的方法可以做到这一点,因为对远程网关有更多的结构是值得的。当使用远程服务时,网关履行两个职责。与本地网关一样,它将远程服务的词汇表转换为主机应用程序的词汇表。但是对于远程服务,它还负责封装远程服务的远程性,比如远程调用如何完成的细节。第二个职责意味着远程网关应该包含一个单独的元素来处理它,我称之为连接。

在这种情况下listAvailableSlots可能是对配置中提供的某个URL的远程调用。

equipmentBookingService.ts……

export async function listAvailableSlots(equipmentCode: string, duration: number, isEmergency: boolean): Promise {const url = new url (config['equipmentServiceRootUrl'] + '/availableSlots') const params = url. searchparams;参数个数。集(“持续时间”,duration.toString())参数。集(isEmergency, isEmergency.toString())参数。const response = await fetch(url) const data = await response.json()返回数据}

配置根URL允许我们通过提供不同的根URL来针对测试实例或存根服务测试系统。这很好,但通过操纵网关,我们可以完全避免远程调用,这可以显著加快测试速度。

连接还使用调用远程调用的机制(在本例中是JavaScript的获取API)处理麻烦。外部网关根据远程API处理网关接口到远程签名的转换,而连接接受该签名并将其表示为HTTP get。把这两个任务分开来做,每个任务都很简单。

然后在构建时将此连接添加到网关类。然后公共函数使用在连接中传递的这个。

类BoneFusionGateway……

private readonly conn:连接构造函数(conn:Connection) {this. conf . conf . conf . conf . conf。conn = conn} async listSlots(length: Duration): Promise {const slots = await this. conf . conf . conf;conn("BFSN", length.toMinutes(), false)返回slot .map(convertSlot)}

网关通常在同一个基础连接上支持多个公共函数。因此,如果我们的治疗应用程序后来需要保留血液过滤机,我们可以向网关添加另一个功能,该功能将使用相同的连接功能,但使用不同的设备代码。网关还可以将来自多个连接的数据合并到一个公共函数中。

当这样的服务调用需要一些配置时,通常明智的做法是将其与使用它的代码分开进行。我们希望治疗计划预约代码能够简单地使用网关,而不需要知道应该如何配置它。一个简单而有用的方法是使用服务定位器。

类ServiceLocator……

boneFusionGateway: boneFusionGateway

serviceLocator.ts……

export让ServiceLocator: ServiceLocator

配置(通常在应用程序启动时运行)

theServiceLocator。boneFusionGateway = new boneFusionGateway (listAvailableSlots)

使用网关的应用程序代码

const slots = await theservicellocator . bonefusiongateway . listslots (Duration.ofHours(2))

有了这种设置,我就可以像这样为连接编写一个带有存根的测试

它('存根连接',async函数(){const input: ebs。Slot[] = [{duration: 120, equipmentCode: "BFSN", equipmentd: "BF-018", date: "2020-05-01", time: "13:00", emergencyOnly: false}, {duration: 180, equipmentCode: "BFSN", equipmentd: "BF-018", date: "2020-05-02", time: "08:00", emergencyOnly: false}, {duration: 150, equipmentCode: "BFSN", equipmentd: "BF-019", date: "2020-04-06", time:"10:00", emergencyOnly: false},] servicelocator . "boneFusionGateway = new boneFusionGateway (async () => input) const expected: Slot[] = [{duration: duration . ofhours (2), date: LocalDate. value . value . value . value . value . value . value . value . value。of(2020, 5,1), time: LocalTime.of(13,0), model: new EquipmentModel("Marrowvate D12")}, {duration: duration . ofhours (3), date: LocalDate. of(3,0), model: new EquipmentModel("Marrowvate D12")}, {duration: duration . ofhours (3), date: LocalDate. of(3,0)的(2020, 5,2), time: LocalTime.of(8,0), model: new EquipmentModel("Marrowvate D12")}, ] expect(await suitableSlots()).toStrictEqual(expected) });

以这种方式存根允许我编写测试,而根本不需要执行远程调用。

然而,根据网关正在进行的翻译的复杂性,我可能更喜欢用应用程序的语言而不是远程服务的语言编写测试数据。我可以用这样的测试来验证suitableSlots移除带有错误装备模型的插槽。

它('存根网关',async function() {const stubGateway = new StubBoneFusionGateway() theServiceLocator。boneFusionGateway = stubGatewaylistSlotsData = [{duration: duration . ofhours (2), date: LocalDate. listSlotsData = [{duration: duration . ofhours (2), date: LocalDate. date];的(2020, 5,1), time: LocalTime.of(12,0), model: new EquipmentModel("Marrowvate D10")}, // not suitable {duration: Duration.ofHours(2), date: LocalDate.of(2020, 5,1), time: LocalTime.of(13,0), model: new EquipmentModel("Marrowvate D12")}, {duration: Duration.ofHours(3), date: LocalDate.of(2020, 5,2), time: LocalTime.of(8,0), model: new EquipmentModel("Marrowvate D12")}, ] const expected: Slot[] = [ {duration: Duration.ofHours(2), date: LocalDate.of(2020, 5,1), time: LocalTime.of(13,0), model: new EquipmentModel("Marrowvate D12")}, {duration: Duration.ofHours(3), date: LocalDate.of(2020, 5,2), time: LocalTime.of(8,0), model: new EquipmentModel("Marrowvate D12")}, ] expect(await suitableSlots()).toStrictEqual(expected) });
class StubBoneFusionGateway extends BoneFusionGateway {listSlotsData: Slot[] = [] async listSlots(length: Duration): Promise {return this。listSlotsData}构造函数(){super (async() =>[]) //未使用的连接,但需要类型检查}}

对网关进行存根可以更清楚地了解其中的应用程序逻辑suitableSlots在这种情况下,过滤掉Marrowvate D10。但是当我这样做时,我并不是在测试网关内部的转换逻辑,所以我至少需要在连接级别上对存根进行一些测试。如果远程系统的数据不是太难跟踪的话,我也许可以通过断掉连接而逃脱惩罚。但是,根据我正在编写的测试,在两个点上都能够存根通常是有用的。

我的编程平台可能支持某种形式的存根直接用于远程调用。例如JavaScript测试环境开玩笑允许我用它的模拟函数存根所有类型的函数调用。我可以使用什么取决于我使用的平台,但正如您所看到的,在没有任何额外工具的情况下,设计具有这些挂钩的网关并不困难。bet188足球

当像这样存根远程服务时,使用它是明智的合同的测试为了确保我对远程服务的假设与服务所做的任何更改保持同步。

示例:重构访问Yo188足球比分直播uTube的代码以引入网关(Ruby)

几年前我写了一篇文章使用一些代码访问YouTube的API来显示视频的一些信息。我展示了代码如何将不同的关注点纠缠在一起,并重构代码以清晰地将它们分开——在过程中引入了一个网关。它提供了如何将网关引入现有代码库的一步一步的解释。

确认

(Chris) Chakrit Likitkhajorn, Cam Jackson, Deepti Mittal, Jason Smith, Karthik Krishnan, Marcelo de Moraes Leite, Matthew Harward和Pavlo Kerestey在我们的内部邮件列表中讨论了这篇文章的草稿。

重大修改