这节课非常关键它决定后续所有的事务边界状态流转业务规则封装位置领域事件触发点Repository 设计方式CQRS 与事件驱动的上下文切割“聚合设计”是 DDD 战术建模中最难也是最重要的部分。聚合设计原则总结1. 聚合边界由强一致性决定 2. 聚合之间不能相互持有实体引用 3. 事务范围 聚合边界 4. 修改聚合必须通过聚合根 5. 不变量必须在聚合内部保持第3课聚合与聚合根Aggregate, Aggregate Root本节目标明确聚合边界学会判断 Entity / Value Object理解事务一致性边界让模型“不会不小心写错业务规则”设计 Order 聚合的正确结构聚合Aggregate很多人以为“聚合”就是“一组对象”但在 DDD 中有明确定义聚合是领域中保持强一致性的边界。聚合根是唯一允许外部修改此聚合状态的入口。因此一个正确设计的聚合必须满足所有业务不变量Invariants必须在聚合内部保持成立例如订单支付时总金额不能为 0聚合的修改只能通过聚合根完成聚合之间不能直接引用内部实体只能引用 ID事务范围必须落在单个聚合内不允许跨聚合事务聚合根Aggregate Root作为聚合的唯一入口外部不能直接修改聚合内部其他对象持久化、仓储都以聚合根为单位示例我们系统中的两个聚合聚合名聚合根内容关系OrderOrderOrderItem 列表、状态、总金额引用 MenuItemMenuMenuMenuItem 列表提供菜品信息从统一语言到聚合划分我们从第1课的统一语言中提炼出候选概念MenuMenuItemOrderOrderItemMoneyPaymentInventoryDelivery接下来要判断哪些是实体Entity哪些是值对象VO哪些应该属于同一个聚合Entity vs Value Object 判断规则Value Object 的特点不依赖 ID不可变只关心属性值组合、安全、可替换符合这些的有Money金额MenuItem菜单项当前菜品的名字、价格快照补充说明MenuItem 应该是 VO因为它属于“价格快照”不可随菜单变化而影响历史订单。如果把 MenuItem 设计成 Entity你的订单历史会发生“穿越”价格被更新、变更名称等。Entity 的特点有 ID生命周期内可变化有领域行为同 ID 即同对象符合这些的有OrderOrderItem ← 它属于 Order 聚合Menu独立聚合确定聚合边界核心我们需要回答一个问题Order OrderItem是否是一个聚合是的这是一个典型“聚合 内部实体”的结构。Order (AR) ├─ OrderItem (Entity) └─ Money, TotalAmount (Value Object)Order 是聚合根AR, Aggregate Root原因它控制所有订单项的增删改它拥有状态机Created / Paid / Completed它维护金额不变量因此所有对 OrderItem 的修改都必须通过 Order 来进行。也就是说下面的做法是非法的orderItem.setQuantity(5);正确做法应该是order.changeItemQuantity(itemId,5);为什么 Menu 是另一个聚合因为菜单变化不影响订单历史因为订单不应该与菜单事务绑定因为跨聚合事务是 DDD 禁止的这是 DDD 中非常关键的原则聚合之间只能通过标识ID协作不允许实体对象跨聚合引用。不允许这样写classOrder{privateMenuItemmenuItem;// ❌ 错跨聚合引用}必须这样classOrder{privateMenuItemSnapshotsnapshot;// VO记录下单时的名称、价格}事务一致性边界决定聚合大小DDD 给了一条定律事务只保证单个聚合内部数据一致性。不允许更新多个聚合的跨聚合事务。例如“下单 扣库存”不能放在一个聚合事务里“支付订单 发送券”不能放在一个聚合事务里它们必须通过领域事件或 Saga / 业务流程编排或补偿模式否则会导致锁竞争放大事务冲突严重系统无法扩展设计订单聚合下面是一个专业 DDD 项目中 Order 聚合的结构Order 聚合 ├─ Order聚合根 │ ├─ id │ ├─ status │ ├─ totalAmountMoney │ ├─ itemsListOrderItem │ ├─ 行为pay / confirm / complete │ └─ 不变量金额0、状态流转正确 └─ OrderItem实体 ├─ productName ├─ quantity ├─ priceMoney └─ subtotalMoney行为业务规则应该放在哪里DDD 规定领域行为必须放在聚合根 / 实体内部而不是 Service。例如订单支付publicvoidpay(){if(status!CREATED)thrownewIllegalStateException(...);this.statusPAID;raiseEvent(newOrderPaidEvent(id,totalAmount));}而不是orderService.pay(order);应用服务应该像这样publicvoidpayOrder(OrderIdid){OrderorderorderRepository.load(id);order.pay();// ← 业务行为在领域对象中orderRepository.save(order);}聚合的“正确性”来自不变量Invariants订单聚合必须保证金额不能小于 0OrderItem 的数量必须大于 0OrderStatus 流转必须正确订单总金额应该是所有 OrderItem 小计之和已完成订单不能修改这些规则一旦散落在多个层系统将无法控制一致性。DDD 的重要原则不变量必须由聚合根统一维护。代码实现项目结构新增一个聚合目录domain/ ├── order/ │ └── model/ │ ├── Order.java │ ├── OrderItem.java │ ├── OrderStatus.java │ ├── OrderId.java │ └── ... └── menu/ └── model/ ├── Menu.java ├── MenuItem.java └── MenuId.javaMenu.java— 菜单聚合根packagecom.example.ordersystem.domain.menu.model;importjava.util.ArrayList;importjava.util.List;/** * 聚合根Menu * 表示一个菜单集合例如当天可售菜品 * 由 MenuItem 组成。 */publicclassMenu{privatefinalMenuIdid;privatefinalListMenuItemitemsnewArrayList();publicMenu(MenuIdid){this.idid;}/** 添加菜品到菜单中 */publicvoidaddItem(Stringname,doubleprice){items.add(newMenuItem(name,price));}/** 查找菜品 */publicMenuItemfindItem(Stringname){returnitems.stream().filter(item-item.getName().equalsIgnoreCase(name)).findFirst().orElseThrow(()-newIllegalArgumentException(菜品不存在: name));}publicListMenuItemgetItems(){returnitems;}publicMenuIdgetId(){returnid;}}MenuItem.java— 值对象packagecom.example.ordersystem.domain.menu.model;/** * 值对象MenuItem * 表示菜单中的单个菜品。 * 不可变没有标识符。 */publicclassMenuItem{privatefinalStringname;privatefinaldoubleprice;publicMenuItem(Stringname,doubleprice){if(price0)thrownewIllegalArgumentException(价格必须大于0);this.namename;this.priceprice;}publicStringgetName(){returnname;}publicdoublegetPrice(){returnprice;}}MenuId.javapackagecom.example.ordersystem.domain.menu.model;importjava.util.UUID;/** * 值对象MenuId * 用于唯一标识菜单。 */publicclassMenuId{privatefinalStringvalue;publicMenuId(Stringvalue){this.valuevalue;}publicstaticMenuIdnewId(){returnnewMenuId(UUID.randomUUID().toString());}publicStringgetValue(){returnvalue;}}修改Order.java— 订单聚合根关联MenuItem我们不直接存MenuItem对象而是存储引用信息例如菜名和价格保持聚合独立防止跨聚合事务。packagecom.example.ordersystem.domain.order.model;importcom.example.ordersystem.domain.menu.model.MenuItem;importcom.example.ordersystem.domain.shared.DomainException;importjava.util.ArrayList;importjava.util.List;/** * 聚合根Order * 管理订单内的所有状态变化和行为。 */publicclassOrder{privatefinalOrderIdid;privatefinalListOrderItemitemsnewArrayList();privateMoneytotalAmountMoney.zero();privateOrderStatusstatusOrderStatus.CREATED;publicOrder(OrderIdid){this.idid;}/** * 从菜单项添加菜品通过 MenuItem * 说明这里接收来自 Menu 聚合的“只读副本”不跨聚合修改。 */publicvoidaddMenuItem(MenuItemmenuItem,intquantity){if(status!OrderStatus.CREATED){thrownewDomainException(订单创建后才能添加菜品);}OrderItemitemnewOrderItem(menuItem.getName(),quantity,newMoney(menuItem.getPrice()));items.add(item);totalAmounttotalAmount.add(item.subtotal());}// 状态变更操作publicvoidpay(){if(status!OrderStatus.CREATED)thrownewDomainException(必须先创建才能支付);this.statusOrderStatus.PAID;}publicvoidconfirm(){if(status!OrderStatus.PAID)thrownewDomainException(订单必须已支付才能确认);this.statusOrderStatus.CONFIRMED;}publicvoidcomplete(){if(status!OrderStatus.CONFIRMED)thrownewDomainException(订单未确认无法完成);this.statusOrderStatus.COMPLETED;}publicvoidcancel(){if(statusOrderStatus.COMPLETED)thrownewDomainException(已完成订单不能取消);this.statusOrderStatus.CANCELLED;}// Getter 方法publicOrderIdgetId(){returnid;}publicListOrderItemgetItems(){returnitems;}publicMoneygetTotalAmount(){returntotalAmount;}publicOrderStatusgetStatus(){returnstatus;}}应用服务OrderWithMenuService.java这个服务会先构建菜单再创建订单展示聚合间的使用方式。packagecom.example.ordersystem.application.order;importcom.example.ordersystem.domain.menu.model.Menu;importcom.example.ordersystem.domain.menu.model.MenuId;importcom.example.ordersystem.domain.order.model.Order;importcom.example.ordersystem.domain.order.model.OrderId;importcom.example.ordersystem.domain.order.repository.OrderRepository;importorg.springframework.stereotype.Service;/** * 应用服务演示 Order 聚合如何使用 Menu 聚合的数据。 */ServicepublicclassOrderWithMenuService{privatefinalOrderRepositoryrepository;publicOrderWithMenuService(OrderRepositoryrepository){this.repositoryrepository;}/** * 创建菜单和订单聚合之间通过数据引用而非对象依赖 */publicOrdercreateOrderFromMenu(){// 创建菜单独立聚合MenumenunewMenu(MenuId.newId());menu.addItem(Burger,25.0);menu.addItem(Fries,10.0);menu.addItem(Coke,5.0);// 创建订单独立聚合OrderordernewOrder(OrderId.newId());order.addMenuItem(menu.findItem(Burger),1);order.addMenuItem(menu.findItem(Fries),2);// 保存订单repository.save(order);returnorder;}}控制层MenuOrderController.javapackagecom.example.ordersystem.interfaces.api;importcom.example.ordersystem.application.order.OrderWithMenuService;importcom.example.ordersystem.domain.order.model.Order;importorg.springframework.web.bind.annotation.*;/** * 控制层演示从菜单创建订单 */RestControllerRequestMapping(/menus)publicclassMenuOrderController{privatefinalOrderWithMenuServiceservice;publicMenuOrderController(OrderWithMenuServiceservice){this.serviceservice;}GetMapping(/order)publicOrdercreateOrderFromMenu(){returnservice.createOrderFromMenu();}}测试访问GET http://localhost:8080/menus/order输出{id:{value:bf04d795-d20f-4bc2-a2fb-a6ff287dcc85},items:[{name:Burger,quantity:1,price:{amount:25.0}},{name:Fries,quantity:2,price:{amount:10.0}}],totalAmount:{amount:45.0},status:CREATED}第3课总结你现在理解了聚合Aggregate定义业务一致性边界聚合根Aggregate Root控制聚合内部行为聚合之间通过引用ID或值对象交互而不是直接调用方法每个聚合都独立持久化