1. 核心概念定义
1.1 聚合(Aggregate)
聚合是DDD中的业务一致性边界,将相关的实体和值对象组合成一个整体,确保聚合内的数据始终保持一致性。
核心原则:
- 聚合内的所有操作都必须通过聚合根进行
- 聚合内的业务规则必须得到保证
- 聚合之间通过唯一标识引用,不直接关联
1.2 聚合根(Aggregate Root)
聚合根是聚合的唯一入口,负责:
- 维护聚合内的业务规则和数据一致性
- 作为聚合的唯一标识和外部访问点
- 协调聚合内的实体和值对象
1.3 实体(Entity)vs 值对象(Value Object)
| 特征 | 实体 | 值对象 |
|---|---|---|
| 唯一标识 | 有(如OrderID) | 无(通过属性值唯一) |
| 可变性 | 可变(如订单状态变更) | 不可变(如地址修改需创建新对象) |
| 生命周期 | 独立,由聚合根管理 | 依赖于实体,无独立生命周期 |
| 相等性 | 通过ID判断 | 通过属性值判断 |
2. 订单领域聚合设计
2.1 订单领域核心元素识别
业务场景:用户下单流程
- 创建订单,包含多个订单项
- 订单有状态(待支付、已支付、已取消等)
- 订单项包含商品信息、数量、单价
- 订单关联收货地址、支付信息
- 订单金额 = 所有订单项金额之和
2.2 聚合边界划分
订单聚合(Order Aggregate):
- 聚合根:Order(订单)
- 实体:OrderItem(订单项,有唯一标识)
- 值对象:Address(地址)、PaymentInfo(支付信息)、ProductSnapshot(商品快照)
用户聚合(User Aggregate):
- 聚合根:User(用户)
- 值对象:UserProfile(用户信息)、ContactInfo(联系信息)
商品聚合(Product Aggregate):
- 聚合根:Product(商品)
- 值对象:ProductDetail(商品详情)、PriceInfo(价格信息)
2.3 订单聚合完整结构
3. 聚合根与聚合对象的详细设计
3.1 订单聚合根(Order)
3.1.1 核心属性
@AggregateRootpublicclassOrder{// 聚合根唯一标识privateOrderIDorderId;// 关联外部聚合的ID(非直接引用)privateUserIDuserId;// 聚合内实体集合privateList<OrderItem>orderItems;// 聚合内值对象privateAddressshippingAddress;privatePaymentInfopaymentInfo;// 聚合根状态privateOrderStatusstatus;privateBigDecimaltotalAmount;privateLocalDateTimecreatedAt;privateLocalDateTimeupdatedAt;// 构造函数和业务方法...}3.1.2 业务方法(确保聚合内一致性)
// 构造函数:创建订单,确保订单项数量和金额一致publicOrder(OrderIDorderId,UserIDuserId,List<OrderItem>orderItems,AddressshippingAddress){// 业务规则1:订单项不能为空if(CollectionUtils.isEmpty(orderItems)){thrownewDomainException("订单项不能为空");}// 业务规则2:计算总金额,确保与订单项金额一致BigDecimalcalculatedTotal=orderItems.stream().map(OrderItem::getTotalPrice).reduce(BigDecimal.ZERO,BigDecimal::add);// 初始化聚合根this.orderId=orderId;this.userId=userId;this.orderItems=newArrayList<>(orderItems);this.shippingAddress=shippingAddress;this.totalAmount=calculatedTotal;this.status=OrderStatus.CREATED;this.createdAt=LocalDateTime.now();this.updatedAt=LocalDateTime.now();}// 取消订单:更新订单状态,确保所有订单项状态一致publicvoidcancel(){// 业务规则:只有待支付状态的订单才能取消if(this.status!=OrderStatus.CREATED){thrownewDomainException("只有待支付状态的订单才能取消");}// 更新订单状态this.status=OrderStatus.CANCELLED;this.updatedAt=LocalDateTime.now();// 无需更新订单项状态,因为订单项状态由订单状态驱动}// 添加订单项:确保金额一致性publicvoidaddOrderItem(OrderItemorderItem){// 业务规则:订单已支付或取消,不能添加订单项if(this.status!=OrderStatus.CREATED){thrownewDomainException("订单已支付或取消,不能添加订单项");}// 添加订单项this.orderItems.add(orderItem);// 更新总金额this.totalAmount=this.totalAmount.add(orderItem.getTotalPrice());this.updatedAt=LocalDateTime.now();}3.2 订单项实体(OrderItem)
3.2.1 核心属性
publicclassOrderItem{// 订单项唯一标识(在聚合内唯一,全局唯一需包含OrderID)privateOrderItemIDorderItemId;// 商品快照(值对象,记录下单时的商品信息)privateProductSnapshotproductSnapshot;// 订单项属性privateIntegerquantity;privateBigDecimalunitPrice;privateBigDecimaltotalPrice;// 构造函数和业务方法...}3.2.2 业务方法
// 构造函数:确保订单项金额计算正确publicOrderItem(OrderItemIDorderItemId,ProductSnapshotproductSnapshot,Integerquantity){// 业务规则1:数量必须大于0if(quantity<=0){thrownewDomainException("订单项数量必须大于0");}// 业务规则2:单价必须大于0if(productSnapshot.getPrice().compareTo(BigDecimal.ZERO)<=0){thrownewDomainException("商品单价必须大于0");}this.orderItemId=orderItemId;this.productSnapshot=productSnapshot;this.quantity=quantity;this.unitPrice=productSnapshot.getPrice();this.totalPrice=this.unitPrice.multiply(newBigDecimal(quantity));}// 更新数量:确保金额同步更新publicvoidupdateQuantity(IntegernewQuantity){// 业务规则:数量必须大于0if(newQuantity<=0){thrownewDomainException("订单项数量必须大于0");}this.quantity=newQuantity;this.totalPrice=this.unitPrice.multiply(newBigDecimal(newQuantity));}3.3 地址值对象(Address)
// 值对象:不可变,通过属性值判断相等性publicclassAddress{privatefinalStringprovince;privatefinalStringcity;privatefinalStringdistrict;privatefinalStringdetail;privatefinalStringzipCode;privatefinalStringcontactName;privatefinalStringcontactPhone;// 构造函数:一次性初始化,无setter方法publicAddress(Stringprovince,Stringcity,Stringdistrict,Stringdetail,StringzipCode,StringcontactName,StringcontactPhone){// 业务规则验证if(StringUtils.isBlank(province)){thrownewDomainException("省份不能为空");}// 其他规则验证...this.province=province;this.city=city;this.district=district;this.detail=detail;this.zipCode=zipCode;this.contactName=contactName;this.contactPhone=contactPhone;}// 只提供getter方法,无setterpublicStringgetProvince(){returnprovince;}// 其他getter方法...// 值对象相等性判断:通过所有属性值比较@Overridepublicbooleanequals(Objecto){if(this==o)returntrue;if(o==null||getClass()!=o.getClass())returnfalse;Addressaddress=(Address)o;returnObjects.equals(province,address.province)&&Objects.equals(city,address.city)&&Objects.equals(district,address.district)&&Objects.equals(detail,address.detail)&&Objects.equals(zipCode,address.zipCode)&&Objects.equals(contactName,address.contactName)&&Objects.equals(contactPhone,address.contactPhone);}}4. 聚合根与聚合对象的设计原则
4.1 边界设计原则
- 业务一致性优先:聚合边界应根据业务规则确定,确保同一业务规则内的实体和值对象在同一聚合内
- 避免过大聚合:单个聚合包含的实体不应超过10个,否则会影响性能和可维护性
- 通过ID引用其他聚合:聚合之间不应直接引用,而应通过唯一标识关联,避免聚合过大
4.2 一致性维护原则
- 聚合根负责一致性:聚合内的所有业务规则由聚合根维护,外部不能直接修改聚合内的实体
- 事务边界与聚合边界一致:一个事务只应修改一个聚合,避免分布式事务
- 不可变值对象:值对象应设计为不可变,避免意外修改
4.3 访问控制原则
- 外部只能访问聚合根:外部系统或其他聚合只能通过聚合根的方法访问聚合内的实体和值对象
- 聚合内实体可直接访问:聚合内的实体可以直接访问同一聚合内的其他实体和值对象
- 禁止跨聚合修改:一个聚合的方法不应修改另一个聚合的状态
5. 聚合根与仓储(Repository)的关系
5.1 仓储设计原则
- 每个聚合根对应一个仓储:Order聚合根对应OrderRepository,User聚合根对应UserRepository
- 仓储只返回完整聚合:仓储的
findById方法必须返回完整的聚合根实例,包含所有关联的实体和值对象 - 仓储负责聚合的持久化:仓储负责将整个聚合保存到数据库,或从数据库加载整个聚合
5.2 订单仓储接口设计
// 只针对聚合根Order的仓储publicinterfaceOrderRepository{// 保存完整聚合voidsave(Orderorder);// 根据聚合根ID加载完整聚合Optional<Order>findById(OrderIDorderId);// 删除完整聚合voiddelete(Orderorder);// 根据业务条件查询聚合根列表List<Order>findByUserId(UserIDuserId);List<Order>findByStatus(OrderStatusstatus);}6. 订单领域聚合的实际应用
6.1 创建订单流程
// 1. 准备订单数据UserIDuserId=newUserID("user-123");OrderIDorderId=newOrderID("order-456");// 2. 创建值对象AddressshippingAddress=newAddress("广东省","深圳市","南山区","科技园","518000","张三","13800138000");ProductSnapshotproductSnapshot1=newProductSnapshot(newProductID("product-789"),"iPhone 15",newBigDecimal(9999),newBigDecimal(9999));ProductSnapshotproductSnapshot2=newProductSnapshot(newProductID("product-012"),"AirPods Pro",newBigDecimal(1999),newBigDecimal(1999));// 3. 创建实体(通过聚合根方法,而非直接实例化)OrderItemorderItem1=newOrderItem(newOrderItemID("order-item-345"),productSnapshot1,1);OrderItemorderItem2=newOrderItem(newOrderItemID("order-item-678"),productSnapshot2,2);// 4. 创建聚合根(确保聚合内一致性)Orderorder=newOrder(orderId,userId,Arrays.asList(orderItem1,orderItem2),shippingAddress);// 5. 调用聚合根业务方法order.addOrderItem(newOrderItem(newOrderItemID("order-item-901"),productSnapshot1,1));// 6. 保存完整聚合到仓储orderRepository.save(order);6.2 取消订单流程
// 1. 从仓储加载完整聚合Optional<Order>orderOpt=orderRepository.findById(newOrderID("order-456"));if(orderOpt.isPresent()){Orderorder=orderOpt.get();// 2. 调用聚合根业务方法(确保业务规则)order.cancel();// 3. 保存更新后的聚合orderRepository.save(order);}7. 聚合设计的常见误区
7.1 误区1:聚合过大
问题:将用户、订单、商品都放在同一个聚合内,导致聚合过大,性能下降
解决:按业务边界拆分,用户、订单、商品分别作为独立聚合,通过ID关联
7.2 误区2:直接关联其他聚合
问题:订单聚合直接引用User对象,导致订单聚合依赖用户聚合的所有变化
解决:订单聚合只保存UserID,需要用户信息时通过UserID查询User聚合
7.3 误区3:外部直接修改聚合内实体
问题:外部系统直接修改OrderItem的数量,绕过了Order聚合根的业务规则
解决:将OrderItem的setter方法设为私有,只能通过Order的addOrderItem或updateOrderItem方法修改
7.4 误区4:聚合内实体过多
问题:一个订单包含数百个订单项,导致聚合加载和保存性能下降
解决:考虑将订单项拆分为独立聚合,或使用分页加载
8. 总结
8.1 聚合根与聚合对象的核心价值
- 业务一致性:确保同一业务规则内的数据始终保持一致
- 清晰的边界:明确业务领域的划分,提高系统的可维护性
- 性能优化:减少跨聚合的关联查询,提高系统性能
- 可测试性:聚合内的业务规则可以独立测试,提高代码质量
8.2 订单领域的聚合设计总结
| 聚合根 | 聚合内实体 | 聚合内值对象 | 核心业务规则 |
|---|---|---|---|
| Order | OrderItem | Address、PaymentInfo、ProductSnapshot | 1. 订单项不能为空 2. 订单总金额等于订单项金额之和 3. 只有待支付状态的订单才能取消 4. 订单项数量必须大于0 |
| User | - | UserProfile、ContactInfo | 1. 用户名不能为空 2. 手机号必须唯一 |
| Product | - | ProductDetail、PriceInfo | 1. 商品名称不能为空 2. 商品价格必须大于0 |
通过合理设计聚合根和聚合对象,可以构建出清晰、一致、高性能的DDD领域模型,为微服务架构奠定坚实的基础。