DDD学习第3课设计聚合与聚合根
这节课非常关键它决定后续所有的事务边界状态流转业务规则封装位置领域事件触发点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或值对象交互而不是直接调用方法每个聚合都独立持久化

相关新闻

前端转大模型:用一次交付过程做复盘

前端转大模型:用一次交付过程做复盘

聊《做过前端的人学大模型,哪些经验可以直接迁移?》之前,先说一句实在的:别急着背概念,先看它在真实项目里到底解决什么问题。摘要先把这篇文章的目标说清楚:看完之后,你应该能判断这件事值不值…

2026/7/15 18:30:16 阅读更多 →
C++ Json-RPC服务端模块化设计:网络、服务管理与消息路由

C++ Json-RPC服务端模块化设计:网络、服务管理与消息路由

1. 项目概述与核心思路做C服务端开发,模块划分是个绕不开的坎。尤其是当你从零开始构建一个像Json-Rpc框架这样的基础设施时,怎么把一堆功能合理地拆开、再优雅地组装起来,直接决定了后续的代码可维护性、扩展性和团队协作效率。很多新手容易…

2026/7/15 18:30:16 阅读更多 →
EBAZ4205开发环境搭建:Vivado + PetaLinux完整配置教程

EBAZ4205开发环境搭建:Vivado + PetaLinux完整配置教程

EBAZ4205开发环境搭建:Vivado PetaLinux完整配置教程 【免费下载链接】EBAZ4205 A 5$ Xilinx ZYNQ development board. 项目地址: https://gitcode.com/gh_mirrors/eb/EBAZ4205 EBAZ4205是一款性价比极高的Xilinx ZYNQ开发板,本文将详细介绍如何…

2026/7/15 18:30:16 阅读更多 →

最新新闻

Codex 接入团队:当 AI 开始写代码,维护成本才是最大的坑

Codex 接入团队:当 AI 开始写代码,维护成本才是最大的坑

这篇不先堆名词。我们把《Codex火了之后,为什么团队反而更关心维护成本?》拆成几级台阶,看完至少知道下一步该学什么、该练什么。摘要先把这篇文章的目标说清楚:看完之后,你应该能判断这件事值不值得做,以及…

2026/7/15 20:47:10 阅读更多 →
VC++ ODBC异步通知模式实战:告别UI卡顿,实现高效数据库查询

VC++ ODBC异步通知模式实战:告别UI卡顿,实现高效数据库查询

1. 项目概述:为什么要在VC中折腾ODBC异步调用?如果你用VC写过数据库应用,特别是那些需要处理大量数据或者连接远程SQL Server、Oracle的服务端程序,肯定对界面“卡死”深有体会。用户点了个查询按钮,程序就进入“未响应…

2026/7/15 20:45:09 阅读更多 →
408-C语言入门基础3

408-C语言入门基础3

目录 一、格式修饰符 二、C语言的基本语句 三、顺序结构 四、选择结构 五、循环结构 一、格式修饰符 二、C语言的基本语句 1.表达式语句 (1)由表达式加上“;”组成的语句称为表达式语句。他的一般形式为: 表达式&#xff…

2026/7/15 20:45:09 阅读更多 →
深入解析TI CC2640R2F三核架构与低功耗蓝牙5.1开发实战

深入解析TI CC2640R2F三核架构与低功耗蓝牙5.1开发实战

1. 项目概述:深入解析CC2640R2F这颗无线MCU的“芯”价值在物联网设备开发的江湖里,选对一颗“芯”往往决定了整个项目的成败。是功耗、性能、成本还是开发便利性?从业十多年,我经手过不少无线项目,从早期的蓝牙2.0到现…

2026/7/15 20:45:09 阅读更多 →
计算机毕业设计之jsp学生宿舍管理系统设计与实现

计算机毕业设计之jsp学生宿舍管理系统设计与实现

近年来互联网络的迅猛发展和电子终端设备的普及,赋予了各行业充足的发展空间。学生宿舍管理系统相比于传统信息技术,时效性是它最大的特色,已经在电子娱乐、经济等中发挥着举足轻重的作用。更是短时间内迅速扩大了线上管理系统的规模。学生只…

