DDD架构为何拆分Entity层?从MVC到领域模型的演进之道

传统的的Spring MVC三层架构在应对复杂业务时显得捉襟见肘,DDD结构通过分层与对象职责分离,为系统架构注入新的活力。

引言:当MVC架构遇上复杂业务

在传统的Spring MVC或三层架构中,我们通常能看到这样的分层结构:

java 复制代码
├── controller
│   └── OrderController.java
├── service
│   └── OrderService.java
├── dao
│   └── OrderDao.java
└── entity
    └── Order.java

这里的Order实体类通常扮演多重角色:

  1. 数据库映射​ - 通过JPA或MyBatis注解与表结构绑定

  2. 业务逻辑载体​ - 包含部分业务方法

  3. 数据传输对象​ - 在Controller、Service间传递

这种设计在项目初期看似简洁高效,但随着业务复杂度的增加,问题逐渐暴露:

  • 业务与数据强耦合:数据库表结构调整会直接冲击业务逻辑

  • 职责混乱:一个实体类承载过多职责,违反单一职责原则

  • 模型表达力不足:简单的属性字段难以表达复杂的业务概念

  • 难以测试:业务逻辑与持久化框架深度绑定

一、DDD的分层架构革命

DDD(领域驱动设计)通过清晰的分层架构,将业务复杂性内聚到领域模型中,同时将技术复杂性隔离到基础设施中。

1.1 传统DDD四层架构

java 复制代码
├── interfaces(用户界面层)
│   └── OrderController.java
├── application(应用层)
│   └── OrderAppService.java
├── domain(领域层)
│   ├── model
│   │   ├── aggregate
│   │   │   └── Order.java(聚合根)
│   │   └── valueobject
│   │       └── Money.java
│   └── service
│       └── OrderDomainService.java
└── infrastructure(基础设施层)
    └── persistence
        ├── OrderEntity.java(持久化实体)
        └── OrderRepositoryImpl.java

1.2 核心拆分:领域对象与数据对象分离

DDD最核心的变革之一,就是将原来"万能"的Entity拆分:

|------------------|------------------|------------------------|----------------|
| 传统MVC Entity | DDD拆解结果 | 所在分层 | 核心职责 |
| 数据库映射 + 业务方法 | 聚合根 | domain | 封装业务逻辑,维护边界一致性 |
| 数据库映射 + 业务方法 | 基础设施层Entity | infrastructure | 纯粹的数据持久化 |
| 数据传输 | DTO | application/interfaces | 跨层数据传输 |
| 数据传输 | VO | interfaces | 前端展示专用 |
| 业务概念表达 | Value Object | domain | 描述不可变业务概念 |

二、深入理解DDD中的各类对象

2.1 聚合根(Aggregate Root) - 业务逻辑的守护者

聚合根是DDD的核心概念,它不仅是业务对象的容器,更是业务规则的执行者。

java 复制代码
/**
 * @Author: 洛洛起不来
 * 订单聚合根示例
 * 位置:domain/model/aggregate/
 */
public class Order implements AggregateRoot {
    private OrderId id;
    private CustomerId customerId;
    private OrderStatus status;
    private Money totalAmount;
    private List<OrderItem> items; // 聚合内实体
    
    // 核心业务方法
    public void placeOrder() {
        validateOrder();
        this.status = OrderStatus.PLACED;
        this.addDomainEvent(new OrderPlacedEvent(this.id));
    }
    
    public void cancelOrder(String reason) {
        if (!this.canBeCanceled()) {
            throw new OrderCannotCancelException("订单已发货,无法取消");
        }
        this.status = OrderStatus.CANCELLED;
        this.addDomainEvent(new OrderCancelledEvent(this.id, reason));
    }
    
    // 业务规则校验
    private void validateOrder() {
        if (items.isEmpty()) {
            throw new EmptyOrderException("订单不能为空");
        }
        if (totalAmount.isNegative()) {
            throw new InvalidAmountException("订单金额不能为负");
        }
    }
}

聚合根的特点

  • 是整个聚合的唯一入口,外部只能通过聚合根操作聚合内对象

  • 封装业务逻辑,维护聚合内数据的一致性

  • 领域模型的核心,与技术实现无关

  • 可以发布领域事件,实现领域间的解耦通信

2.2 基础设施层Entity - 纯粹的数据容器

java 复制代码
/**
 * @Author: 洛洛起不来
 * 订单持久化实体
 * 位置:infrastructure/persistence/
 */
@Entity
@Table(name = "t_order")
public class OrderEntity {
    @Id
    private Long id;
    
