为什么外部系统很困难
在本节中,我们将回顾六边形架构方法背后的驱动力——处理外部系统的困难。对外部系统的依赖在开发中会引起问题。解决方案引出了一个很好的设计方法。
让我们看看处理外部系统的一种简单方法。我们用户的任务是从数据库中提取本月的销售报告。我们将编写一段代码来完成这个任务。软件设计如下:

在这个设计中,我们以通常的方式将销售数据存储在数据库中。我们编写一些代码来代表用户提取报告。它是一个单一的代码,作为一个单一的步骤完成整个工作。它将连接到数据库,发送查询,接收结果,进行一些处理,并格式化结果以供用户阅读。
从好的方面来看,我们知道这种编码风格是有效的。它将实现向用户提供销售报告的目标。从坏的方面来看,这段代码结合了三个不同的职责——访问数据库、执行逻辑和格式化报告。它可能会将 SQL 语句与 html5
标签混合在一起,以生成格式化的报告。正如我们在前一章中看到的,这可能会使未来在一个领域的代码更改波及并影响其他领域。理想情况下,这不应该发生。但真正的挑战是为这段代码编写测试。我们需要解析和理解我们向用户发送报告的格式。我们还需要直接与该数据库合作。
在以下小节中,我们将回顾外部系统对测试提出的一些更广泛的挑战。这些包括环境问题、意外事务、不确定数据、操作系统调用和第三方库。
环境问题带来的麻烦
我们的软件运行的环境经常带来挑战。假设我们的代码从数据库中读取数据。即使代码是正确的,由于我们无法控制的环境问题,它可能无法读取该数据。这些问题包括:
-
网络连接中断:许多原因可能导致这种情况。在本地,网络电缆被错误地拔出。也许数据库托管在互联网上的某个地方,而我们的 ISP 已经中断了连接。
-
电源故障:数据库服务器上的电源故障,或本地网络交换机的电源故障足以使数据库无法访问。
-
设备限制:也许数据库服务器本身已经用尽了磁盘空间,无法运行。也许我们编写的查询以某种方式击中数据库,需要很长时间才能完成,可能是由于缺少索引。
无论原因是什么,如果我们的代码无法访问数据库中的数据,它将无法工作。由于这是一种可能性,为我们的报告生成代码编写测试变得更加困难。
即使我们的代码可以访问数据库中的数据,在测试中处理它也不容易。假设我们编写一个测试,通过读取用户名来验证我们可以正确读取生产数据库。我们期望读取什么用户名?我们不知道,因为测试无法控制添加了什么数据。可用的用户名将是真实用户添加的任何名称。我们可以让测试添加一个已知的测试用户名到数据库中——但这样,我们就创建了一个真实用户可以与之交互的虚假用户。这根本不是我们想要的。
数据库存储数据,给我们的测试带来进一步的问题。假设我们针对一个测试数据库编写一个测试,该测试首先写入一个测试用户名。如果我们之前运行过这个测试,测试用户名将已经存储在数据库中。通常,数据库会报告重复项错误,测试将失败。
针对数据库的测试需要清理。任何存储的测试数据必须在测试完成后删除。如果我们尝试在测试成功后删除数据,如果测试失败,删除代码可能永远不会运行。我们可以通过在测试运行之前始终删除数据来避免这种情况。这样的测试运行速度会很慢。
意外触发真实事务的测试
当我们的代码仅限于访问生产系统时,每次我们使用该代码时,都会在生产中发生一些事情。支付处理器可能会发出费用。真实的银行账户可能会被扣款。警报可能会被激活,导致真实的疏散。在夏威夷的一个著名例子中,系统测试触发了一条真实的短信,称夏威夷正受到导弹攻击——实际上并没有。这是非常严重的事情。
夏威夷虚假导弹攻击警告
有关此测试出错的示例的详细信息,请参见 https://en.wikipedia.org/wiki/2018_Hawaii_false_missile_alert 。 |
意外的真实交易可能导致公司的实际损失。它们可能最终成为企业 3R 的损失——收入、声誉和保留。这些都不好。我们的测试绝不能意外触发生产系统的真实后果。
我们应该期待什么数据?
在我们的销售报告示例中,编写测试的最大问题是我们需要提前知道月度销售报告的正确答案是什么。当我们连接到生产系统时,我们如何做到这一点?答案将是销售报告所说的任何内容。我们没有其他方法知道。
我们需要销售报告代码正确工作才能测试销售报告代码是否正确工作的事实在这里是一个大问题!这是一个我们无法打破的循环依赖。
操作系统调用和系统时间
有时,我们的代码可能需要调用操作系统来完成其工作。也许它需要不时删除目录中的所有文件,或者它可能依赖于系统时间。一个例子是日志文件清理实用程序,它每周一凌晨 02:00 运行。该实用程序将删除 /logfiles/
目录中的每个文件。
测试这样的实用程序会很困难。我们必须等到周一凌晨 02:00 并验证所有日志文件是否已被删除。虽然我们可以做到这一点,但这并不是很有效。找到一个更好的方法,让我们可以随时测试,理想情况下不删除任何文件,那将是非常好的。
与第三方服务的挑战
在商业软件中,一个常见的任务是接受客户的付款。为此,我们不可避免地使用第三方支付处理器,例如 PayPal
或 Stripe
。除了网络连接的挑战外,第三方 API 还给我们带来了进一步的挑战:
-
服务停机时间:许多第三方 API 会有一段时间的预定维护,期间服务不可用。这对我们来说意味着 “测试失败”。
-
API 更改:假设我们的代码使用 API 版本1,而 API 版本2被推送到线上。我们的代码仍将使用版本1的调用,这些调用在 API 版本2上可能不再有效。现在,这被认为是不良实践——它被称为破坏已发布的接口——但它可能并且确实会发生。更糟糕的是,对于我们的单一代码,版本2的更改可能会导致我们代码中的各处更改。
-
慢速响应:如果我们的代码向外部服务发出 API 调用,总是有可能响应会比我们的代码预期的晚。我们的代码通常会以某种方式失败,并导致测试失败。
当我们将外部服务和单一的整体代码混合在一起时,存在许多挑战,这使维护和测试变得复杂。问题是我们可以做些什么?下一节将探讨依赖倒置原则如何帮助我们遵循一种称为六边形架构的设计方法,使外部系统更容易处理。