问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

DDD - 彻底搞懂 DO 、DTO 、 Entity

创作时间:
作者:
@小白创作中心

DDD - 彻底搞懂 DO 、DTO 、 Entity

引用
CSDN
1.
https://blog.csdn.net/yangshangwei/article/details/145816198

在领域驱动设计(DDD)中,DO(Data Object)、DTO(Data Transfer Object)和Entity(领域实体)是三种不同职责的对象,它们的协作关系直接决定了系统的清晰性和可维护性。本文将深入解析这三种对象的角色定位、职责差异以及它们在系统架构中的协作关系,并指出常见的设计误区,提供最佳实践建议。

引言

在领域驱动设计(DDD)中,DO(Data Object)、DTO(Data Transfer Object)Entity(领域实体)是三种不同职责的对象,它们的协作关系直接决定了系统的清晰性和可维护性。

1. 角色定义与职责对比

对象类型
定位
职责
生命周期
典型特征
Entity
领域模型核心
封装业务逻辑,维护领域规则,保证聚合根的一致性
贯穿领域层
- 具有唯一标识(ID)
- 包含业务方法
DO
数据持久化对象
与数据库表结构直接映射,仅关注数据存储格式
仅在数据访问层(DAO)
- 纯数据结构
- 无业务逻辑
DTO
跨层数据传输载体
在应用层与外部系统(如前端、其他服务)之间传输数据
请求/响应过程
- 扁平化结构
- 无行为

2. 协作关系与转换流程

典型分层架构中的交互

  
┌───────────────┐       ┌───────────────┐       ┌───────────────┐
│   应用层       │       │   领域层       │       │   基础设施层     │
│ (Application) │       │  (Domain)     │       │ (Infrastructure)│
├───────────────┤       ├───────────────┤       ├───────────────┤
│ 使用 DTO      │◀─────▶│ 操作 Entity    │◀─────▶│ 操作 DO        │
│ 接收/返回数据  │       │ 执行业务逻辑    │       │ 读写数据库       │
└───────────────┘       └───────────────┘       └───────────────┘
  

关键转换场景

  1. DO → Entity(数据持久化 → 领域模型)
  • 场景:从数据库加载数据到内存领域对象
  • 实现
  
public class OrderRepositoryImpl implements OrderRepository {
    @Override
    public Order findById(OrderId id) {
        OrderDO orderDO = jdbcTemplate.query(...); // 查询DO
        return OrderConverter.toEntity(orderDO);    // 转换为Entity
    }
}
  
  1. Entity → DTO(领域模型 → 外部接口)
  • 场景:向客户端返回订单详情
  • 实现
  
@RestController
public class OrderController {
    @GetMapping("/orders/{id}")
    public OrderDTO getOrder(@PathVariable String id) {
        Order order = orderService.getOrder(id);    // 获取Entity
        return OrderAssembler.toDTO(order);         // 转换为DTO
    }
}
  
  1. DTO → Entity(外部请求 → 领域模型)
  • 场景:处理用户下单请求
  • 实现
  
@PostMapping("/orders")
public void createOrder(@RequestBody OrderCreateDTO dto) {
    Order order = OrderFactory.fromDTO(dto);       // DTO转Entity
    orderService.createOrder(order);               // 调用领域服务
}
  

3. 关键差异与设计原则

(1) 业务逻辑归属

  • Entity:必须包含领域行为
  
public class Order {
    private OrderStatus status;
    
    // 领域方法:取消订单
    public void cancel() {
        if (status == OrderStatus.SHIPPED) {
            throw new DomainException("已发货订单不可取消");
        }
        this.status = OrderStatus.CANCELLED;
    }
}
  
  • DO/DTO:禁止包含业务逻辑
  
// 反模式:DTO中出现业务判断
public class OrderDTO {
    public boolean isCancellable() { // ❌ 错误!逻辑应属于Entity
        return status.equals("CREATED");
    }
}
  

