用六边形架构构建可维护系统 - IlkkaSeppälä


传统分层架构存在的谬误:这篇博客文章是关于实施Alistair Cockburn的六角形架构。我们先来谈谈分层架构。这是一种众所周知的架构模式,它将应用程序组织到每个都有其特定用途的层中。数据库层负责数据事务,业务层负责业务逻辑,表示层处理用户输入。分层架构实现了所谓的关注分离原则,这导致了更多可维护的应用程序。对软件中某个区域的更改不会影响其他区域。

传统分层:
表现层  -- > 业务层 --->数据层

这种构建应用程序的方式可以被认为是简单而有效的。但它也有几个缺点。当您看到使用分层架构实现的应用程序时,应用程序核心在哪里?是数据库吗?也许应该是业务逻辑,一些小东西分散到表现层。这是分层的典型问题:没有应用程序核心!只有层和核心逻辑分散在这里或那里。当业务逻辑开始泄漏到表现层时,无法在没有用户界面的情况下测试应用程序。

一个核心,多个端口和适配器
Hexagonal Architecture通过围绕一个核心来构建应用程序来解决这个问题。主要目标是创建完全可测试的系统,这些系统可以由用户,程序和批处理脚本在数据库中独立驱动。
仅核心并不是很有用。必须要驱动这个应用程序,调用业务逻辑方法。它可以是HTTP请求,自动测试或集成API。这些用于驱动我们称为主端口的应用程序的接口和使用它们的模块是主适配器。
此外,核心有其依赖性。例如,可能存在核心要求检索和更新数据的数据存储模块。由核心驱动的这些模块的接口称为应用程序的辅助端口。
辅助端口可以具有一个或多个实现。例如,可能存在用于测试的模拟数据库和用于运行应用程序的真实数据库。辅助端口实现称为辅助适配器。这里有别名为Ports and Adapters for Hexagonal Architecture。描述相同概念的其他建筑模式是Uncle Bob的Clean Architecture和Jeffrey Palermo的Onion Architecture

六边形架构:

该图显示了领域这个核心如何被六边形多边上的端口包围。实际的端口数量不必是六个,它可以更少,也可以更多,具体取决于应用程序的需求。在外六边形层上驻留主要和次要适配器。
“裸体对象Naked Objects ”设计模式被认为是六边形体系结构的实现。Apache Isis框架使用裸体对象,用户定义领域对象,框架自动生成用户界面和REST API。

彩票系统
接下来,我们将通过构建彩票系统来演示六边形架构。彩票系统将提供两个主要端口:一个用于用户提交彩票,另一个用于系统管理员执行抽奖。源码Github
辅助端口包括彩票数据库,用于电汇的银行业务以及用于处理和存储彩票事件的事件日志。系统的六边形可以在下图中看到。


从核心概念入手:
我们从系统核心开始实现。首先,我们需要确定彩票系统的核心概念。可能最重要的一个核心是彩票。在“彩票”这个模型中,您应该选择号码并提供您的联系方式。这导致我们编写以下类。

LotteryTicket类包含LotteryNumbers和PlayerDetails。

LotteryNumbers类包含保存指定数字或生成随机数的方法,并测试数字是否与另一个LotteryNumbers实例相等。PlayerDetails是一个简单的价值对象,包含玩家的电子邮件地址,银行帐号和电话号码。

核心业务逻辑:
现在我们有了代表我们核心概念的名词,我们需要实现定义系统工作方式的核心业务逻辑。在类LotteryAdministration和LotteryService类中,我们编写了彩票播放器和系统管理员所需的方法。
管理员LotteryAdministration类可以使用resetLottery()方法开始新的彩票轮次。在这个阶段,玩家将他们的彩票提交到数据库中,并且当时间到期时,管理部门要求performLottery()绘制中奖号码并检查每张奖金的门票。
彩票玩家用于submitTicket()提交彩票轮票。抽签结束后checkTicketForPrize()告诉球员他们是否赢了。
LotteryAdministration并且LotteryService依赖于彩票数据库,银行和事件日志端口。我们使用Guice依赖注入框架为每个目的提供正确的实现类。核心逻辑在LotteryTest中进行测试。

玩家的主要适配器:
现在核心实现准备就绪,我们需要为玩家定义主适配器。我们引入ConsoleLottery类来提供允许玩家与彩票系统交互的命令行界面。
它有命令查看和转移银行帐户资金,提交和检查彩票。

管理员的主适配器:
我们还需要定义面向适配器的彩票管理员。这是另一个命名的命令行界面ConsoleAdministration。
界面的命令允许我们查看提交的票证,执行抽奖抽奖和重置彩票数据库。

银行的二级端口:
接下来,我们实现辅助端口和适配器。第一个是银行支持,使我们能够操纵银行账户资金。为了解释这个概念,玩家可以在彩票上写下他的银行账号,如果彩票赢了彩票,系统会自动汇款。
银行端口有两个用于不同目的的适配器。
第一个InMemoryBank是基于简单HashMap的测试实现。彩票服务的银行账户被静态初始化,以包含足够的资金来支付奖金,以防一些彩票获胜。
另一个适配器MongoBank基于Mongo,适合生产使用。运行任一命令行界面都使用此适配器。

事件日志的辅助端口:
另一个辅助端口是彩票事件日志。当玩家提交彩票并进行抽奖时发送事件。
我们有两个适用于此端口的适配器:第一个StdOutEventLog用于测试,只是将事件发送到标准输出。第二种MongoEventLog是更复杂,具有持久存储并且基于Mongo。

数据库的辅助端口:
最后一个辅助端口是数据库。它包含存储和检索彩票的方法。
该端口有两个适配器。这LotteryTicketInMemoryRepository是一个模拟数据库,仅将其内容保存在内存中,用于测试。该MongoTicketRepository用于生产运行和在应用程序重新启动提供持久存储。

彩票应用:
有了所有部分,我们创建了一个命令行应用程序来驱动彩票系统。测试应用程序使用管理方法开始抽奖轮次并开始从玩家收集彩票。一旦提交了所有彩票,就执行彩票抽奖并检查所有提交的彩票以获得胜利。
运行测试应用程序会生成以下输出:

Lottery ticket for monica@google.com was submitted. Bank account 265-748 was charged for 3 credits.
Lottery ticket for lisa@google.com was submitted. Bank account 024-653 was charged for 3 credits.
Lottery ticket for harriet@google.com was submitted. Bank account 842-404 was charged for 3 credits.
Lottery ticket for ian@google.com was submitted. Bank account 663-765 was charged for 3 credits.

最后的话
使用六边形体系结构实现的应用程序是一种维护和使用的乐趣。框架,用户界面和数据库等实现细节被推出核心,应用程序可以在没有它们的情况下工作。我们可以清楚地指出六边形的中心,并说这是我们的应用程序,它使用这些技术来实现子模块接口。限制仅通过端口发生的通信会强制应用程序生成可测试和可维护的代码。
Hexagonal Architecture的完整演示应用程序可在Java Design Patterns Github存储库中找到。