DDD领域驱动的了解
代码思维:
-
明确的界限上下文(通过接口、事件、消息隔离)
-
聚合根与实体的行为内聚
-
值对象的不变性
-
使用领域服务表达跨实体的业务逻辑
-
使用防腐层与外部系统集成
具体解释
概念 | 说明 |
---|---|
领域(Domain) | 指你要解决的问题空间,比如“订单管理”、“设备管理”、“任务调度”就是不同的领域。 |
领域模型(Domain Model) | 是对领域的抽象建模,体现业务概念、规则、状态和行为。 |
实体(Entity) | 是领域模型中的一种对象,具有唯一标识(Id)和生命周期,状态可变。 |
值对象(Value Object) | 无唯一标识、不可变、用来描述属性的一种建模手段,如:地址、坐标。 |
聚合(Aggregate) | 是由实体和值对象组成的集合,统一作为一个一致性边界进行操作,一起保证业务的一致性 |
聚合根(Aggregate Root) |
是聚合中的主实体,外部只能通过它访问整个聚合。负责维护聚合内的一致性 |
领域服务(Domain Service) | 当某些业务行为不适合放在实体或值对象中时,就用服务来承载。领域内跨多个实体的操作逻辑,不适合放在单一实体上 |
应用服务 | 应用层的逻辑编排器,调用领域模型、发事件、出来外部服务等 |
我理解的 DDD 分层架构,先将系统划分为不同的限界上下文,比如设备管理和任务调度,每个上下文有自己的应用服务层,负责编排业务流程,再往下是领域服务层,封装领域内的业务逻辑,领域层核心是聚合根实体和值对象,它们承载状态和行为。基础设施层负责数据持久化、外部接口和事件机制,保证领域模型与技术细节解耦。
小试牛刀
实体(Entity)
定义:
具有唯一标识(ID)、生命周期,与其属性无关的对象。
关键特征:
-
有唯一标识符(如 Id、Mac、Sn)
-
身份不变,即使属性变了,仍然是同一个实体
-
会被持续修改和追踪
例子:
- 用户(User):即使手机号、名字、地址都改了,用户的 ID 没变,还是同一个人
- 订单(Order):订单状态、内容可能变,但订单号唯一
值对象(Value Object)
定义:
不需要唯一标识、根据属性值来定义等价性的对象。
关键特征:
-
不可变(immutable),修改 = 创建新值对象
-
无唯一标识,两个值对象属性值一样就是同一个
-
用于建模“描述”而不是“谁”
例子:
- 地址(Address):省、市、区、街道都一样,就是同一个地址
- 金额(Money):货币+数值一样,就是同一个金额
public class Address { public string City { get; } public string Street { get; } public Address(string city, string street) { City = city; Street = street; } public override bool Equals(object obj) => obj is Address other && City == other.City && Street == other.Street; public override int GetHashCode() => HashCode.Combine(City, Street); }
3. 聚合(Aggregate)
定义:
一个或多个实体和值对象组成的一致性边界,是业务操作的最小单元。
关键特征:
-
聚合是一个整体,只通过聚合根访问
-
内部数据之间具有强一致性(例如一起修改、一起验证)
例子:
- 一个订单(聚合)中包含:
- 订单实体(Order) ✅ 聚合根
- 多个订单项(OrderItem)实体
- 收货地址(Address)值对象
聚合根(Aggregate Root)
定义:
聚合中负责对外暴露行为和数据的唯一入口点。
关键特征:
-
所有外部只能通过聚合根访问聚合内部数据
-
聚合根控制其内部成员的生命周期与业务逻辑
例子:
订单(Order)是聚合根,不能直接操作订单项(OrderItem),必须通过 Order 来添加、删除项
public class Order { public Guid Id { get; private set; } private List<OrderItem> _items = new(); public Address ShippingAddress { get; private set; } public void AddItem(Product product, int quantity) { _items.Add(new OrderItem(product.Id, quantity, product.Price)); } }
重点理解
再明确一下「值对象」的本质:
值对象强调的是:
“值”本身很重要,而不是“这个东西是谁”。
-
它是描述性的
-
通常是小而精的模型
-
不追踪生命周期、不可变
值对象常见的使用场景(含代码例子)
1. 金额 Money(金额 + 货币)
public class Money { public decimal Amount { get; } public string Currency { get; } public Money(decimal amount, string currency) { Amount = amount; Currency = currency; } public Money Add(Money other) { if (Currency != other.Currency) throw new InvalidOperationException("币种不一致"); return new Money(Amount + other.Amount, Currency); } }
-
可以封装业务逻辑:不同币种不能相加
-
可以在多个地方复用:商品价格、订单总价、发票金额
-
不需要 ID,我们只关注值
3.手机号、邮箱、身份证号等(值验证 + 不可变)
public class Email { public string Value { get; } public Email(string value) { if (!Regex.IsMatch(value, @"^\S+@\S+\.\S+$")) throw new ArgumentException("邮箱格式错误"); Value = value; } }
-
这种值对象可以封装格式验证逻辑
-
保证数据一进入领域层就是干净的、有效的
-
比用
string Email
强得多(更严谨、更具语义)
什么时候选“值对象”而不是“实体”
场景 | 选择 | 原因说明 |
---|---|---|
用户的“收货地址” | ✅ 值对象 | 不需单独身份,属性一致即相同 |
“订单项”中的每一项(商品+数量) | ❌ 实体 | 每一项可能有唯一标识(商品Id) |
商品的“价格” | ✅ 值对象 | 封装货币、金额和业务逻辑 |
“用户”对象 | ❌ 实体 | 每个用户都有唯一 ID |
“验证码” | ✅ 值对象 | 短暂、无身份、只要值正确即可 |
总结一句话理解:值对象
值对象建模的本质是,把“没有身份的业务含义”封装成语义清晰、行为安全的类。
重新理解「值对象」= 强类型的“描述信息 + 行为”
值对象是一种“语义明确 、 不可变 、 无需身份”的小型业务建模单位,它让你的代码表达更严谨、更安全、更清晰。它提升了 业务语义表达力、数据合法性校验、代码可维护性
在实体中,建模那些重要但无唯一标识的业务概念,为关键属性提供强语义封装、业务规则封装、约束校验和不可变性控制。它通常用于:1.格式校验(如 Email、手机号)2.取值范围验证(如百分比、金额)
聚合 & 聚合根的直观理解(先说结论)
-
聚合(Aggregate):一组紧密相关的实体和值对象,组成一个业务一致性边界,被看作一个整体。
-
聚合根(Aggregate Root):这组整体的“代表”或“入口”,外部系统只能通过它来访问聚合内部。
类比帮助理解(现实场景类比)
角色 | 类比 | 含义说明 |
---|---|---|
聚合(Aggregate) | 一个订单 + 所有订单项 | 是一个整体,要保证内部业务规则一致性 |
聚合根(AR) | 订单(Order) | 是“代表”,所有修改只能通过它发起 |
聚合成员 | 订单项(OrderItem) | 是内部子对象,不能被外部直接操作 |
聚合与聚合根的特点详解
聚合(Aggregate)特点
-
聚合是强一致性边界(例如修改订单和订单项,要一次完成)
-
聚合内部可以包含多个实体和值对象
-
聚合是业务建模的“最小事务单位”,一个聚合内的更新,应该是原子的
聚合根(Aggregate Root)职责
-
聚合根是聚合的唯一外部访问入口
-
负责协调聚合内部所有成员的行为
-
控制其下属对象的创建、修改、删除
-
聚合外部 只能通过聚合根引用内部对象
实际开发中的聚合结构(C# 示例)
以订单系统为例:
聚合根:Order(订单)public class Order { public Guid Id { get; private set; } public DateTime CreatedTime { get; private set; } private List<OrderItem> _items = new(); public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly(); public Address ShippingAddress { get; private set; } public void AddItem(Guid productId, int quantity, decimal price) { var item = new OrderItem(productId, quantity, price); _items.Add(item); } public decimal GetTotalPrice() => _items.Sum(x => x.GetTotal()); }
聚合内部实体:OrderItem(订单项)
public class OrderItem { public Guid ProductId { get; private set; } public int Quantity { get; private set; } public decimal UnitPrice { get; private set; } public OrderItem(Guid productId, int quantity, decimal price) { ProductId = productId; Quantity = quantity; UnitPrice = price; } public decimal GetTotal() => Quantity * UnitPrice; }
-
外部不能直接操作
OrderItem
,只能通过Order.AddItem()
。 -
外部也不允许直接修改
ShippingAddress
,而是应该有ChangeAddress()
这种方法。
为什么要有「聚合」这个概念?
▶ 背后的目标:控制数据一致性范围DDD 强调:
我们要建模的是真实业务,不是数据库表。
举例:
-
不要因为数据库有 Order 和 OrderItem 两张表,就在业务层随意增删查改。
-
应该由 Order(聚合根)统一控制其内部的状态,防止外部“跳过业务逻辑”破坏一致性。
聚合设计的实践经验(重要!)
-
聚合最好不要过大,否则容易出现性能瓶颈或并发冲突。
-
一个事务只操作一个聚合(例如:订单聚合、商品聚合分开)。
-
聚合根的 ID 通常是主键,方便跨服务引用。
-
不允许外部直接持有聚合内部对象的引用。
总结一句话:
聚合是领域内的一致性单元,聚合根是聚合的守门人,负责所有业务行为的入口和规则校验。
值对象 聚合根 负责的职责
值对象(Value Object)职责
编号 | 职责 | 说明 |
---|---|---|
1️⃣ | 校验 | 表达业务字段的有效性约束(如邮箱格式、手机号长度) |
2️⃣ | 约束 | 限定属性值在某些规则内(如金额为正,数量不能为负) |
3️⃣ | 不可变性 | 一旦创建就不能修改,所有属性为 readonly 或构造函数传入 |
4️⃣ | 无唯一标识 | 判断是否相等靠“值”,不依赖 Id |
5️⃣ | 可复用/组合 | 可以嵌入多个实体中,形成高复用组件(如 Email、Address) |
聚合根(Aggregate Root)职责
编号 | 职责 | 说明 |
---|---|---|
1️⃣ | 业务行为封装 | 处理核心业务逻辑(如订单确认、支付等) |
2️⃣ | 管理聚合内实体和值对象 | 是子对象(子实体、值对象)的“管理者” |
3️⃣ | 跨子对象规则协调 | 子对象间有协作/规则冲突时,由聚合根协调 |
4️⃣ | 状态变更操作 | 聚合根对状态的修改必须由它自身发起 |
5️⃣ | 聚合边界内一致性控制 | 保证聚合内部数据的一致性(如商品库存不能小于零) |
6️⃣ | 是唯一入口点 | 外部只通过聚合根访问/操作内部对象 |
7️⃣ | 控制事务边界(事务最小单元) | 一个事务最多只操作一个聚合,防止分布式事务 |
8️⃣ | 持久化只持根 | ORM(如 EF、FreeSql)只持久化聚合根 |
9️⃣ | 避免过大聚合 | 聚合尽量小而完整,防止聚合过大导致性能问题 |
🔟 | ID 是聚合标识符 | 通常就是数据库主键,是聚合根的唯一标识 |
DDD中的几大模型
1. 贫血模型(Anemic Domain Model)
实体类只有属性(字段),没有行为(业务逻辑)
所有业务逻辑写在Service 中
像什么?
public class Order { public Guid Id { get; set; } public List<OrderItem> Items { get; set; } public decimal TotalPrice { get; set; } }
public class OrderService { public void AddItem(Order order, Product product, int quantity) { var item = new OrderItem(product.Id, quantity, product.Price); order.Items.Add(item); } }
优点:
结构清晰
和数据库结构一一对应,开发快
缺点:
违背 OOP 封装原则
领域行为散落在多个 Service 中,难维护
逻辑失去了上下文保护,容易出 bug
⚠️ 很多系统号称 DDD,实则还是贫血模型(失血 DDD)
2. 失血模型(Bloodless DDD):伪 DDD
这是贫血模型的“升级版谎言”。
特征:
-
模型结构上照搬 DDD(有实体、有聚合、有仓储)
-
但领域行为全部放在 ApplicationService 中或直接写在 Controller
-
看似用 DDD 架构,其实是过程式事务脚本
public class OrderApplicationService
{
public void PlaceOrder(Guid userId, List<ItemDto> items)
{
// 直接操作 entity,没有封装行为
var order = new Order();
foreach (var item in items)
{
order.Items.Add(new OrderItem(item.ProductId, item.Quantity, item.Price));
}
}
}
问题:
-
把“设计”当成“形状”,没有落地 DDD 精神
-
失去了封装性和领域建模的好处
3. 胀血模型(Bloated Model)(也叫“肥胖模型”)
特征:
-
所有逻辑、依赖、工具都写进一个实体类或聚合根里
-
聚合根变成“大胖子”,动辄几百上千行
例子
public class Order
{
// 除了订单逻辑,还处理发票、发送消息、日志、邮件...
}
问题:
-
职责不清晰,耦合严重
-
无法测试、无法维护
4. 事务脚本模型(Transaction Script)
不是 DDD 模型,但常被拿来对比。
特点:
-
所有业务流程通过一个个 Service 方法(函数)来表达
-
每个方法处理一个业务流程的开始到结束
适合:
-
逻辑简单的小系统
-
不需要复杂建模
5. 充血模型(Rich Domain Model)
核心特征:
-
实体类中既有属性,也包含自身的业务逻辑
-
领域行为被封装在聚合根中
像什么?
public class Order
{
public Guid Id { get; private set; }
private List<OrderItem> _items = new();
public void AddItem(Product product, int quantity)
{
if (quantity <= 0) throw new Exception("数量必须大于0");
_items.Add(new OrderItem(product.Id, quantity, product.Price));
}
}
优点:
-
符合 OOP 封装原则
-
高内聚、职责明确
-
可读性强、方便测试、业务语义清晰
缺点:
-
学习曲线高,需要良好设计能力
-
对架构、团队习惯要求高
✅ DDD 推荐使用这种充血模型
模型类型 | 特点说明 | 常见问题 | 是否符合 DDD |
---|---|---|---|
贫血模型 | 实体无行为,Service 全管业务 | 逻辑散、可维护性差 | ❌ |
充血模型 | 实体封装行为,行为与数据一致 | 架构复杂,学习曲线高 | ✅ |
失血模型 | 看起来像 DDD,但行为全跑外面 | 骗自己在用 DDD | ❌ |
胀血模型 | 聚合根变胖,职责乱、耦合重 | 无法测试、扩展困难 | ❌ |
事务脚本模型 | 流程清晰、开发快(尤其在传统业务系统中) | 缺乏抽象、可复用性差 | ❌(非 DDD) |
DDD不是反对增删改查,而是强调:当增删改查“背后含有业务含有和规则”时,这些操作应该在 实体/聚合根 中体现处理,而不是藏在Service 或 Controller 里