大约一年前,我们的工作流程团队开始开发跨业务多个领域的新应用。我们面临着一个有趣的挑战:是从头开始构建应用程序的核心,同时还需要使用许多不同系统中存在的数据。
我们需要的一些数据点,例如有关电影,制作日期,员工和拍摄地点的数据,分布在实现各种协议的许多服务中:gRPC,JSON API,GraphQL等。现有数据对于我们应用程序的行为和业务逻辑至关重要。我们从一开始就需要高度集成。
可切换的数据源
将可见性引入我们产品的早期应用程序之一是作为单体构建的。单体架构允许快速开发和快速更改。某一时刻,有30多个开发人员正在使用它,并且它具有300多个数据库表。
随着时间的流逝,应用程序从广泛的服务产品演变为高度专业化的产品。这导致决定将单体分解为特定的服务。该决定并非针对性能问题,而是针对所有这些不同领域设置了界限,并使专门团队能够独立开发针对特定领域的服务。
单体仍然提供了我们为新应用所需的大量数据,但我们知道单体将在某个时候分解。我们不确定分手的时间,但是我们知道分手是不可避免的,我们需要做好准备。
因此,我们一开始可以利用来自单体的某些数据,因为它们仍然是真实的来源,但要准备将这些数据源在联机后立即交换给新的微服务
利用六角形架构
在不影响业务逻辑的情况下我们还需要支持交换数据源的能力,因此需要将这两者分离。我们决定根据 Hexagonal Architecture 和Bob叔叔的 Clean Architecture 背后的原理来构建我们的应用程序。
六边形架构思想是将输入和输出置于我们设计的边缘。业务逻辑不应该依赖于我们公开REST还是GraphQL API,也不应该依赖于我们从何处获取数据,不依赖于数据库,通过gRPC或REST公开的微服务API,或者仅仅是一个简单的CSV文件。
该模式使我们能够将应用程序的核心逻辑与外界的关注隔离开。将我们的核心逻辑隔离开意味着我们可以轻松更改数据源详细信息,而不会造成重大影响或无需将主要代码重写为代码库。
在具有清晰边界的应用程序中,我们还看到的主要优势之一是测试策略-我们的大多数测试都可以验证我们的业务逻辑,而无需依赖易于更改的协议。
定义核心概念
借鉴六边形/六角形结构,定义业务逻辑的三个主要概念是实体,存储库和交互器。
使用这三种主要类型的对象,我们可以定义业务逻辑,而无需任何知识或关心数据保存在何处以及如何触发业务逻辑。业务逻辑之外是数据源和传输层:
使用传统的分层体系结构,我们将使所有依赖项指向一个方向,上面的每一层都取决于下面的层。传输层将取决于交互程序,交互程序将取决于持久层。
在“六角体系结构”中,所有依赖点都向内指向-我们的核心业务逻辑对传输层或数据源一无所知。传输层仍然知道如何使用交互器,数据源知道如何符合存储库接口。
这样,我们就可以为其他Studio系统的不可避免的更改做好准备,并且只要需要进行更改,就很容易完成交换数据源的任务。
切换数据源
交换数据源的需求比我们预期的要早:我们突然遇到了一个单体的读取限制,并且需要将某个实体的特定读取切换到在GraphQL聚合层上公开的较新的微服务。微服务和单体保持同步,并且从一个服务或另一个服务读取的数据相同,产生的结果相同。
我们设法在2小时内将读取数据从JSON API传输到GraphQL数据源。
我们之所以能够如此快地完成它,主要原因是六角结构(通过适当的抽象,就很容易更改数据源)。我们没有让任何持久性细节泄漏到我们的业务逻辑中。我们创建了一个实现存储库接口的GraphQL数据源。一个简单的单行(one-line)变化是所有我们需要开始从不同的数据源读取。
此时,我们知道六角建筑已经为我们发挥作用了。
单行更改的很大一部分在于,它可以减轻发布风险。如果下游微服务在初始部署时失败,则回滚非常容易。这也使我们能够分离部署和激活,因为我们可以决定通过配置使用哪个数据源。
隐藏数据源详细信息
该体系结构的一大优势是我们能够封装数据源实现细节。我们遇到了这样一种情况:我们需要一个尚不存在的API调用-服务具有一个API来获取单个资源,但没有实现批量获取。与提供API的团队进行交流后,我们意识到此端点需要一些时间才能交付。因此,我们决定在构建此端点的同时,提出另一种解决方案来解决该问题。
我们定义了一个存储库方法,该方法可以在给定多个记录标识符的情况下获取多个资源-并且该方法在数据源上的初始实现向下游服务发送了多个并发调用。我们知道这是一个临时解决方案,数据源实现的第二个要点是在实现后使用批量API。
这样的设计使我们能够继续满足业务需求,而不会产生太多技术债务,也无需事后更改任何业务逻辑。因为我们的业务逻辑不需要了解特定的数据源限制。
测试策略
当我们开始尝试六角结构时,我们知道我们需要提出一种测试策略。我们知道,要获得高速发展的先决条件就是拥有可靠且超快速的测试套件。我们认为它不是一个很好的选择,而是必须具备的。
我们决定在三个不同的层次上测试我们的应用程序:
我们不测试存储库,因为它们是数据源实现的简单接口,并且我们很少测试实体,因为它们是定义了属性的普通对象。我们测试实体是否具有其他方法(不涉及持久层)。
与可以轻松在任何机器上运行的测试套件一起工作非常好,并且我们的开发团队可以在不中断的情况下处理其日常功能。
延迟决策
在将数据源切换到不同的微服务时,我们处于非常有利的位置。关键好处之一是,我们可以延迟有关是否以及如何存储应用程序内部数据的某些决定。根据功能的用例,我们甚至可以灵活地确定数据存储的类型-是关系型还是文档型。
鲍伯叔叔说得很好:
好的架构的目的是延迟决策。为什么?因为当我们推迟做出决定时,我们需要更多的信息来做出决定。
在项目开始时,关于我们正在构建的系统的信息量最少。我们不应该将自己锁定在一个不知情的决定而导致 项目悖论 的体系结构中。
我们现在做出的决定对我们的需求很有意义,并且使我们能够快速行动。六角结构的最好之处在于,它可以使我们的应用程序灵活地适应将来的需求。