(2) 标识与生命周期

  • Entity:通过唯一标识(ID)区分不同实例
  
public class OrderId {
    private String value; // 领域唯一标识
}
  
  • DO:主键与数据库表一致(可能与领域ID不同)
  
CREATE TABLE orders (
    db_id BIGINT PRIMARY KEY,  -- 数据库自增ID
    order_id VARCHAR(32)       -- 领域ID(唯一业务标识)
);
  
  • DTO:通常无标识,仅传输瞬时数据

(3) 数据完整性

  • Entity:强一致性,保证聚合根完整性
  
public class Order {
    private List<OrderItem> items;
    
    public void addItem(Product product, int quantity) {
        items.add(new OrderItem(product.getId(), quantity));
        this.total = calculateTotal(); // 自动维护总金额
    }
}
  
  • DTO:可接受部分数据(如分页查询结果)
  
public class OrderSummaryDTO {
    private String orderId;
    private BigDecimal totalAmount; // 不包含明细项
}
  

4. 常见误区与解决方案

误区1:将DO直接暴露为Entity

  • 反模式
  
// ❌ DO与Entity混用
@Entity
@Table(name = "orders")
public class OrderDO {
    @Id
    private Long dbId;       // 数据库主键
    private String status;   // 数据库字段名直接暴露
    // 无业务方法
}
  
  • 修复方案
  
// ✅ 明确分离
public class Order {                // 领域Entity
    private OrderId id;            // 领域标识
    private OrderStatus status;     // 领域枚举
    public void cancel() { ... }    // 领域行为
}
public class OrderDO {              // 持久化对象
    private Long dbId;
    private String orderId;
    private String status;
}
  

误区2:DTO包含领域逻辑

  • 反模式
  
public class OrderDTO {
    private List<OrderItemDTO> items;
    
    // ❌ 在DTO中计算金额
    public BigDecimal getTotal() {
        return items.stream()
            .map(item -> item.getPrice().multiply(item.getQuantity()))
            .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
}
  
  • 修复方案
  
// ✅ 金额计算由Entity完成
public class Order {
    private Money total;
    
    public void calculateTotal() {
        this.total = items.stream()
            .map(OrderItem::subtotal)
            .reduce(Money.ZERO, Money::add);
    }
}
  

5. 最佳实践总结

  1. 明确分层边界
  • 基础设施层:只处理DO(
    OrderDAO
    操作
    OrderDO

  • 领域层:只处理Entity(
    OrderRepository
    返回
    Order

  • 应用层:协调DTO与Entity转换(
    OrderService
    使用
    OrderDTO
    与外部交互)

  1. 使用转换层隔离模型
  • 工厂模式
    OrderFactory.fromDTO(dto)

  • 映射工具:MapStruct、ModelMapper

  
@Mapper
public interface OrderConverter {
    Order toEntity(OrderDO orderDO);
    OrderDO toDO(Order order);
}
  
  1. 领域ID与数据库ID分离
  
public class Order {
    private OrderId id; // 领域标识(如订单号"ORD-2023-1001")
}
public class OrderDO {
    private Long dbId;  // 数据库自增ID
    private String orderId; // 对应领域ID
}
  
  1. 通过防腐层隔离外部模型
  
// 适配外部服务的DTO
public class PaymentGatewayAdapter {
    public ExternalPaymentRequest toExternalRequest(Order order) {
        // 转换领域对象为第三方要求的DTO格式
    }
}
  

总结

在DDD中,Entity是业务逻辑的载体,DO是技术实现的细节,DTO是跨边界通信的媒介。它们的核心区别在于:

  • Entity关心"业务是什么"(What)

  • DO关心"数据怎么存"(How)

  • DTO关心"数据怎么传"(How)

保持三者界限清晰,才能构建出高内聚、低耦合的领域模型。正如Eric Evans在《领域驱动设计》中所说: “领域模型不是数据模型,它的作用是描述系统中存在的各种概念及其关系,而不是定义如何存储这些概念。”

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号