    @Column(name = "customer_id")
    private Long customerId;
    
    @Column(name = "total_amount")
    private BigDecimal totalAmount;
    
    @Column(name = "status")
    private Integer status;
    
    @Column(name = "created_at")
    private LocalDateTime createdAt;
    
    // 只有getter/setter,没有业务逻辑
    // 纯粹的贫血模型
}

基础设施层Entity的特点

  • 只包含数据和映射关系,没有任何业务逻辑

  • 与具体的ORM框架(JPA、MyBatis)强绑定

  • 负责在数据库和领域模型之间进行数据转换

  • 一个聚合根可能对应多个基础设施Entity

2.3 值对象(Value Object) - 业务概念的精准表达

java 复制代码
/**
 * 货币值对象
 * 通过属性值定义,而非标识
 */
@Value
public class Money implements ValueObject {
    private final BigDecimal amount;
    private final String currency;
    
    public Money add(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new CurrencyMismatchException("货币类型不匹配");
        }
        return new Money(this.amount.add(other.amount), this.currency);
    }
    
    public Money multiply(BigDecimal multiplier) {
        return new Money(this.amount.multiply(multiplier), this.currency);
    }
    
    public boolean isGreaterThan(Money other) {
        return this.amount.compareTo(other.amount) > 0;
    }
    
    // 值对象的相等性比较基于属性值
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Money money = (Money) o;
        return amount.compareTo(money.amount) == 0 && 
               currency.equals(money.currency);
    }
}

值对象的特点

  • 通过属性值定义,而不是标识(ID)

  • 不可变(immutable),创建后状态不可修改

  • 描述无状态的业务概念

  • 可包含领域行为(如Money的加减运算)

2.4 DTO与VO - 数据传输与展示的桥梁

java 复制代码
/**
 * DTO:应用层数据传输
 * 位置:application/dto/
 */
@Data
public class CreateOrderDTO {
    private Long customerId;
    private List<OrderItemDTO> items;
    private String deliveryAddress;
    private String paymentMethod;
}

/**
 * VO:前端展示专用
 * 位置:interfaces/vo/
 */
@Data
public class OrderDetailVO {
    private String orderId;
    private String orderNo;
    private String customerName;
    private String statusText;
    private String totalAmount;
    private String createTime;
    private List<ProductInfoVO> products; // 聚合了其他领域的数据
    private String deliveryAddress;
    private String paymentInfo;
}

DTO与VO的区别

|----------|----------------------------|-------------|
| 特性 | DTO | VO |
| 使用场景 | 跨层数据传输,如Service到Controller | 前端展示专用 |
| 数据来源 | 通常对应单个领域模型 | 可能聚合多个领域的数据 |
| 格式处理 | 保持原数据格式 | 可能进行格式化、裁剪 |
| 职责 | 解耦领域模型和接口 | 适配前端展示需求 |

三、对象间的协作流程

以一个电商"下单"业务为例,展示各对象如何协同工作:

java 复制代码
/**
 * 完整的下单流程示例
 */
@Service
public class OrderAppService {
    
    @Autowired
    private OrderRepository orderRepository;
    
    @Autowired
    private ProductService productService;
    
    @Transactional
    public OrderResultVO placeOrder(CreateOrderDTO dto) {
        // 参数校验
        validateCreateOrderDTO(dto);
        
        // 创建聚合根
        Order order = OrderFactory.createOrder(dto);
        
        // 调用领域服务检查库存
        productService.checkInventory(order.getProductItems());
        
        // 执行业务逻辑
        order.placeOrder();
        
        // 保存聚合根
        orderRepository.save(order);
        
        // 发布应用事件(可选)
        applicationEventPublisher.publishEvent(
            new OrderCreatedEvent(order.getId())
        );
        
        // 返回前端VO
        return OrderAssembler.toVO(order);
    }
}

具体的数据流转:

四、为什么需要这么多对象类型?

4.1 单一职责原则的极致体现

DDD将原来"一肩挑"的Entity拆分为多个对象,每个对象都有明确的职责:

  • 聚合根:负责业务规则

  • 值对象:负责业务概念表达

  • 基础设施Entity:负责数据持久化

  • DTO:负责跨层数据传输

  • VO:负责前端展示适配

4.2 解耦带来的架构优势

业务与数据持久化解耦

java 复制代码
// 传统方式:业务逻辑与JPA注解混杂
@Entity
public class Order {
    @Id
    private Long id;
    
    @Column(name = "order_no")
    private String orderNo;
    
