六边形架构 Hexagonal Architecture
什么是六边形架构
六边形架构(Hexagonal Architecture),又称端口与适配器架构(Ports and Adapters),是由 Alistair Cockburn 在 2005 年提出的一种软件设计模式。它旨在将应用程序的核心业务逻辑与外部世界(如数据库、用户界面、第三方服务)完全隔离,使系统更易于测试、维护和演化。
其核心思想是:将应用程序视为一个被端口包围的六边形,外部请求只能通过这些端口进入内部,内部的业务逻辑也仅通过端口与外界交互。六边形的每条边代表一种交互通道,而适配器负责将具体的外部技术连接到对应的端口上。
核心概念:端口与适配器
端口(Port)
端口是应用程序定义的抽象接口,描述了一种与外界交互的契约。端口不关心具体的实现技术,它们只说明“我需要什么”或者“我能提供什么”。
端口分为两类:
- 驱动端(Driving Side)端口:由外界主动发起调用,用以触发应用程序的业务逻辑。例如:HTTP API 接口、命令行接口、消息队列监听器等。对应的端口抽象通常是应用服务接口或命令处理器接口。
- 被驱动端(Driven Side)端口:由应用程序主动调用,用以获取外部资源或发送数据。例如:数据库仓储接口、邮件发送接口、事件总线接口等。
适配器(Adapter)
适配器是端口的具体实现,负责将外部技术的细节适配为端口所要求的格式。适配器位于六边形之外,可以被随时替换而不影响核心逻辑。
- 驱动适配器:例如一个 REST 控制器,它接收 HTTP 请求,将其转换为调用应用服务的参数,并将返回结果格式化为 JSON 响应。
- 被驱动适配器:例如一个 MySQL 数据库实现,它具体执行 SQL 语句,并把查询结果转换为领域对象返回给核心层。
架构示意
虽然无法用图片直接展示,但可以用文字描述典型的六边形架构分层:
[ 外部世界 ] <——> [ 适配器层 ] <——> [ 端口接口 ] <——> [ 应用核心 ]
(Web/DB/CLI) (REST/MySQL/…) (接口定义) (领域+应用服务)
应用核心完全不知道外部世界的存在,所有依赖都由外向内指向核心。这种依赖方向保证了核心的独立性和可测试性。
与传统分层架构的对比
| 传统分层架构 | 六边形架构 |
|---|---|
| 上层依赖下层,依赖方向固定 | 依赖倒置,所有依赖指向核心 |
| 业务逻辑容易与技术细节耦合 | 技术与业务完全解耦 |
| 更换数据库或 UI 需要改动业务层 | 只需替换适配器,核心不变 |
| 测试通常需要启动整个栈 | 核心可以完全隔离测试 |
在传统三层架构(Controller → Service → Repository)中,Service 往往会直接依赖 Repository 的具体实现(如 JPA Repository),导致业务逻辑被框架绑架。而六边形架构强制在 Service 层只依赖接口,由适配器实现该接口并注入。
核心优势
- 技术无关性:可以推迟技术选型,先专注于业务建模。对于新技术,只需编写新的适配器。
- 高可测试性:核心领域逻辑可以在不依赖数据库、网络等任何外部资源的情况下进行单元测试。
- 可替换性:更换数据库、消息队列或前端框架时,仅需更换适配器,核心代码零改动。
- 关注点分离:业务规则清晰集中,不会夹杂技术细节,代码更易理解。
- 允许演进:随着需求变化,可以逐步添加新的端口和适配器,而不会影响现有功能。
实战要点与基本原则
1. 核心只依赖抽象,不依赖具体实现
编写领域服务时,只使用接口来定义所需的外部依赖(如 OrderRepository 接口),绝不使用具体的类(如 JpaOrderRepository)。
2. 适配器负责协议转换
适配器必须将外部协议格式的数据转换为核心需要的领域对象,以及将领域对象转换为外部所需的格式。例如,REST 适配器将 JSON 请求转为领域命令对象。
3. 按端口组织测试
- 驱动端测试:通过端口调用核心,验证业务规则。通常使用 mock 的被驱动适配器。
- 被驱动端测试:针对真实的外部技术编写集成测试,确保适配器符合端口契约。
4. 项目的典型包结构建议
domain/ ← 核心领域模型、领域服务、端口接口
application/ ← 应用服务、用例编排,依赖领域端口
adapters/
drivings/ ← REST控制器、CLI命令、消息监听器等
driven/ ← 数据库实现、外部API客户端、邮件发送器等
configuration/ ← 依赖注入、适配器装配
如何开始实践
步骤 1:识别核心领域
抽取与具体技术无关的业务实体、值对象和领域服务。例如在电商系统中,提取 Order、Product、PricingService 等概念。
步骤 2:定义端口接口
根据用例定义驱动端口(应用服务接口)和被驱动端口(仓储接口、通知接口等)。例如:
// 被驱动端口:订单仓储
public interface OrderRepository {
void save(Order order);
Optional<Order> findById(OrderId id);
}
// 驱动端口:下单用例接口
public interface PlaceOrderUseCase {
OrderConfirmation execute(PlaceOrderCommand command);
}
步骤 3:实现核心业务
核心领域服务只依赖端口接口。例如 PlaceOrderService 通过构造函数注入 OrderRepository 接口,通过调用仓储来持久化。
步骤 4:构建适配器
- 为
OrderRepository编写一个基于 JPA 的实现JpaOrderRepository。 - 为
PlaceOrderUseCase创建一个 REST 控制器OrderController,它调用用例并将结果返回。
步骤 5:组装应用
在启动模块中使用依赖注入将所有组件连接起来,将适配器的实例注入到核心需要的端口。
测试策略
六边形架构为测试带来了极大的便利:
- 快速领域测试:对核心业务逻辑编写单元测试,不需要任何框架或外部资源,使用模拟的端口实现即可。例如在测试
PlaceOrderService时,注入一个内存实现的OrderRepository。 - 适配器集成测试:单独测试每个适配器是否正确地实现了端口契约。例如使用测试容器测试
JpaOrderRepository,确保其真的能够保存和读取数据。 - 全栈端到端测试:按需编写少量测试覆盖关键场景,验证所有适配器连接正确。
常见误区与注意事项
- 过度设计:不要为了六边形而六边形。在简单的 CRUD 应用或原型中,可能不需要如此严格的隔离。应从业务复杂度出发。
- 端口不是必须六个:六边形的边数可以灵活,一般只有 2 - 4 个端口,驱动力来自于“隔离”的需求而非形状。
- 避免端口泄漏:不要将技术细节带入端口接口。例如不要在仓储接口中抛框架特定的异常,或使用框架独有的分页对象。
- 不要忽略应用服务层:端口的调用和编排通常需要一个应用服务层,它负责事务、权限检查、事件发布等,领域服务应保持纯净。
总结
六边形架构通过“端口与适配器”的模式,将核心业务逻辑完全隔离在应用中心,使得软件能够从容应对技术环境的变化。它强调依赖倒置和接口隔离,带来了卓越的可测试性、可维护性和技术灵活性。对于中大型业务系统或需要长期演进的项目,采用六边形架构能够显著降低技术债务,提高团队开发效率。入门时建议从一个明确的边界上下文开始,逐步体会核心与外部解耦的价值。