六边形架构Port层请求和响应用的是 DTO吗

背景和价值

在六边形架构中,Port 层(接口层)的请求和响应是否使用 DTO,取决于具体场景和设计选择,但通常建议遵循以下原则:


1. Port 接口的输入/输出是否用 DTO?

情况 1:输入端口(驱动端口)

  • 定义:外部(如用户、API)调用应用核心的接口(如 OrderService.placeOrder())。
  • 是否用 DTO
    推荐使用 DTO
    • 原因
      • 隔离外部请求与领域模型,避免污染核心逻辑。
      • 适配不同外部协议(如 REST、GraphQL)时,DTO 可灵活转换。
    • 示例
      // port 层接口(输入端口)
      public interface OrderService {
          OrderResponse placeOrder(OrderRequest request); // 使用DTO
      }
      
      // DTO 定义(通常放在 port 模块或独立模块中)
      public class OrderRequest {
          private String productId;
          private int quantity;
          // getters/setters
      }
      
      public class OrderResponse {
          private String orderId;
          private String status;
          // getters/setters
      }
      

情况 2:输出端口(被驱动端口)

  • 定义:应用核心调用外部服务(如数据库、第三方API)的接口(如 OrderRepository.save())。
  • 是否用 DTO
    ⚠️ 视情况而定
    • 如果外部数据模型与领域模型差异大
      ✅ 使用 DTO(如数据库表结构与领域对象不一致时)。
      public interface OrderRepository {
          void save(OrderDbEntity dbEntity); // 数据库专用DTO
      }
      
    • 如果外部模型与领域模型一致
      ❌ 可直接传递领域对象(保持简洁)。
      public interface OrderRepository {
          void save(Order order); // 直接使用领域对象
      }
      

2. DTO 应该放在哪个模块?

  • port 模块
    如果 DTO 是接口契约的一部分(如 API 请求/响应),建议放在 port 模块。
  • adapter 模块
    如果 DTO 是技术特定的(如 JPA 实体、Feign 客户端请求),则放在 adapter 模块。

3. 为什么推荐 Port 层使用 DTO?

  1. 解耦领域模型
    • 避免外部请求参数(如 REST API 的 JSON)直接映射到领域对象,防止非法状态污染业务逻辑。
  2. 协议独立性
    • 同一接口可适配不同协议(如 REST 和 gRPC),DTO 可分别转换。
  3. 安全控制
    • DTO 可隐藏敏感字段(如数据库主键、内部状态),仅暴露必要数据。

4. 反例:不用 DTO 的问题

// 反例:直接使用领域对象作为接口参数
public interface OrderService {
    void placeOrder(Order order); // Order 是领域对象
}
  • 问题
    • 外部调用者可能绕过业务规则,构造非法的 Order 对象。
    • API 变更会直接影响领域模型。

5. 完整示例

port 模块

// 输入端口接口 + DTO
public interface OrderService {
    OrderResponse placeOrder(OrderRequest request);
}

// 输出端口接口(直接使用领域对象)
public interface OrderRepository {
    Order findById(OrderId id);
}

adapter 模块

// REST 适配器(输入适配器)
@RestController
public class OrderController implements OrderService {
    @Override
    @PostMapping("/orders")
    public OrderResponse placeOrder(@RequestBody OrderRequest request) {
        // 将 DTO 转换为领域对象
        Order order = convertToDomain(request);
        // 调用领域逻辑...
        return convertToResponse(order);
    }
}

// 数据库适配器(输出适配器)
@Repository
public class JpaOrderRepository implements OrderRepository {
    @Override
    public Order findById(OrderId id) {
        OrderEntity jpaEntity = jpaRepo.findById(id);
        return convertToDomain(jpaEntity); // 数据库DTO转领域对象
    }
}

6. 总结

场景 是否用 DTO 推荐位置
输入端口(API 请求/响应) ✅ 必须 port 模块
输出端口(数据库访问) 视情况(模型差异大时用) adapter 模块
内部领域逻辑传递 ❌ 不用 domain 模块

通过合理使用 DTO,能严格隔离六边形架构的各层,保持核心业务与技术细节的解耦。

参考资料

posted @ 2025-04-23 22:59  向着朝阳  阅读(48)  评论(0)    收藏  举报