    // 业务逻辑中掺杂了数据库字段映射
    public boolean isOverdue() {
        return status == 1 && createdTime.plusDays(7).isBefore(LocalDateTime.now());
    }
}

// DDD方式:业务逻辑独立
public class Order implements AggregateRoot {
    private OrderStatus status;
    private LocalDateTime createdTime;
    
    // 纯粹的领域逻辑
    public boolean isOverdue() {
        return status == OrderStatus.UNPAID && 
               createdTime.plusDays(7).isBefore(LocalDateTime.now());
    }
}

领域模型和外部接口解耦

java 复制代码
// 领域模型稳定不变
public class User {
    private UserId id;
    private String username;
    private Email email;
    private PhoneNumber phone;
}

// 接口模型可独立演化
public class UserDTO {
    private String id;
    private String name;
    private String email;
    private String phone;
    
    // 适配不同接口需求
    public static UserDTO from(User user) {
        UserDTO dto = new UserDTO();
        dto.setId(user.getId().getValue());
        dto.setName(user.getUsername());
        dto.setEmail(user.getEmail().getValue());
        dto.setPhone(user.getPhone().getFormattedNumber());
        return dto;
    }
}

4.3 应对变化的架构弹性

场景1:数据库从MySQL迁移到MongoDB

java 复制代码
// 传统架构:需要修改所有Entity类
@Entity  // 需要删除JPA注解
@Document(collection = "orders")  // 添加MongoDB注解
public class Order {
    // 需要修改所有字段映射
}

// DDD架构:只需修改基础设施层
// 领域层不变
public class Order implements AggregateRoot {
    // 业务逻辑保持不变
}

// 基础设施层适配
@Document(collection = "orders")
public class OrderDocument {
    // MongoDB特定的映射
    @Id
    private String mongoId;
    private String orderId;
    // 其他字段...
}

场景2:前端需求变化,需要新字段

java 复制代码
// 传统架构:可能需要修改数据库和Entity
@Entity
public class Order {
    // 需要添加新字段
    private String newFieldForFrontend;
}

// DDD架构:只需修改VO
public class OrderDetailVO {
    // 添加前端需要的新字段
    private String newDisplayField;
    
    public static OrderDetailVO from(Order order) {
        OrderDetailVO vo = new OrderDetailVO();
        vo.setNewDisplayField(order.calculateNewField());
        return vo;
    }
}

五、总结

架构的本质是在各种约束下做出权衡。DDD通过增加短期复杂度,换取长期的可维护性和业务适应能力。当你的系统需要应对频繁变化的复杂业务时,这种"复杂"的拆分反而成为了对抗真正混乱的最佳武器。

进一步学习资源

  • 《领域驱动设计:软件核心复杂性应对之道》- Eric Evans

  • 《实现领域驱动设计》- Vaughn Vernon

  • 示例项目:https://github.com/ddd-by-examples

作者观点:DDD不是最完美的开发方式,但它提供了一套系统的思考框架,帮助我们在业务复杂性与技术实现之间找到平衡点。记住,好的架构不是一开始就设计出来的,而是在不断演进中生长出来的。

相关推荐
前端不太难4 小时前
AI 原生架构:鸿蒙App的下一代形态
人工智能·架构·harmonyos
Fzuim4 小时前
从 LLM 接口到 Agent 接口:AI 融合系统的架构演进与未来趋势分析报告
人工智能·ai·重构·架构·agent·runtime
sayang_shao12 小时前
ARM架构运行模式学习笔记
arm开发·学习·架构
一叶飘零_sweeeet12 小时前
服务注册发现深度拆解:Nacos vs Eureka 核心原理、架构选型与生产落地
微服务·云原生·eureka·nacos·架构·注册中心
Tadas-Gao13 小时前
Mem0分层记忆系统:大语言模型长期记忆的架构革命与实现范式
人工智能·语言模型·自然语言处理·架构·大模型·llm·transformer
lpfasd12313 小时前
QCLAW 浏览器联通指南:原理、架构与配置详解
ai·架构·程序员创富
源远流长jerry14 小时前
在 Ubuntu 22.04 上配置 Soft-RoCE 并运行 RDMA 测试程序
linux·服务器·网络·tcp/ip·ubuntu·架构·ip
宇擎智脑科技14 小时前
A2A Python SDK 源码架构解读:一个请求是如何被处理的
人工智能·python·架构·a2a
uzong15 小时前
Harness Engineering 是什么?一场新的 AI 范式已经开始
人工智能·后端·架构