本文目录导读:

端口适配器模式(Port-Adapter Pattern),也常被称为六边形架构或洋葱架构,是领域驱动设计(DDD)中非常经典的一种架构模式,它的核心思想是将业务逻辑(内部)与外部技术细节(数据库、消息队列、API等)解耦。
下面通过一个电商订单系统的案例,逐步展示端口适配器模式的落地。
案例背景:电商订单系统
假设我们要开发一个订单服务,需要实现以下功能:
- 创建订单(业务逻辑)
- 保存订单(需要数据库)
- 发送订单确认通知(需要邮件/短信服务)
- 处理支付回调(外部系统调用)
传统做法:业务逻辑直接调用数据库的 OrderRepository 实现类或直接调用 sendEmail() 方法,这种方式会导致业务逻辑与具体技术(MySQL、阿里云短信)深度耦合,替换成本高。
端口适配器做法:我们在业务层之间定义接口(端口),让外部技术去实现接口(适配器)。
核心角色定义
在编码前,我们先明确四个核心角色:
| 角色 | 说明 | 在本案例中的体现 |
|---|---|---|
| 核心领域(Core) | 纯业务逻辑,不依赖任何框架或技术 | OrderService |
| 端口(Port) | 内部的接口(抽象),定义“需要做什么” | OrderRepository(持久化端口)、NotificationPort(通知端口) |
| 适配器(Adapter) | 外部的具体实现,实现了端口 | MySqlOrderAdapter、SmsNotificationAdapter |
| 外部系统 | 适配器对接的实际技术 | MySQL数据库、阿里云短信网关 |
代码实现(Java 伪代码)
核心领域层(内部,不依赖外部框架)
// 领域模型(实体)
public class Order {
private Long id;
private String productName;
private OrderStatus status;
// ... getters & setters
}
// ---------- 定义端口(接口) ----------
// 持久化端口:定义“怎么存”,但不关心存到MySQL还是Redis
public interface OrderRepositoryPort {
void save(Order order);
Order findById(Long id);
}
// 通知端口:定义“怎么发通知”,但不关心用短信还是邮件
public interface NotificationPort {
void sendOrderConfirmation(String phoneNumber, String content);
}
// ---------- 核心业务服务(纯Java) ----------
public class OrderService {
// 只依赖接口(端口),不依赖具体实现(适配器)
private final OrderRepositoryPort orderRepository;
private final NotificationPort notificationPort;
// 通过构造函数注入(依赖倒置)
public OrderService(OrderRepositoryPort orderRepository, NotificationPort notificationPort) {
this.orderRepository = orderRepository;
this.notificationPort = notificationPort;
}
// 核心业务逻辑:创建订单
public Order createOrder(String productName, long userId, String userPhone) {
// 1. 业务校验(不涉及技术)
if (productName == null || productName.isEmpty()) {
throw new IllegalArgumentException("商品名称不能为空");
}
// 2. 构建订单实体
Order order = new Order();
order.setProductName(productName);
order.setStatus(OrderStatus.PENDING);
// 3. 调用端口:保存(具体怎么存,由外部适配器决定)
orderRepository.save(order);
// 4. 调用端口:发送通知(具体怎么发,由外部适配器决定)
notificationPort.sendOrderConfirmation(userPhone, "您的订单已创建:" + productName);
return order;
}
}
外部适配器层(外层,依赖具体技术)
这一层就是“适配器”,负责将技术细节适配到业务端口。
// ---------- 适配器 1:MySQL持久化实现 ----------
// 假设使用了 Spring Data JPA 或 MyBatis
public class MySqlOrderRepositoryAdapter implements OrderRepositoryPort {
private final JdbcTemplate jdbcTemplate; // 假设使用JDBC
@Override
public void save(Order order) {
// 具体的SQL语句,这里不需要业务关心
jdbcTemplate.update("INSERT INTO orders (product_name, status) VALUES (?, ?)",
order.getProductName(), order.getStatus());
}
@Override
public Order findById(Long id) {
// 查询并组装对象
return jdbcTemplate.queryForObject(...);
}
}
// ---------- 适配器 2:短信通知实现 ----------
public class SmsNotificationAdapter implements NotificationPort {
@Override
public void sendOrderConfirmation(String phoneNumber, String content) {
// 调用阿里云短信 SDK
AliyunSmsClient.send(phoneNumber, content);
}
}
// ---------- 如果需要换成邮件,只需新增一个适配器,不需要改业务代码 ----------
public class EmailNotificationAdapter implements NotificationPort {
@Override
public void sendOrderConfirmation(String email, String content) {
// 调用 JavaMail API 发送邮件
}
}
装配(组合根)
在系统启动时(如Spring的 @Configuration 或手动 new),将适配器注入端口。
// 使用依赖注入容器(如Spring)进行装配
@Configuration
public class AppConfig {
@Bean
public OrderService orderService(OrderRepositoryPort orderRepository, NotificationPort notification) {
return new OrderService(orderRepository, notification);
}
@Bean
public OrderRepositoryPort orderRepository(DataSource dataSource) {
return new MySqlOrderRepositoryAdapter(dataSource);
}
// 如果将来想用邮件,只需要改这里返回的Bean,OrderService完全不用动
@Bean
public NotificationPort notificationPort() {
return new SmsNotificationAdapter();
}
}
核心优势(结合案例解析)
业务逻辑的“绝对隔离”
OrderService 中没有任何 import java.sql.* 或 import com.alibaba.sms.* 的代码,它只知道“先存一下,再通知一声”,具体怎么存、怎么发,外包给端口。
替换技术成本极低
- 场景A:数据库从 MySQL 换成 PostgreSQL。
- 只需新增一个
PostgresOrderRepositoryAdapter实现端口,修改@Bean注入即可,业务代码一行不改。
- 只需新增一个
- 场景B:通知从短信换成邮件。
- 新增
EmailNotificationAdapter,修改配置即可。
- 新增
便于单元测试
在测试 OrderService 时,我们可以直接注入假的适配器(Mock),不需要启动数据库和短信服务:
public class OrderServiceTest {
@Test
public void testCreateOrder() {
// 使用 Mockito 模拟端口
OrderRepositoryPort mockRepo = mock(OrderRepositoryPort.class);
NotificationPort mockNotif = mock(NotificationPort.class);
OrderService service = new OrderService(mockRepo, mockNotif);
// 调用业务,验证只调用了两个端口
service.createOrder("iPhone", 1L, "13800138000");
verify(mockRepo).save(any(Order.class));
verify(mockNotif).sendOrderConfirmation(eq("13800138000"), anyString());
}
}
延伸:与“六边形架构”的映射
如果你听过“六边形架构”,这个案例完全可以映射过去:
[外部:短信服务] -- 适配器 --> (通知端口 <---+ +---> 适配器 --> [MySQL数据库])
| |
[外部:HTTP请求] -- 控制器适配器 --> (命令端口 ------ [核心业务] ------ 查询端口) -- 适配器 --> [缓存服务]
| |
[外部:消息队列] -- 适配器 --> (事件端口 <-------+ +---> 适配器 --> [外部API])
- 左侧(请求进来):HTTP控制器、消息队列监听器都属于“驱动适配器”,它们将外部请求转换为内部命令。
- 右侧(依赖出去):MySQL、短信、Redis都属于“被驱动适配器”,它们实现内部定义的端口。
- 中间是纯Java业务,像一个“心脏”,不感知外部世界的存在。
端口适配器案例的关键在于:
- “端口”是内部的接口,是业务的诉求(我要存数据,我要发短信)。
- “适配器”是外部的插件,是技术的实现(用MySQL存,用阿里云发)。
- 依赖关系永远指向内部(外层适配器依赖内层端口,内层业务不依赖外层技术)。
实际开发中,如果你发现“换数据库要改业务代码”“短信服务商变了要改业务代码”,那就是缺少端口适配器设计,这个模式非常适合业务逻辑复杂、长期演进、需要保持核心代码纯净的中大型系统。