DDD 战术设计:从实体到聚合根,构建领域模型
DDD 战术设计从实体到聚合根构建领域模型目录实体值对象怎么判断聚合聚合根实战Order 聚合小结上一篇我们聊了战略设计统一语言、限界上下文、上下文映射。业务边界划清楚了接下来就是往边界里面填东西。每个上下文内部的领域模型该怎么设计很多人第一次接触 DDD 战术设计会把它理解成换一种方式写 Java 类。定义几个 Entity加几个 VO和传统 CRUD 似乎没什么区别。但战术设计关注的不是类怎么拆而是代码里的对象是否能表达真实业务概念业务规则是否属于正确的领域对象。如果只是把数据库表映射成 Entity再把逻辑全部写进 Service本质上仍然是传统贫血模型。对象只是数据容器行为散落在各处。战术设计要做的是反过来让对象自己管理自己的状态和规则。实体先看一个订单。订单有订单号、下单时间、订单状态、商品列表、收货地址。下单之后用户可以取消商家可以发货用户可以确认收货。整个过程中订单的状态一直在变待支付、已支付、已发货、已完成。但不管状态怎么变通过订单号总能找到它。上周下的单这周还能查到。这个订单号就是它的身份标识状态变化是它的业务行为。同时具备这两个特征的对象在 DDD 里叫实体Entity。需要注意DDD 中的实体不是数据库表的同义词。一个数据库表可能对应多个领域实体一个领域实体也不一定直接对应一张表。数据库关注的是存储和查询效率领域模型关注的是业务行为和规则。两者可能结构类似但关注点不同。publicclassOrder{privatefinalStringorderId;// 唯一标识创建后不可变privateOrderStatusstatus;// 可变状态privateListOrderItemitems;privateMoneytotalAmount;privateDeliveryInfodeliveryInfo;privateLocalDateTimecreateTime;privateOrder(StringorderId,StringuserId,DeliveryInfodeliveryInfo){this.orderIdorderId;this.statusOrderStatus.PENDING_PAYMENT;this.itemsnewArrayList();this.totalAmountnewMoney(BigDecimal.ZERO,CNY);this.deliveryInfodeliveryInfo;this.createTimeLocalDateTime.now();}publicstaticOrdercreate(StringuserId,DeliveryInfodeliveryInfo){returnnewOrder(generateId(),userId,deliveryInfo);}// 业务行为publicvoidcancel(){if(this.status!OrderStatus.PENDING_PAYMENT){thrownewBusinessException(只有待支付的订单可以取消);}this.statusOrderStatus.CANCELLED;}publicvoidpay(){if(this.status!OrderStatus.PENDING_PAYMENT){thrownewBusinessException(只有待支付的订单可以支付);}this.statusOrderStatus.PAID;}}注意几个设计细节。orderId是final的构造函数是private的创建只能通过Order.create()静态工厂方法。这样可以避免外部代码new Order()之后随意 setter绕过业务规则。领域对象通常不会暴露无约束的 setter而是通过业务方法改变状态。cancel()和pay()方法里的状态校验也体现了这一点。只有待支付的订单才能取消这条规则写在 Order 内部而不是散落在 Service 层。调用方不需要知道哪些状态允许取消直接调order.cancel()就行不满足条件实体自己会抛异常。这就是第一篇里提到的充血模型。传统架构下的 Order 通常只有 getter/setter业务逻辑全在 Service 里这种叫贫血模型。DDD 鼓励把业务规则放进实体本身让对象自己管自己。值对象再看订单里的金额。一笔 100 元的金额和另一笔 100 元的金额有区别吗从业务角度看没有。你不需要区分这个 100 元和那个 100 元。只要金额和币种相同它们就是一样的。你也不会去修改一个金额对象如果订单金额从 100 变成 120你直接用一个新的 Money(120) 替换掉旧的而不是money.setAmount(120)。这种没有唯一标识、不可变、用属性值判断相等的对象在 DDD 里叫值对象Value Object。publicclassMoney{privatefinalBigDecimalamount;privatefinalStringcurrency;publicMoney(BigDecimalamount,Stringcurrency){this.amountamount;this.currencycurrency;}publicMoneyadd(Moneyother){if(!this.currency.equals(other.currency)){thrownewBusinessException(币种不同无法相加);}returnnewMoney(this.amount.add(other.amount),this.currency);}Overridepublicbooleanequals(Objecto){if(thiso)returntrue;if(!(oinstanceofMoney))returnfalse;Moneymoney(Money)o;returnamount.compareTo(money.amount)0currency.equals(money.currency);}}amount和currency都是final的创建之后不能改。equals方法比较的是属性值不是对象地址。两个new Money(100, CNY)出来的对象equals返回true。值对象的add方法返回的是一个新对象不是修改自身。这和实体的行为方式完全不同实体是改自己的状态值对象是返回一个新的值。值对象还有一个常被忽略的价值消除基本类型迷恋Primitive Obsession。传统写法中金额用BigDecimal amount加String currency两个散字段表示方法签名是pay(BigDecimal amount, String currency)。调用方看到的是两个裸参数语义全靠注释和命名约定。用Money对象替代之后pay(Money amount)的语义一目了然。值对象让领域概念显式化而不仅仅是减少代码量。地址也是典型的值对象publicclassDeliveryInfo{privatefinalStringname;privatefinalStringphone;privatefinalStringprovince;privatefinalStringcity;privatefinalStringdistrict;privatefinalStringdetail;}收货地址不需要一个地址 ID来标识。北京市海淀区中关村大街 1 号就是这个地址换一种说法它不需要追踪这个地址昨天是什么样、今天是什么样。地址变了直接换一个新的 DeliveryInfo 对象。怎么判断什么时候用实体什么时候用值对象判断标准是这个东西在业务上需要追踪吗订单需要追踪用户要看订单状态客服要查订单历史财务要对账。所以订单是实体。金额不需要追踪100 元就是 100 元没有这个 100 元的生命周期这种说法。所以金额是值对象。地址不需要追踪大部分场景下用户改了收货地址旧地址就不用了不需要保留历史。所以地址是值对象。维度实体值对象唯一标识有订单号、用户ID没有可变性状态可变不可变相等判断比较 ID比较属性值生命周期有完整的创建、修改、销毁用完即弃替换无感典型例子Order、User、ProductMoney、Address、DateRange值对象在代码里往往比实体简单得多但它在 DDD 里的地位不低。用值对象替代裸的基本类型可以让领域模型更丰富。Money比BigDecimal amount String currency两个散字段更清晰DeliveryInfo比六个散字段更内聚。而且值对象的不可变性天然避免了很多并发和状态管理的问题。聚合实体和值对象有了下一个问题一堆相关的对象怎么组织一个订单包含多个订单项OrderItem包含收货信息DeliveryInfo包含金额Money。这些对象不是孤立的它们共同构成一个业务上完整的概念——“一笔订单”。如果你只改了 OrderItem 的数量但没有重新计算订单总金额数据就出现了不一致。聚合Aggregate本质上是一致性边界。一个业务规则如果需要同时保证多个对象的一致性这些对象应该属于同一个聚合。聚合内部的对象可以互相引用但外部不能直接访问聚合内部的对象只能通过一个统一的入口来操作。Order 聚合 │ ├── Order聚合根 ← 外部只能通过它访问 │ │ │ ├── OrderItem 1 ← 内部对象外部看不到 │ ├── OrderItem 2 │ └── OrderItem 3 │ └── DeliveryInfo ← 值对象属于聚合内部为什么需要这层约束假设外部代码可以直接操作 OrderItem// 直接修改订单项数量但没有更新总金额orderItem.setQuantity(10);// 此时 order.getTotalAmount() 还是旧值数据不一致了把 OrderItem 包在聚合内部外部代码就不能直接orderItem.setQuantity(10)必须通过 Order 来操作// 通过聚合根修改内部会自动重新计算总金额order.updateItemQuantity(itemId,10);聚合根在updateItemQuantity内部可以同时做两件事修改数量和重算金额。数据一致性由聚合根保证调用方不需要关心。但一致性边界不是越大越好。订单和用户、库存虽然有业务关联但它们不是同一个一致性边界。订单支付成功后需要通知库存扣减、积分发放但这些操作不需要和订单修改在同一个事务里完成。它们属于不同的聚合通过领域事件或领域服务协作。聚合根聚合的入口叫聚合根Aggregate Root。它是聚合内唯一对外可见的对象外部代码只能拿到聚合根的引用不能直接访问内部的 OrderItem 或 DeliveryInfo。设计聚合有几个原则值得记住。聚合要小。聚合内的对象越少锁定的范围越小并发冲突的概率越低。一个订单聚合包含订单项和收货信息就够了不要把商品信息、用户信息也塞进来。商品有自己的聚合用户也有自己的聚合。聚合之间通过 ID 引用不通过对象引用。订单聚合需要知道商品信息时不要在 Order 里持有一个 Product 对象的引用而是存一个productId。反例// 错误Order 直接持有 User 对象publicclassOrder{privateUseruser;// 加载 Order 时可能连带加载整个 User}这样做的问题有两个加载 Order 时可能连带加载整个 User 对象图两个聚合的生命周期被绑定改 User 的结构可能直接影响 Order。正确做法是只存 IDpublicclassOrder{privateStringuserId;// 只存 ID两个聚合完全解耦}两个聚合的生命周期完全独立各自维护各自的模型。这也呼应了上一篇讲的防腐层思想不同上下文保持自己的模型同一上下文内的不同聚合也保持各自独立。publicclassOrderItem{privateStringorderItemId;// 聚合内部的实体有自己的 IDprivateStringproductId;// 通过 ID 引用商品不持有 Product 对象privateStringproductName;// 快照下单时的商品名privateMoneyprice;// 快照下单时的单价privateintquantity;}OrderItem 用productId引用商品同时把下单时的商品名和价格做个快照存下来。这样商品那边改了名字或价格历史订单不受影响。你可能会问OrderItem 有orderItemId它不应该是值对象吗答案是实体。因为业务上需要区分不同的订单项用户买了两件 iPhone 15可能是两个独立的订单项不同数量、不同备注不能只靠商品 ID 判断是否相同。OrderItem 属于 Order 聚合但它仍然是实体。实战Order 聚合把前面的概念串起来来看 Order 聚合的核心行为。// 聚合根实体publicclassOrder{privatefinalStringorderId;privatefinalStringuserId;privateOrderStatusstatus;privateListOrderItemitems;privateDeliveryInfodeliveryInfo;privateMoneytotalAmount;privateOrder(StringorderId,StringuserId,DeliveryInfodeliveryInfo){this.orderIdorderId;this.userIduserId;this.statusOrderStatus.PENDING_PAYMENT;this.itemsnewArrayList();this.deliveryInfodeliveryInfo;this.totalAmountnewMoney(BigDecimal.ZERO,CNY);}publicstaticOrdercreate(StringuserId,DeliveryInfodeliveryInfo){returnnewOrder(generateId(),userId,deliveryInfo);}publicvoidaddItem(StringproductId,StringproductName,Moneyprice,intquantity){OrderItemitemnewOrderItem(productId,productName,price,quantity);this.items.add(item);this.totalAmountthis.totalAmount.add(item.getSubtotal());}publicvoidcancel(){if(this.status!OrderStatus.PENDING_PAYMENT){thrownewBusinessException(只有待支付的订单可以取消);}this.statusOrderStatus.CANCELLED;}publicvoidpay(){if(this.status!OrderStatus.PENDING_PAYMENT){thrownewBusinessException(只有待支付的订单可以支付);}this.statusOrderStatus.PAID;}}外部代码的使用方式// 创建订单OrderorderOrder.create(userId,deliveryInfo);order.addItem(productId,productName,price,2);// 取消订单聚合根自己校验状态order.cancel();调用方只和 Order 打交道不需要知道 OrderItem 怎么存、Money 怎么算、DeliveryInfo 是不是值对象。聚合根封装了内部的复杂性对外暴露的是业务行为而不是数据操作。小结战术设计里最基础的三个概念可以用一张图概括它们的关系实体有唯一标识、状态可变、封装业务规则值对象没有标识、不可变、用属性判断相等聚合把相关对象打包成一个整体通过聚合根保证数据一致性。聚合内部的模型设计好了有些业务逻辑还是放不下。下单时要扣库存、发优惠券、记录积分这些操作涉及多个聚合不属于任何一个实体。下一篇我们来聊领域服务。

相关新闻

PostgreSQL向量化扩展pgvector:原理、优化与应用实践

PostgreSQL向量化扩展pgvector:原理、优化与应用实践

1. PostgreSQL向量化革命:为什么需要pgvector?在传统关系型数据库领域,PostgreSQL一直以其强大的扩展性著称。但当我们进入AI时代,处理非结构化数据(如图片、文本、音频)的需求爆炸式增长,这些数…

2026/7/22 7:27:15阅读更多 →
AI率检测与降AI工具实战指南

AI率检测与降AI工具实战指南

1. 为什么我们需要关注AI率问题?在学术写作和内容创作领域,AI率已经成为衡量作品原创性的重要指标。简单来说,AI率指的是文本中被检测出由人工智能生成的比例。随着AI写作工具的普及,这个问题变得愈发突出。我最近帮一位研究生朋友…

2026/7/22 7:27:15阅读更多 →
边界监督在离线安全强化学习中的创新应用

边界监督在离线安全强化学习中的创新应用

1. 项目概述:边界监督在离线安全强化学习中的创新应用这个标题指向的是强化学习领域一个非常前沿的研究方向——如何在完全离线的训练环境中确保智能体的安全性。2025年NIPS会议论文《Boundary to region supervision for offline safe reinforcement learning》提出…

2026/7/22 7:27:15阅读更多 →
AI模型的智能本质与能力边界解析

AI模型的智能本质与能力边界解析

1. 项目概述 "这是智能模型吗?"这个标题引发了一个关于当前AI模型智能本质的深度思考。作为一名从业者,我经常被问到这个问题——无论是来自客户、合作伙伴还是普通用户。要回答这个问题,我们需要先明确什么是"智能"&…

2026/7/22 9:45:35阅读更多 →
CrossFlow框架:一步生成图像的潜在扩散模型革新

CrossFlow框架:一步生成图像的潜在扩散模型革新

1. 项目概述:CrossFlow如何颠覆传统图像生成流程 最近在CV圈引起热议的CrossFlow框架,本质上是在解决潜在扩散模型(Latent Diffusion Models)的一个结构性缺陷。传统流程中,模型先在潜空间(latent space&am…

2026/7/22 9:45:35阅读更多 →
咖啡苦味与品质的真相:从烘焙化学到品鉴实践

咖啡苦味与品质的真相:从烘焙化学到品鉴实践

1. 黑咖啡苦味与品质的真相探秘每次走进精品咖啡店,总能看到有人皱着眉头咽下一口黑咖啡后说"这咖啡够苦,品质肯定好"。这种将苦味与品质划等号的认知误区,在咖啡爱好者中相当普遍。实际上,咖啡的苦味程度与其品质并非简…

2026/7/22 9:45:35阅读更多 →
TI RTC寄存器编程实战:从BCD编码到时钟补偿的嵌入式时间管理

TI RTC寄存器编程实战:从BCD编码到时钟补偿的嵌入式时间管理

1. 项目概述与RTC核心价值 在嵌入式系统里,实时时钟(RTC)模块就像设备内置的一块永不掉电的机械手表。无论主处理器是酣睡在低功耗模式,还是经历了断电重启,这块“手表”都在默默地、精确地走着。它的核心价值&#xf…

2026/7/22 9:45:35阅读更多 →
FPC精密测量与GL-8000系列3D线激光轮廓测量仪在手机制造中的应用

FPC精密测量与GL-8000系列3D线激光轮廓测量仪在手机制造中的应用

FPC(柔性印刷电路板)是智能手机内部的核心连接部件。与传统的刚性PCB相比,FPC采用聚酰亚胺(PI)等柔性基材制成,厚度可薄至0.1mm以下。在相同载流量条件下,FPC比刚性PCB重量减轻约90%&#xff0c…

2026/7/22 9:45:35阅读更多 →
C++学生信息管理系统:从设计到实现的完整工程实践指南

C++学生信息管理系统:从设计到实现的完整工程实践指南

1. 项目概述:从零到一构建一个实用的学生信息管理系统 又到了期末,C课程设计的选题让人头疼。很多同学会选择“学生信息管理系统”,觉得它听起来简单,网上模板也多。但真正动手时才发现,从“能运行”到“好用、健壮、代…

2026/7/22 9:43:35阅读更多 →
Go语言静态资源打包方案对比与实践指南

Go语言静态资源打包方案对比与实践指南

1. 项目背景与核心需求在Go语言开发中,我们经常需要处理静态资源文件的打包问题。无论是Web应用的模板文件、前端资源,还是配置文件、证书等,都需要随程序一起分发。传统做法是将这些文件与编译后的二进制文件放在同一目录下,但这…

2026/7/22 0:53:59阅读更多 →
Go语言实现高性能LDAP认证服务的架构与实践

Go语言实现高性能LDAP认证服务的架构与实践

1. 项目背景与核心价值LDAP(轻量级目录访问协议)作为企业级身份认证的黄金标准,已经服务了超过80%的财富500强公司。我在金融科技领域实施统一认证体系时,发现传统Java方案存在启动慢、内存占用高等痛点。而Go语言凭借其协程并发模…

2026/7/22 0:53:59阅读更多 →
【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

更多请点击: https://intelliparadigm.com 第一章:AI面试官实战指南的核心价值与适用场景 AI面试官并非替代人类HR的“黑箱工具”,而是以可解释、可审计、可迭代的方式,赋能招聘全链路的关键基础设施。其核心价值在于将主观经验沉…

2026/7/22 0:53:59阅读更多 →
中小企业小程序开发公司怎么选:预算、上手和售后避坑指南

中小企业小程序开发公司怎么选:预算、上手和售后避坑指南

中小企业做小程序,最常见的矛盾是预算有限,但又不希望功能太单薄;没有技术团队,但又希望后续能自己运营;想快速上线,又担心隐性收费和售后失联。选型时如果只看“低价套餐”或“案例数量”,很容…

2026/7/22 0:01:17阅读更多 →
GEO优化如何沉淀长期内容资产?广拓时代谈AI搜索时代的内容ROI

GEO优化如何沉淀长期内容资产?广拓时代谈AI搜索时代的内容ROI

企业做营销,最怕钱花完了,资产没有留下。 效果广告能带来一段时间的曝光,但预算停止后,流量往往也随之停止。短视频内容可能在几天内冲高,也可能很快沉下去。AI搜索时代,企业需要重新思考一个问题&#xff…

2026/7/22 0:01:17阅读更多 →
Agent 终态判定:何时该停止思考、给出最终回复

Agent 终态判定:何时该停止思考、给出最终回复

Agent 终态判定:何时该停止思考、给出最终回复 一、你的 Agent 在"再想想"的循环里绕了 12 轮,用户已经关窗口了 Agent 与人最大的区别是:人知道什么时候该停下来给答案,Agent 会一直"想"下去。你给 Agent 接…

2026/7/22 0:01:17阅读更多 →
YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

如果你在部署 YOLOv8 时,发现推理速度只有可怜的 1-2 FPS,而别人的演示视频却能跑到 30 FPS 以上,那么问题很可能不在模型本身,而在于你的整个处理链路。很多开发者拿到一个训练好的 YOLOv8 模型后,会直接使用官方示例…

2026/7/21 22:53:50阅读更多 →
Coze与Dify对比指南:低代码AI应用开发从入门到实战

Coze与Dify对比指南:低代码AI应用开发从入门到实战

1. 从零到一:为什么你需要了解 Coze 和 Dify?如果你对 AI 应用开发感兴趣,但一看到“大模型”、“智能体”、“工作流”这些词就头疼,觉得门槛太高,那这篇文章就是为你准备的。很多开发者,包括我自己&#…

2026/7/21 18:53:30阅读更多 →
AI生图工具怎么选?2026年6月版实测对比

AI生图工具怎么选?2026年6月版实测对比

做自媒体的朋友应该都有体会:配图一直是个让人头疼的问题。2026年,AI生图工具已经非常成熟了,但工具太多反而不知道怎么选。以下是截至2026年6月我对主流AI生图工具的实测对比。Midjourney V8.1:速度之王2026年6月11日&#xff0c…

2026/7/21 18:53:30阅读更多 →