由_DALL·E_生成的
在当今快速发展的环境中,高效地组织代码对于构建可扩展、可维护和可测试的应用程序至关重要。尽管许多开发人员倾向于使用分层架构,这经常会带来紧密耦合、领域渗漏以及测试困难等问题。
六角架构 是一种模式,它提供了业务逻辑和技术细节之间清晰分离的灵活性,带来更大的灵活性。
一些背景信息:
我的Hexagonal架构之旅始于2020,感谢我的同事Alexey,是他让我了解到Hexagonal架构的概念。当时,我们只能找到一些理论文章,并为了模糊的例子,我们花了大量的时间进行实验。在这篇文章中,我想拆解Hexagonal架构,并提供一个使用Java和Spring的实用指南** .
分层架构一直是构建应用程序的默认方式,并且它一直很好地为我们服务。对于许多简单的项目来说,它提供了一种熟悉且逻辑清晰的方式来组织代码,将控制器、服务和存储库等功能区分开。随着应用程序的增长和演变,分层架构可能逐渐成为一个障碍,而不是优势。让我们来看看其中一些常见的问题。
- 紧耦合:分层架构中的组件常常变得相互依赖,使得在不影响其他层的情况下适应或替换特定层变得困难。这种紧耦合在应用程序需要演进或与新技术集成时降低了灵活性。
- 测试挑战:业务逻辑往往分散在多个层次上,这使得测试变得复杂,并需要依赖复杂的环境设置。
- 灵活性降低:更改或更新领域模型,尤其是与特定技术代码绑定时,可能非常昂贵。技术堆栈变化时,更新嵌入技术层的核心领域逻辑变得复杂且容易出错。
当我们启动新的服务时,我们经常会构建一个干净、有条理的包结构,遵循分层设计。然而,尽管我们尽了最大努力,随着时间的流逝,这种结构往往会变得混乱。随着我们添加新功能、响应变化的需求或集成其他系统,严格的界限逐渐变得模糊,导致包结构变得混乱。
向着右方巨大的震动发展!如何应对这种情况?从哪里开始呢?
图1. 从分层架构演化到复杂的包结构
(由 DALL·E 生成)
分层架构本身并不是一种坏的设计或反模式。它是一种熟悉且默认的选择,通常在初期表现得很好。但对于复杂且不断演进的应用程序而言,改变方法可以带来显著的长期优势。
什么是六边形架构设计?六边形架构并不是一个全新的概念;相反,它是一种转变视角,将领域模型(即我们对现实世界的思维表示)置于中心。通过专注于领域,六边形架构有助于与技术及非技术利益相关者之间的协作,从而使开发与业务需求更易对齐。从根本上说,六边形架构是一种以领域为中心的方法。其主要目标是使应用程序的核心领域独立于诸如API和数据库等技术细节。这种独立性是通过围绕应用程序的三个主要组件进行组织而实现的。
- 领域:应用程序的核心,包含业务的核心逻辑和模型。领域是唯一真正理解业务并承载业务规则定义的部分。
- 端口:允许领域与外界通信的接口。端口定义了期望的交互方式,使领域能够保持与特定技术细节的隔离。
- 适配器:将领域与外部系统(如数据库、API 和用户界面)连接起来的实现。适配器负责处理与这些系统交互的具体细节,并将外部数据格式转换为领域自身的模型格式。
这种结构让业务逻辑与外部系统隔离开来,保持独立。适配器可以轻松更换,这使得更换或添加新技术而不影响核心业务变得更加简单。
图2:抽象的六角形结构
不要尝试将其用于具有简单业务逻辑的小项目!
这样做只会让代码变得更复杂,你将不得不至少使用三个不同的模型,并不断在这几个模型之间进行映射,而不是直接将你的DTO存储到数据库或传递给其他服务。
图3. 酒吧点单系统示例(使用DALL·E创建)
为了将六边形架构应用到实际中,让我们通过一个酒吧系统的实例来看看如何实现。以下是一些关键功能(或例如用户故事):
- 接单:接客户的单并管理。
- 找配方:找各种饮料的配方。
- 调制饮料:调制客户点的饮料,按照配方来。
在传统的分层架构里,结构可能看起来像这样(在左边),而右边则展示了相同功能在六边形架构中是如何组织的。
图4. 从分层包结构转变为六边形设计
入站端口和适配器: 控制器通过“in 端口”连接到领域,处理来自外部的请求或交互(例如,HTTP 请求)。在这种情形下,订单请求将通过一个 in 端口 进行处理,该端口由 OrderController 使用,并由领域中的 MixologyService 实现。
领域模型:这部分包含了核心的业务逻辑。例如,MixologyService这样的服务会处理调制饮料的逻辑,使用Cocktail模型表示业务逻辑。它还会与外部系统如食谱和订单进行交互。
出端口:“出端口”定义了域与外部系统如何互动。例如,在本例中,RecipeCatalogOutPort 表示用于访问食谱目录的接口,域服务使用该接口来查找饮料配方。
图5. 一个示例映射到六边形架构(Hexagonal Architecture)。
让我们通过代码片段来看看这个结构的实际表现吧!
OrderController 是一个适配器,它将外部世界(HTTP 请求,)与领域模型连接起来。它与 CocktailOrderInPort,一个 输入端口 进行交互,而不关心领域逻辑的具体实现。
@RestController // 注解@RestController表示该类是一个REST风格的控制器
class OrderController { // 订单控制器,处理与订单相关的请求
private CocktailOrderInPort cocktailOrderPort; // 定义一个私有成员变量,用于调用订单处理接口
@PostMapping // 注解@PostMapping表示该方法处理POST请求
public CocktailDTO sendOrder(@RequestBody String orderRequest){ // 发送订单方法,接收订单请求并处理
Cocktail cocktail = cocktailOrderPort.heyGiveMeDrink(orderRequest); // 获取饮料的方法,根据订单请求返回鸡尾酒
return CocktailDTO.from(cocktail); // 将Cocktail对象转换为DTO对象,DTO是数据传输对象,用于传输鸡尾酒的信息
}
}
在这个设置中,MixologyService 代表领域服务,实现了输入端口(CocktailOrderInPort)。在处理订单时,它使用 RecipeCatalogOutPort 来获取配方信息,将领域逻辑与如数据库实现等技术细节隔离。为了简化起见,酒吧的原料是无限的(假设),并省略了 BarStoragePort。
@Service
public class MixologyService implements CocktailOrderInPort {
private final RecipeCatalogOutPort recipeCatalogPort;
private final DomainEventObserverPort observer;
@Override
public Cocktail 嘿,给我来一杯(String order) {
var maybeCocktail = recipeCatalogPort.find(order);
if (maybeCocktail.isEmpty()) {
observer.observe(MixologyEvent.failedToFindRecipe(order));
throw new RuntimeException("我们这里没有能调制的配方哦,您要的可能是:" + order);
}
var cocktail = maybeCocktail.get();
observer.observe(MixologyEvent.cocktailMixed(cocktail.name()));
return cocktail;
}
}
六边形架构(Hexagonal Architecture)的关键好处
六边形架构强调以领域为中心,其中核心逻辑受到保护,不受外部因素干扰。这带来了很多好处:
- 清晰的职责分离:领域模型与特定技术组件保持独立,这简化了领域驱动设计的过程。
- 增强了灵活性:更换适配器,比如更换数据库或外部 API,都非常简单。
- 提高了可测试性:端口和适配器通过将领域逻辑与输入输出操作隔离,简化了单元测试。
保持一致性是确保六边形架构完整性的关键。如果没有定期维护,即使是很结构化的代码库也会随时间逐渐退化,领域规则、接口边界和编码风格可能会变得模糊或漂移。为了防止这种情况的发生,建立严格的编码标准和定期代码审查以确保符合架构原则。然而,保持领域与技术细节的隔离需要更多努力,这时ArchUnit就可以派上用场了。
ArchUnit 是一个用于验证架构规则的Java库,允许你定义约束以强化六边形架构(端口和适配器架构)的设计原则。它帮助制定围绕包结构和控制依赖关系的规则,确保:
- 领域服务和适配器都应无法从其他层被访问,从而使业务逻辑与技术细节保持独立。
- 端口是领域与适配器之间唯一的接口,确保所有外部通信都通过一个明确定义的边界。
- 领域模型代表核心业务概念,充当各层之间的“纽带”。
图6: 用于六边形架构(Hexagonal 架构)的 ArchUnit 规则
这些规则避免技术细节意外泄露到领域层,从而保持了六边形架构所需的清晰界限。
整合 ArchUnit 可以自动帮助我们强制执行代码中的架构边界,减少对人工检查的需求。将这些测试集成到 CI/CD 流水线中,这样,随着项目的增长,您可以保持架构原则的应用一致性。
结尾:我们在代码中构建和解释现实世界概念的方法稍作改变,可以增强解决问题的清晰性,并加强与产品相关方的合作。
六边形架构 通过清晰地将业务逻辑与技术实现分离,提供了一种灵活、易于维护且可测试的应用设计方式。虽然它需要仔细的设计和严格的纪律,但从长远来看,尤其是对于复杂应用而言,最初的投入是值得的。
将 六边形架构 与 ArchUnit 和 依赖注入 模式(使用 Spring)结合,进一步强化了这种做法。ArchUnit 强制执行架构边界,确保一致性,而 Spring 的依赖注入则平滑地连接各个模块,支持模块化、可扩展的设计,让业务逻辑独立于技术细节。
本文中使用到的代码示例可在GitHub上找到。