2026/7/15 20:43:08 阅读更多 →
使用ffprobe命令深度剖析视频流元数据

使用ffprobe命令深度剖析视频流元数据

1. 认识ffprobe:视频元数据的瑞士军刀 第一次接触ffprobe时,我正被一个直播流的卡顿问题困扰。当时只知道用播放器反复测试,直到同事扔给我一行命令: ffprobe -show_streams rtsp://example.com/live 。这个看似简单的工具瞬间揭…

2026/7/15 20:43:08 阅读更多 →

日新闻

YOLO11 改进 - 特征融合 | STFFM空间时间特征融合模块,强化时空互补、抑制噪声,助力小目标检测高效涨点

YOLO11 改进 - 特征融合 | STFFM空间时间特征融合模块,强化时空互补、抑制噪声,助力小目标检测高效涨点

前言 本文介绍了面向红外小目标检测的时空特征融合模块——STFFM,用于增强复杂背景下目标与噪声、杂波的区分能力。该方法通过拼接空间特征与时间/运动特征,并结合通道注意力、空间注意力和残差增强机制,实现对关键语义通道与疑似目标区域的…

2026/7/15 0:01:00 阅读更多 →
YOLO26 改进 - 特征融合 | STFFM空间时间特征融合模块,强化时空互补、抑制噪声,助力小目标检测高效涨点

YOLO26 改进 - 特征融合 | STFFM空间时间特征融合模块,强化时空互补、抑制噪声,助力小目标检测高效涨点

前言 本文介绍了面向复杂背景小目标检测的时空特征融合模块——STFFM。该模块通过空间分支与时间/运动分支的特征拼接,引入通道注意力和空间注意力对融合特征进行自适应筛选,并结合残差增强与通道压缩,突出目标区域、抑制背景噪声。我们将 S…

2026/7/15 0:01:00 阅读更多 →
行星减速机为什么能提高扭矩?从功率守恒到输出扭矩校核

行星减速机为什么能提高扭矩?从功率守恒到输出扭矩校核

一、为什么减速以后扭矩会增大 旋转机械的功率、转速和扭矩之间存在以下关系: T 9550 P n 其中: T为扭矩,单位Nm; P为功率,单位kW; n为转速,单位r/min。 在功率基本不变的情况下:…

2026/7/15 0:03:00 阅读更多 →

周新闻

互联网大厂 Java 求职面试:燕双非的搞笑回答与技术探讨

互联网大厂 Java 求职面试:燕双非的搞笑回答与技术探讨

互联网大厂 Java 求职面试:燕双非的搞笑回答与技术探讨 在一个阳光明媚的上午,互联网大厂的面试官坐在桌前,准备迎接他的面试候选人——燕双非,一个以搞笑和幽默著称的程序员。第一轮提问 面试官:燕双非,作…

2026/7/14 16:53:23 阅读更多 →
车载以太网PMA测试设备选型:示波器、VNA、信号源3类仪器关键参数与预算评估

车载以太网PMA测试设备选型:示波器、VNA、信号源3类仪器关键参数与预算评估

车载以太网PMA测试设备选型:示波器、VNA、信号源3类仪器关键参数与预算评估在智能驾驶和车联网技术快速发展的今天,车载以太网作为新一代车载网络的核心传输技术,其物理层性能直接决定了数据传输的可靠性和稳定性。1000BASE-T1作为当前主流的…

2026/7/15 19:42:20 阅读更多 →
VSCode EIDE 插件 2.0:APM32/STM32 项目迁移实战,5步完成Keil工程转换

VSCode EIDE 插件 2.0:APM32/STM32 项目迁移实战,5步完成Keil工程转换

VSCode EIDE 插件 2.0:APM32/STM32 项目迁移实战指南嵌入式开发领域正经历一场工具链的静默革命。当传统Keil用户首次打开VSCode的扩展市场搜索EIDE时,往往会惊讶于这个看似简单的插件竟能重构十余年的开发习惯。本文将揭示如何用五个精准步骤&#xff0…

2026/7/15 17:52:08 阅读更多 →

月新闻