转载并翻译技术博客:https://netflixtechblog.com/ready-for-changes-with-hexagonal-architecture-b315ec967749
随着Netflix Originals产品每年不断的增长,我需要构建能够提高开发效率的于应用程序。 我们庞大的开发团队开发了许多的应用程序,这些程序从播放音频到播放视频,从脚本内容获取、交易谈判和供应商管理到调度、简化制作工作流程等。
有着高度集成需求的app
大约在一年之前,我门的Studio Workflows团队开始工作在一个跨多个业务领域的app上。有一个有趣的挑战,我们需要从头开始开发我们的核心程序,但是我们需要的数据是保存在许多不同的系统中。
某些我们需要的数据点,例如电影、产品、员工、位置等数据,分布在不同的系统中,这些系统使用了不同的协议来实现,例如: gRPC, JSON API, GraphQL等,这些数据对于我们系统的行为和业务逻辑是非常重要的,程序在开发初期集成度就很高。
切换数据源
有一个前期被开发成单体应用程序为我们的程序带来了经验。这个单体应用程序允许快速开发和快速更改,但是并不需要空间的掌握并不需要。在某个时间点,超过30名开发人员在上面工作,并且这个单体应用程序的表超过300张。
随着时间的推移,这个单体应用程序从可以处理很多业务的服务演变成了高度专业化的产品。这导致做了一个拆分单体应用到具体服务的决定。做这个决定不是应用性能问题,而是不同业务领域的边界和为了让专业团队能够独立的开发不同特定领域的服务。
我们新的应用程序需要的大量数据仍然来自这个单体应用,但是我们知道这个单体应用将会被拆分在未来的某个时间点。我们不能保证在那个时间点会拆分,但是我们知道这一定发生,并且我们需要做一些准备。
因此,我们可以刚开始使用来自这个单体应用的数据,因为它现在仍然是数据源,不过做好随时切换到其他别的数据源的准备当这些数据源上线。
使用六边形架构
我们需要一个能力能够支持切换数据源在不影响业务逻辑的情况下,因此我们知道需要让业务逻辑和数据源解耦。我们决定基于六边形架构构建应用程序。
六边形架构的思想是把输入和输出作为我们设计的边界。业务逻辑不应该依赖暴漏一个REST还是GraphQL API,并且也不应该依赖我们的数据从哪里来,通过数据库、gRPC或者REST为服务的API,还是仅仅是一个CSV文件。
这个设计模式让我们可以讲我们程序的核心逻辑与外界影响隔离开。让核心逻辑隔离意味着我们可以很容易的改变数据源的细节在没有大的影响或者重写代码的情况下。
我们看到的让一个程序有着清晰的边界的一个主要的优势是我们的测试策略,我们测试的大部分能够验证业务逻辑在不依赖易变的协议情况下。
定义核心概念
使用六边形架构之后,有三个定义在我们业务逻辑中的核心概念,分别是Entities,Repositories,Interactors。
- Entities是领域对象,例如一个电影或者一个拍摄地点,它们不知道他们会被存储在哪里。
- Repositories是用来获取、创建和改变entities的接口。它们有一组可以和数据源交互并且返回单个entities或者一组entities的方法,例如UserRepository。
- Interactors是组织或者执行领域行为的类,可以想一个Service Object或者Use Case Object。它们实现了某个领域的复杂业务逻辑和验证逻辑。
有了这三个主要的对象类型,我们可以定义业务逻辑在不知道任何信息或者关心数据被保存在哪里以及业务逻辑怎么样被出触发。在业务逻辑之外的是数据源和传输层。 - Data Sources是对于不同存储实现的适配器。一个数据源可能会是SQL数据库的书配齐,也有可能是elastic search的适配器,或者是REST API适配器,甚至是某些简单的CSV文件或者Hash适配器。一个数据源实现了定义在Repository的方法并且有抓取和推送数据的实现
-
Transport Layer能调用一个Interactor去执行业务逻辑。对于我们的系统中我们认为它是作为输入的存在。在大多数微服务中常见的Transport Layer是HTTP API层和一组处理请求的controllers。通过把业务逻辑放在interactors中,我们不在耦合一个特定的Transport Layer或者Controller。Interactors不仅能够被一个controller出发,还可以被一个事件,一个cron job或者一个命令触发。
在一个传统的分层架构中,我们把所有的依赖都指向一个方向,上层依赖与下层。传输层依赖于Interactors,Interactors依赖与持久层。
在六边形架构中,所有的依赖都是向内的,我们的核心业务逻辑不知道任何事情关于传输层或者数据源。但是传输层知道怎么样使用Interactors,数据源知道怎么样实现repository接口。
有了这写,我们做好了准备应对随时切换到其他Studio系统,无论这些什么时候发生,切换数据源的任务都可以很容易去完成的。
切换数据源
切换数据源的需求到来的比我们想象中的要早,我们突然遇到了一个单体应用的读取限制,需要把一个用于一个entity的读取切换到一个向外暴露GraphQL聚合层的新微服务。这个微服务和那个单体应用在数据上面保持同步,从这个两个服务读取数据会产生相同的结果。
我们想方设法在两个小时内把从JSON API读取切换到了GraphQL。
我们能够切换的这么快的主要原因是六边形架构。我们没有让任何持久层的信息被业务逻辑感知。我们构建了一个实现了Repository interface 的GraphQL数据源。简单一行的改变是我们需要使用不同数据源的读取
在这个时候,我们知道了六边形架构为我们带来了好处。
这个很小改动的地方重要的地方在于它降低了上线的风险。在游微服务在初始部署失败的情况下它是非常容易回滚。这也让我们可以去解耦部署和激活,因此我们能够通过使用配置决定使用那个数据源。
隐藏数据细节
六边形架构的一个非常大的好处是我们能够封装数据源实现的细节。我们碰到一个案例,我们需要一个不存在的API调用,一个服务有一个API去获取单个资源但是并没有实现去批量获取资源。在跟提供这个API的团队沟通过之后,我们意识到需要这个接口需要一些时间才能上线。因此我们决定使用另外一个方案解决这个问题,当批量获取资源的接口还是开发中。
我们定义了一个Repository方法,可以在给定多个记录标识符的情况下获取多个资源。在初版的实现方法中,发送多个并行的请求给下游服务。我们知道这是一个临时方案,真正的方案应该是使用批量获取的接口去实现这个方法。
像这样的设计能够让我们满足业务需求在不积累很多技术债的情况下,并且也不需要改变任何业务逻辑。
测试策略
当我们开始给六边形架构做实验的时候,我们知道我们需要提出一种测试策略。我们知道实现快速开发的前提是拥有一个可靠且速度快的测试套件。我们认为它并不是可以有,而是一定得有。
我们决定测试我们的应用程序在三个层面上。
-
我们测试有着核心业务逻辑并且不依赖任何持久层和传输层的Interactors。我们使用依赖注入和repository的mock。这是我们对业务逻辑进行详细测试的地方,这是我门努力进行的大部分测试。
-
我们测试数据源来决定是够正确的集成了其他服务,是否它们能够满足repository interface,并且检测了错误的行为是怎样的。我们尽量减少这些测试的数量
-
我们又一个贯穿整个流程的测试规划,从传输层到Interactors、Repositories、data sources,最后到下游服务。这个规划测试了是否我们服务正确的集成到了一起。如果一个数据源是一个外面的API,我们命中这个endpoint并且记录响应(将响应存储在git里面),这是的我们的测试套件运行是非常快的在每次调用中。我们不想花太多时间在API层,通常是一个成功的case和一个是宝的case在每个领域下面。
我们不去测试repositories,因为它们是简单的需要被data source实现的接口,并且我们几乎不测entities,因为它们是定义了一些属性POJO,如果entities有方法会测试。
我们还有改进的余地,比如不ping我们依赖的任何服务,而是100%以来契约测试。上面一些测试套件里面,我们想办法让他们在单线程的环境下100秒内运行3000个测试用例。
使用可以在任何机器上都可以轻松运行的测试套件很好,并且我们的开发团队可以在不中断的情况下处理日常功能
延迟决定
我们在有一个有利的环境当我们需要切换数据源到不同的微服务。其中一个重要的好处是我们可以延迟一些决定,关于是否以及如何在我们的应用程序内部存储数据。基于功能的需求,我们甚至一定的灵活性缺决定使用那种数据存储类型----RDS或者Documents。
在项目的刚开始,我们只有很少的信息关于我们正在构建的程序,我们不应该在不知情的情况下做决策,导致使用了不合适的结构。
我们做的决定对于我们的需求是合理的,这是的我们的开发速度很快。六边形架构最好的地方在于它让我们的程序有很强的灵活性对于即将来临的需求。