别再死记硬背DDD概念了!用Java代码手把手带你理解聚合、实体与值对象

发布时间:2026/6/30 14:49:19
别再死记硬背DDD概念了!用Java代码手把手带你理解聚合、实体与值对象 用Java代码实战解析DDD核心概念聚合、实体与值对象很多开发者初次接触领域驱动设计DDD时都会被其中抽象的概念所困扰。聚合根、实体、值对象这些术语听起来高大上但在实际编码中却不知如何落地。本文将彻底改变你学习DDD的方式——不再死记硬背理论而是通过一个完整的Java订单系统示例让你在IDE中亲手编写代码直观感受这些概念的实际应用。1. 从贫血模型到充血模型理解DDD的本质在传统开发中我们常会写出所谓的贫血模型——对象仅仅是一堆属性的集合业务逻辑都分散在Service层中。这种模式虽然简单但随着业务复杂度的提升会导致代码难以维护和理解。而DDD提倡的充血模型则是将数据和操作数据的行为封装在一起。让我们看一个典型的贫血模型例子// 贫血模型示例 - 不推荐 public class Order { private Long id; private String customer; private ListOrderItem items; private boolean paid; // 只有getter和setter没有业务行为 } public class OrderService { public void addItem(Order order, OrderItem item) { if (!order.isPaid()) { order.getItems().add(item); } else { throw new IllegalStateException(订单已支付不能添加商品); } } }而在DDD的充血模型中同样的业务逻辑会被封装在领域对象内部// 充血模型示例 - 推荐 public class Order { private Long id; private String customer; private ListOrderItem items; private boolean paid; public void addItem(OrderItem item) { if (!this.paid) { this.items.add(item); } else { throw new IllegalStateException(订单已支付不能添加商品); } } }关键区别贫血模型中业务逻辑在Service中对象只是数据容器充血模型中业务逻辑在对象内部对象是活的提示在DDD实践中大约80%的业务逻辑应该放在领域模型中只有协调、事务管理等跨聚合的逻辑才放在Service中2. 构建订单系统实战聚合、实体与值对象现在让我们构建一个完整的订单系统来演示DDD核心概念。我们将创建以下核心类Order- 聚合根OrderItem- 实体Address- 值对象OrderStatus- 值对象枚举2.1 定义值对象Address和OrderStatus值对象是DDD中的重要概念它们没有唯一标识通过属性值来区分并且通常是不可变的。// 值对象示例 - Address public final class Address { private final String province; private final String city; private final String detail; public Address(String province, String city, String detail) { this.province province; this.city city; this.detail detail; } // 值对象应该实现equals和hashCode Override public boolean equals(Object o) { if (this o) return true; if (o null || getClass() ! o.getClass()) return false; Address address (Address) o; return Objects.equals(province, address.province) Objects.equals(city, address.city) Objects.equals(detail, address.detail); } Override public int hashCode() { return Objects.hash(province, city, detail); } // 只有getter没有setter确保不可变性 public String getProvince() { return province; } public String getCity() { return city; } public String getDetail() { return detail; } } // 值对象示例 - OrderStatus枚举 public enum OrderStatus { CREATED(已创建), PAID(已支付), SHIPPED(已发货), COMPLETED(已完成), CANCELLED(已取消); private final String description; OrderStatus(String description) { this.description description; } public String getDescription() { return description; } }值对象的特点没有唯一标识通过属性值区分通常不可变immutable应该正确实现equals()和hashCode()可以安全共享2.2 定义实体OrderItem实体与值对象的关键区别在于实体有唯一标识并且实体的生命周期中这个标识保持不变。// 实体示例 - OrderItem public class OrderItem { private Long id; // 唯一标识 private String productId; private String productName; private BigDecimal price; private int quantity; public OrderItem(Long id, String productId, String productName, BigDecimal price, int quantity) { this.id id; this.productId productId; this.productName productName; this.price price; this.quantity quantity; } // 实体的equals和hashCode基于标识符 Override public boolean equals(Object o) { if (this o) return true; if (o null || getClass() ! o.getClass()) return false; OrderItem orderItem (OrderItem) o; return Objects.equals(id, orderItem.id); } Override public int hashCode() { return Objects.hash(id); } // 业务方法 public BigDecimal getSubtotal() { return price.multiply(BigDecimal.valueOf(quantity)); } // getter方法 public Long getId() { return id; } public String getProductId() { return productId; } // 其他getter... }实体的特点有唯一标识ID通过ID而不是属性值来区分可以有可变的状态通常包含业务逻辑2.3 定义聚合根Order聚合是DDD中最复杂的概念之一。一个聚合是一组相关对象的集合它们作为一个整体被处理。每个聚合都有一个聚合根外部只能通过聚合根来访问聚合内部的对象。// 聚合根示例 - Order public class Order { private Long id; private String customerId; private Address shippingAddress; // 值对象 private OrderStatus status; // 值对象 private ListOrderItem items new ArrayList(); // 实体集合 private LocalDateTime createdAt; public Order(Long id, String customerId, Address shippingAddress) { this.id id; this.customerId customerId; this.shippingAddress shippingAddress; this.status OrderStatus.CREATED; this.createdAt LocalDateTime.now(); } // 聚合根的业务方法 public void addItem(OrderItem item) { if (status ! OrderStatus.CREATED) { throw new IllegalStateException(只能在订单创建状态添加商品); } items.add(item); } public void removeItem(Long itemId) { if (status ! OrderStatus.CREATED) { throw new IllegalStateException(只能在订单创建状态移除商品); } items.removeIf(item - item.getId().equals(itemId)); } public void pay() { if (items.isEmpty()) { throw new IllegalStateException(订单没有商品不能支付); } this.status OrderStatus.PAID; } public void cancel() { if (status OrderStatus.COMPLETED) { throw new IllegalStateException(已完成订单不能取消); } this.status OrderStatus.CANCELLED; } public BigDecimal getTotalAmount() { return items.stream() .map(OrderItem::getSubtotal) .reduce(BigDecimal.ZERO, BigDecimal::add); } // getter方法 public Long getId() { return id; } // 其他getter... }聚合的设计要点一致性边界聚合内的所有对象必须保持一致状态通过根访问外部只能通过聚合根Order来修改聚合内部事务边界通常一个聚合对应一个事务小聚合原则聚合应该尽可能小只包含真正需要强一致性的对象3. 仓储模式持久化聚合根在DDD中我们使用仓储Repository模式来持久化和检索聚合根。仓储抽象了数据访问细节让领域层专注于业务逻辑。// 仓储接口 public interface OrderRepository { Order findById(Long id); ListOrder findByCustomerId(String customerId); void save(Order order); void delete(Order order); } // 使用JPA实现的仓储 Repository public class JpaOrderRepository implements OrderRepository { PersistenceContext private EntityManager entityManager; Override public Order findById(Long id) { return entityManager.find(Order.class, id); } Override public ListOrder findByCustomerId(String customerId) { return entityManager.createQuery( SELECT o FROM Order o WHERE o.customerId :customerId, Order.class) .setParameter(customerId, customerId) .getResultList(); } Override Transactional public void save(Order order) { if (order.getId() null) { entityManager.persist(order); } else { entityManager.merge(order); } } Override Transactional public void delete(Order order) { entityManager.remove(entityManager.contains(order) ? order : entityManager.merge(order)); } }仓储的最佳实践一个聚合根对应一个仓储仓储只保存和检索完整的聚合查询方法返回聚合根而不是聚合内部的对象对于复杂查询可以考虑使用CQRS模式分离读写模型4. 领域服务处理跨聚合的业务逻辑当业务逻辑涉及多个聚合时我们可以使用领域服务来封装这些逻辑。领域服务应该是无状态的。// 领域服务示例 - 订单支付服务 public class OrderPaymentService { private final OrderRepository orderRepository; private final PaymentGateway paymentGateway; public OrderPaymentService(OrderRepository orderRepository, PaymentGateway paymentGateway) { this.orderRepository orderRepository; this.paymentGateway paymentGateway; } Transactional public void processPayment(Long orderId, PaymentMethod method) { Order order orderRepository.findById(orderId) .orElseThrow(() - new OrderNotFoundException(orderId)); if (order.getStatus() ! OrderStatus.CREATED) { throw new IllegalStateException(订单当前状态不能支付); } BigDecimal amount order.getTotalAmount(); PaymentResult result paymentGateway.charge(amount, method); if (result.isSuccess()) { order.pay(); orderRepository.save(order); } else { throw new PaymentFailedException(result.getErrorMessage()); } } }领域服务的使用场景业务逻辑涉及多个聚合需要与外部系统交互如支付网关不适合放在单个聚合中的复杂业务规则5. 实战中的常见问题与解决方案5.1 如何设计聚合的边界确定聚合边界是DDD中最具挑战性的部分之一。以下是一些指导原则业务一致性需要保持强一致性的对象应该放在同一个聚合中性能考虑聚合不宜过大否则会影响性能事务边界一个聚合通常对应一个事务修改频率经常一起修改的对象应该放在同一个聚合中常见聚合划分示例业务场景推荐聚合设计理由订单系统Order(聚合根)OrderItemOrderItem没有独立存在的意义博客系统Post(聚合根)CommentComment可以独立存在但通常通过Post访问用户权限User(聚合根)和Role(聚合根)分开用户和角色是独立变化的5.2 如何处理聚合间的引用在DDD中聚合之间不应该直接持有对象引用而应该通过ID来引用。这样可以保持聚合的独立性避免加载不必要的对象明确聚合的边界// 不推荐直接对象引用 public class Order { private Customer customer; // 直接引用另一个聚合 } // 推荐通过ID引用 public class Order { private String customerId; // 只保存ID }5.3 值对象与实体的转换有时候业务需求的变化可能导致原来的值对象需要变成实体或者反过来。例如一开始Address可能设计为值对象后来业务需要跟踪每个地址的修改历史这时就需要将其改为实体。转换策略值对象→实体添加唯一标识修改equals/hashCode实现实体→值对象移除唯一标识改为基于属性的equals/hashCode// 值对象变为实体 public class Address { private Long id; // 新增唯一标识 private String province; private String city; // equals和hashCode现在基于id Override public boolean equals(Object o) { if (this o) return true; if (o null || getClass() ! o.getClass()) return false; Address address (Address) o; return Objects.equals(id, address.id); } Override public int hashCode() { return Objects.hash(id); } }5.4 领域事件的应用领域事件是DDD中另一个重要模式用于解耦聚合之间的交互。当聚合发生重要状态变化时可以发布领域事件其他聚合可以订阅这些事件并做出反应。// 领域事件示例 public class OrderPaidEvent { private final Long orderId; private final String customerId; private final BigDecimal amount; private final LocalDateTime occurredOn; public OrderPaidEvent(Long orderId, String customerId, BigDecimal amount) { this.orderId orderId; this.customerId customerId; this.amount amount; this.occurredOn LocalDateTime.now(); } // getter方法... } // 在聚合中发布事件 public class Order { // ... private final ListDomainEvent domainEvents new ArrayList(); public void pay() { // ...支付逻辑 this.status OrderStatus.PAID; domainEvents.add(new OrderPaidEvent(this.id, this.customerId, this.getTotalAmount())); } public ListDomainEvent getDomainEvents() { return Collections.unmodifiableList(domainEvents); } public void clearDomainEvents() { domainEvents.clear(); } }领域事件的典型用途维护跨聚合的一致性最终一致性触发业务流程如支付后发货更新读模型CQRS模式系统集成通知外部系统6. DDD实战技巧与性能优化6.1 延迟加载与聚合设计大型聚合可能导致性能问题。一些优化策略懒加载对关联集合使用懒加载拆分聚合将大聚合拆分为多个小聚合非规范化在聚合内适当冗余数据避免加载关联对象// 使用JPA的懒加载 public class Order { OneToMany(fetch FetchType.LAZY, cascade CascadeType.ALL, orphanRemoval true) private ListOrderItem items new ArrayList(); }6.2 并发控制聚合作为一致性边界需要考虑并发控制。常用方法乐观锁使用版本号控制悲观锁在数据库层面加锁领域事件使用事件溯源模式// JPA乐观锁示例 public class Order { Version private Long version; // ... }6.3 CQRS模式命令查询职责分离CQRS是DDD中常用的模式将读写模型分离命令端处理业务逻辑使用完整的聚合模型查询端优化查询使用扁平的DTO模型// 查询专用模型 public class OrderSummary { private Long orderId; private String customerName; private BigDecimal totalAmount; private String status; // 简单字段无复杂业务逻辑 }6.4 测试策略DDD代码需要有良好的测试覆盖单元测试测试聚合内部的业务逻辑集成测试测试仓储实现和领域服务契约测试测试领域事件的契约// 聚合单元测试示例 class OrderTest { Test void shouldAddItemWhenOrderIsNotPaid() { Order order new Order(1L, customer1, new Address(上海, 上海市, 浦东新区)); OrderItem item new OrderItem(1L, prod1, 商品1, BigDecimal.valueOf(100), 2); order.addItem(item); assertEquals(1, order.getItems().size()); } Test void shouldThrowWhenAddingItemToPaidOrder() { Order order new Order(1L, customer1, new Address(上海, 上海市, 浦东新区)); order.pay(); OrderItem item new OrderItem(1L, prod1, 商品1, BigDecimal.valueOf(100), 2); assertThrows(IllegalStateException.class, () - order.addItem(item)); } }7. 从理论到实践DDD实施路线图对于想要在项目中实施DDD的团队建议采用渐进式策略识别核心子域找到业务中最复杂、最有价值的部分建立通用语言与业务专家一起定义术语设计聚合从小的、明确的聚合开始实现仓储为聚合根创建仓储添加领域服务处理跨聚合逻辑引入领域事件解耦聚合间的交互逐步扩展从核心域扩展到支撑子域常见陷阱与规避方法陷阱表现解决方案贫血模型只有getter/setter没有业务逻辑将业务逻辑移入聚合中过大聚合加载性能差并发冲突多应用小聚合原则拆分大聚合仓储滥用仓储变成万能DAO一个聚合根一个仓储不暴露内部对象过度设计为不复杂的业务使用DDD只在核心域使用DDD其他用简单模式