领域
在 DDD 中,“领域(Domain)” 指的是软件要解决的 “业务范围” 及其包含的所有业务概念、规则和逻辑。
简单来说:
- 如果你开发的是 “电商系统”,那么 “电商” 就是核心领域,包含 “商品、订单、支付、物流” 等子业务范围;
- 如果你开发的是 “医院挂号系统”,那么 “医疗挂号” 就是核心领域,包含 “患者、医生、排班、挂号单” 等子业务范围。
领域的本质是 “业务的边界和内容”,DDD 的所有设计都围绕 “如何精准映射领域” 展开。
领域的层级划分:从宏观到微观
层级 | 定义 | 示例(电商领域) |
---|---|---|
核心领域 | 业务的核心价值所在,是企业竞争力的关键,需要投入最多资源设计。 | 订单履约(下单、库存扣减、支付联动) |
支撑领域 | 为核心领域提供支持,但不直接产生核心价值,可复用或简化设计。 | 商品管理(商品上架、分类、定价) |
通用领域 | 跨领域复用的基础能力,与具体业务关联弱,可通过开源组件或通用服务实现。 | 用户认证、日志记录、短信通知 |
领域模型(对领域的抽象)
1、对于领域内的对象进行建模,从而抽象出来模型。以银行为例。
2、我们的项目应该开始于创建领域模型,而不是考虑如何设计数据库和编写代码。使用领域模型,我们可以一直用业务语言去描述和构建系统,而不是使用技术人员的语言。
通用语言和界限上下文
通用语言
1、“我想要商品可以被删除”→“我想要把删除的还原回来”→"Windows回收站都能
2、此"用户"非彼"用户
3、通用语言:一个拥有确切含义的、没有二义性的语言。
界限上下文
通用语言离不开特定的语义环境,只有确定了通用语言所在的边界,才能没有歧义的描述一个业务对象。
总结
- 通用语言是 “内容”,解决 “说什么” 的问题,确保团队对业务的理解一致;
- 限界上下文是 “容器”,解决 “在哪里说” 的问题,确保语言在合适的范围内有效;
- 二者结合,既保证了每个子领域的内聚性(语言统一),又控制了整体系统的复杂度(边界清晰),是 DDD 解决复杂业务问题的核心方法论。
实体和值对象
实体(Entity)
核心特征
- 有唯一标识符(ID),通过 ID 区分不同实例(即使属性完全相同,ID 不同就是不同实体);
- 状态可随业务流程变化(具有生命周期);
- 封装自身的业务行为和状态变更规则。
适用场景
代表业务中 “有身份、可变化” 的事物,如订单、用户、商品等。
// 订单ID(使用值对象封装ID,更具业务含义)
public record OrderId(Guid Value);// 订单状态(枚举表示状态变更)
public enum OrderStatus { Pending, Paid, Shipped, Completed, Cancelled }// 订单项(实体,属于订单聚合)
public class OrderItem
{public Guid ProductId { get; }public string ProductName { get; }public decimal UnitPrice { get; }public int Quantity { get; }public decimal TotalPrice => UnitPrice * Quantity;public OrderItem(Guid productId, string productName, decimal unitPrice, int quantity){if (quantity <= 0) throw new ArgumentException("数量必须大于0");if (unitPrice < 0) throw new ArgumentException("单价不能为负数");ProductId = productId;ProductName = productName;UnitPrice = unitPrice;Quantity = quantity;}
}// 订单实体(核心实体)
public class Order
{// 唯一标识(实体的核心特征)public OrderId Id { get; }// 状态(可变化的属性)public OrderStatus Status { get; private set; }// 订单项集合public IReadOnlyList<OrderItem> Items => _items.AsReadOnly();private readonly List<OrderItem> _items = new();// 下单时间(创建后不变)public DateTime CreatedTime { get; }// 支付时间(状态变更时赋值)public DateTime? PaidTime { get; private set; }// 构造函数(初始化实体,设置初始状态)public Order(OrderId id){Id = id;Status = OrderStatus.Pending;CreatedTime = DateTime.UtcNow;}// 业务行为:添加订单项(封装规则)public void AddItem(OrderItem item){if (Status != OrderStatus.Pending)throw new InvalidOperationException("只有待支付订单可以添加商品");_items.Add(item);}// 业务行为:支付订单(状态变更规则)public void Pay(){if (Status != OrderStatus.Pending)throw new InvalidOperationException("只有待支付订单可以支付");if (!Items.Any())throw new InvalidOperationException("订单没有商品,无法支付");Status = OrderStatus.Paid;PaidTime = DateTime.UtcNow;}// 业务行为:取消订单(状态变更规则)public void Cancel(){if (Status is OrderStatus.Shipped or OrderStatus.Completed)throw new InvalidOperationException("已发货或已完成的订单不能取消");Status = OrderStatus.Cancelled;}
}
实体设计要点
- ID 不可变:通常在构造函数中初始化,不提供修改 ID 的方法;
- 行为优先:将状态变更逻辑封装为方法(如
Pay()
、Cancel()
),而非直接暴露setter
; - 相等性判断:重写
Equals
时以 ID 为依据(示例中OrderId
用record
类型自动实现值相等)。
值对象(Value Object)
核心特征
- 无唯一标识符,通过属性值判断相等性;
- 不可变(创建后属性不能修改);
- 描述事物的 “属性或特征”,而非事物本身。
适用场景
代表 “属性组合” 或 “无身份的概念”,如地址、金额、颜色等。
C# 示例:地址和金额值对象
// 地址值对象(描述位置属性)
public class Address : IEquatable<Address>
{// 不可变属性(无setter)public string Province { get; }public string City { get; }public string Street { get; }public string ZipCode { get; }// 构造函数初始化所有属性,并验证有效性public Address(string province, string city, string street, string zipCode){Province = string.IsNullOrWhiteSpace(province) ? throw new ArgumentException("省份不能为空") : province;City = string.IsNullOrWhiteSpace(city) ? throw new ArgumentException("城市不能为空") : city;Street = string.IsNullOrWhiteSpace(street) ? throw new ArgumentException("街道不能为空") : street;ZipCode = string.IsNullOrWhiteSpace(zipCode) ? throw new ArgumentException("邮编不能为空") : zipCode;}// 重写相等性判断(基于所有属性值)public bool Equals(Address? other){if (other is null) return false;return Province == other.Province && City == other.City && Street == other.Street && ZipCode == other.ZipCode;}public override bool Equals(object? obj) => Equals(obj as Address);public override int GetHashCode() => HashCode.Combine(Province, City, Street, ZipCode);// 提供便捷的解构方法public void Deconstruct(out string province, out string city, out string street, out string zipCode){province = Province;city = City;street = Street;zipCode = ZipCode;}
}// 金额值对象(描述数值+币种)
public record Money(decimal Amount, string Currency)
{// 静态验证逻辑(确保创建的值对象有效)public static Money Create(decimal amount, string currency){if (amount < 0) throw new ArgumentException("金额不能为负数");if (string.IsNullOrWhiteSpace(currency)) throw new ArgumentException("币种不能为空");return new Money(amount, currency);}// 提供领域行为(如金额相加,需确保币种一致)public Money Add(Money other){if (Currency != other.Currency)throw new InvalidOperationException("不同币种不能相加");return new Money(Amount + other.Amount, Currency);}
}
值对象设计要点
- 不可变性:所有属性设为
get-only
,通过构造函数一次性初始化; - 值相等性:重写
Equals
和GetHashCode
,基于所有属性值判断相等; - 自我验证:在构造函数中验证属性有效性(如金额不能为负);
- 行为封装:包含与自身属性相关的逻辑(如
Money.Add()
确保币种一致)。
实体与值对象的核心区别
维度 | 实体(Entity) | 值对象(Value Object) |
---|---|---|
标识 | 有唯一 ID(身份标识) | 无 ID(通过属性值标识) |
可变性 | 状态可随业务变化 | 不可变(创建后无法修改) |
相等性 | 基于 ID 判断相等 | 基于属性值判断相等 |
生命周期 | 有明确的创建、修改、删除过程 | 通常随所属实体创建 / 销毁 |
示例 | 订单、用户、商品 | 地址、金额、坐标 |
实践建议
- 优先设计值对象:将 “属性组合” 封装为值对象(如
Address
),可减少实体复杂度; - 实体依赖值对象:实体通常包含多个值对象(如订单包含收货地址
Address
); - 用
record
简化值对象:C# 9+ 的record
类型自动实现值相等性,适合快速定义简单值对象(如Money
)。
通过合理区分实体和值对象,能让领域模型更贴合业务本质,同时提升代码的可读性和可维护性。
聚合和聚合根
聚合 (Aggregate) 是一组相关联的领域对象的集合,它们被视为一个整体来处理。聚合根 (Aggregate Root) 是聚合中作为访问入口点的对象,它负责维护聚合的完整性和一致性。
1、目的:高内聚,低耦合。有关系的实体紧密协作,而关系很弱的实体被隔离
2、把关系紧密的实体放到一个聚合中,每个聚合中有实体作为聚合根
(Aggregate Root),所有对于聚合内对象的访问都通过聚合根来进行,外部对象只能持有对聚合根的引用
3、聚合根不仅仅是实体,还是所在聚合的管理者。
购物车例子:
using System;
using System.Collections.Generic;
using System.Linq;// 聚合根:购物车(实体)
public class Cart
{private readonly List<CartItem> _items = new List<CartItem>();public Guid Id { get; }public string UserId { get; }public IReadOnlyCollection<CartItem> Items => _items.AsReadOnly();// 其他属性和方法与之前相同...private Cart(Guid id, string userId){Id = id;UserId = userId ?? throw new ArgumentNullException(nameof(userId));}public static Cart Create(string userId){return new Cart(Guid.NewGuid(), userId);}// 添加带产品信息值对象的方法public void AddItem(ProductInfo product, int quantity){if (quantity <= 0)throw new ArgumentException("数量必须大于0", nameof(quantity));var existingItem = _items.FirstOrDefault(i => i.Product.ProductId == product.ProductId);if (existingItem != null){existingItem.UpdateQuantity(existingItem.Quantity + quantity);}else{_items.Add(new CartItem(product, quantity));}}
}// 购物项(实体)
public class CartItem
{// 产品信息(值对象)public ProductInfo Product { get; }public int Quantity { get; private set; }public decimal TotalPrice => Product.UnitPrice * Quantity;public CartItem(ProductInfo product, int quantity){Product = product ?? throw new ArgumentNullException(nameof(product));Quantity = quantity > 0 ? quantity : throw new ArgumentException("数量必须大于0", nameof(quantity));}internal void UpdateQuantity(int newQuantity){if (newQuantity <= 0)throw new ArgumentException("数量必须大于0", nameof(newQuantity));Quantity = newQuantity;}
}// 产品信息(值对象)
public class ProductInfo
{public string ProductId { get; }public string Name { get; }public decimal UnitPrice { get; }public string Description { get; }// 值对象通常是不可变的(构造后不能修改)public ProductInfo(string productId, string name, decimal unitPrice, string description){ProductId = productId ?? throw new ArgumentNullException(nameof(productId));Name = name ?? throw new ArgumentNullException(nameof(name));UnitPrice = unitPrice >= 0 ? unitPrice : throw new ArgumentException("单价不能为负数", nameof(unitPrice));Description = description ?? string.Empty;}// 值对象需要重写相等性检查public override bool Equals(object obj){return obj is ProductInfo info &&ProductId == info.ProductId &&Name == info.Name &&UnitPrice == info.UnitPrice &&Description == info.Description;}public override int GetHashCode(){return HashCode.Combine(ProductId, Name, UnitPrice, Description);}
}
- 聚合根 (Cart) 是整个聚合的入口点,外部只能通过它来操作购物车中的商品
- 聚合规则:
- 购物项 (CartItem) 不能独立存在,必须属于某个购物车
- 所有对购物项的操作(添加、修改、删除)都必须通过购物车进行
- 购物车确保了业务规则(如数量不能为负)
- 仓储设计:只存在 ICartRepository,没有 ICartItemRepository,因为购物项不能独立持久化
这种设计保证了购物车数据的一致性,例如不会出现数量为负的购物项,也不会出现不属于任何购物车的孤立购物项。
在上面的购物车 (Cart) 聚合中,我们也可以清晰地区分实体 (Entity) 和值对象 (Value Object):
-
实体 (Entity):具有唯一标识,其身份不依赖于属性,即使属性变化,实体身份依然保持不变
- Cart(购物车):是实体,也是聚合根
- 有唯一标识
Id
- 即使购物车中的商品(属性)发生变化,购物车本身的身份依然不变
- 生命周期独立,有自己的创建、修改、删除等状态变化
- 有唯一标识
- CartItem(购物项):是实体
- 虽然没有显式定义
Id
,但通过ProductId
在购物车范围内具有唯一标识 - 它的身份由 "属于哪个购物车" 和 "哪个商品" 共同确定
- 可以独立修改(如更新数量)而保持身份不变
- 虽然没有显式定义
- Cart(购物车):是实体,也是聚合根
-
值对象 (Value Object):没有唯一标识,其身份由属性值共同决定,属性相同则视为相同对象
在我们的示例中没有显式定义值对象,但可以扩展一个来更好地理解:
聚合,聚合根,实体,值对象的关系,
聚合(Aggregate)
├─ 聚合根(Aggregate Root) → 实体(Entity)
│ ├─ 其他实体(Entity)
│ └─ 值对象(Value Object)
└─ 其他实体(Entity)└─ 值对象(Value Object)
- 实体 (Entity) 与值对象 (Value Object):领域模型的基础元素
- 两者都是领域中的具体概念,是构成聚合的基本单元
- 实体:有唯一标识,身份优先于属性(例如:用户、订单)
- 值对象:无唯一标识,属性组合决定其身份,且不可变(例如:地址、金额、颜色)
- 关系:值对象可以作为实体的属性存在;实体可以包含多个值对象
- 聚合 (Aggregate):实体和值对象的有机组合
- 聚合是一组紧密相关的实体和值对象的集合,被视为一个不可分割的整体
- 目的:保证领域模型的一致性,避免因分散修改导致的数据不一致
- 规则:聚合内部的对象可以相互引用,但外部对象只能通过聚合根访问聚合内部成员
- 聚合根 (Aggregate Root):聚合的 "大门"
- 聚合根是聚合中唯一对外暴露的实体,是外部访问聚合的唯一入口
- 职责:
- 维护聚合内部的业务规则和数据一致性
- 负责聚合内部对象的创建和管理
- 作为与外部对象交互的 "代理人"
- 特征:
- 必须是实体(有唯一标识)
- 聚合中只能有一个聚合根
- 仓储 (Repository) 只针对聚合根设计
购物车领域模型关系图
购物车聚合(CartAggregate)
├─ 聚合根:购物车(Cart) → 实体
│ ├─ 购物项(CartItem) → 实体
│ │ ├─ 产品信息(ProductInfo) → 值对象
│ │ └─ 数量(Quantity) → 实体属性
│ └─ 用户ID(UserId) → 实体属性
└─ 优惠信息(Discount) → 值对象
各概念在购物车场景中的具体体现
- 值对象 (Value Object):关注 "是什么",无唯一标识,不可变
- ProductInfo(产品信息)
- 包含属性:产品 ID、名称、单价、描述、图片 URL
- 特点:属性完全相同则视为同一个对象(比如 "iPhone 15 128G 黑色")
- 不可变:创建后不能修改(商品信息变更应创建新的 ProductInfo)
- Discount(优惠信息)
- 包含属性:优惠码、折扣率、有效期
- 特点:通过属性组合确定其身份(相同优惠码和折扣率视为同一优惠)
- ProductInfo(产品信息)
- 实体 (Entity):关注 "是谁",有唯一标识,可修改
- CartItem(购物项)
- 标识:通过 "所属购物车 + 关联产品 ID" 确定唯一身份
- 可修改属性:数量(用户可以增减购买数量)
- 依赖关系:必须属于某个购物车,不能独立存在
- Cart(购物车)
- 标识:有全局唯一 ID(CartId)
- 可修改属性:包含的购物项集合(添加 / 删除 / 修改购物项)
- 生命周期:有明确的创建(用户打开购物车)、修改(添加商品)、删除(清空或过期)过程
- CartItem(购物项)
- 聚合根 (Aggregate Root):购物车 (Cart)
- 是购物车聚合的 "入口" 和 "管理者",外部只能通过它访问聚合内的对象
- 负责维护聚合的一致性:
- 确保购物项数量不能为负(通过
AddItem()
方法校验) - 防止添加不存在的商品(通过 ProductInfo 验证)
- 计算购物车总金额(汇总所有购物项的价格)
- 确保购物项数量不能为负(通过
- 对外暴露操作接口:
AddItem()
、RemoveItem()
、UpdateQuantity()
等
- 聚合 (Aggregate):购物车聚合
- 包含:购物车 (Cart)、购物项 (CartItem)、ProductInfo、Discount
- 边界规则:
- 外部不能直接修改 CartItem,必须通过 Cart 的方法
- 保存购物车时,整个聚合作为一个整体持久化
- 删除购物车时,所有包含的购物项也会被一同删除
- 一致性保证:添加商品时自动计算总金额,应用优惠时重新计算价格
总结购物车场景中的关系
- 值对象是 "静态属性":如商品信息、优惠规则,它们描述事物的特征
- 实体是 "动态对象":如购物车和购物项,它们有身份和生命周期
- 聚合根是 "管理者":购物车控制所有内部操作,确保数据一致
- 聚合是 "完整单元":将相关对象打包,作为一个整体处理(如保存、删除)
这种设计确保了购物车数据的完整性(比如不会出现数量为负的商品),同时符合用户操作购物车的实际业务场景。
领域服务和应用服务
1、聚合中的实体中没有业务逻辑代码,只有对象的创建、对象的初始化、状态管理等个体相关的代码
2、对于聚合内
的业务逻辑,我们编写领域服务(DomainService),而对于跨聚协作
以及聚合与外部系统
协作的逻辑,我们编写应用服务(ApplicationService)
3、应用服务协调多个领域服务、外部系统来完成一个用例。
在购物车场景中,领域服务 (Domain Service) 和应用服务 (Application Service) 承担不同职责,共同完成业务流程。我们通过具体例子来理解它们的区别和协作方式:
核心概念区分
- 领域服务:封装领域内复杂的业务规则或跨实体 / 聚合的操作,确保领域逻辑的纯粹性
- 应用服务:协调领域对象完成用户用例,处理事务、权限等技术细节,不包含核心业务规则
关键区别与职责划分
- 领域服务 (CartDomainService)
- 包含核心业务规则:如库存检查、数量限制、折扣计算等聚合中的实体中没有业务逻辑代码,只有对象的创 建、对象的初始化、状态管理等个体相关的代码 2、对于聚合内的业务逻辑,我们编写领域服务 (DomainService),而对于跨聚合协作以及聚合与外 部系统协作的逻辑,我们编写应用服务(Application Service) 3、应用服务协调多个领域服务、外部系统来完成一个 用例。
- 直接操作领域对象(Cart、CartItem 等)
- 不依赖基础设施(仓储、日志等),不涉及数据库操作
- 方法参数通常是领域对象,而非原始类型
- 示例:
AddProductToCart
封装了添加商品的所有业务约束
- 应用服务 (CartApplicationService)
- 负责协调用户用例:如 "添加商品到购物车" 这个完整流程
- 处理跨领域的技术细节:依赖注入、事务管理、数据持久化
- 作为用户与领域模型之间的桥梁:接收 DTO / 原始类型参数,返回处理结果
- 不包含业务规则,而是调用领域服务完成核心逻辑
- 示例:
AddProductToCartAsync
协调了获取数据、调用领域服务、保存结果的完整流程
协作流程说明
当用户执行 "添加商品到购物车" 操作时:
- 应用服务接收用户输入(用户 ID、商品 ID、数量)
- 应用服务从仓储获取所需的领域对象(购物车、商品信息)
- 应用服务调用领域服务处理核心业务逻辑(检查库存、添加商品)
- 应用服务将修改后的领域对象保存到仓储
- 应用服务返回结果给用户
这种设计确保了业务规则集中在领域服务中,应用服务专注于流程协调,使系统更易于维护和扩展。
仓储(Repository)和工作单元(Unit of Work)
- 仓储(Repository):封装对数据的访问,为领域模型提供类似集合的接口,隔离领域层与数据访问层
- 工作单元(Unit of Work):跟踪领域对象的变更,协调多个仓储的操作,确保数据一致性(通常与事务相关),这些工作单元要么全部成功,要么全部失败。
领域事件(Domain Event) 和集成事件(Integration Event)
- 领域事件:发生在限界上下文内部的、对领域有意义的事件(如 “订单状态变更”“商品添加到购物车”),用于捕获领域行为的结果,实现领域内的解耦和业务响应。
- 集成事件:发生在限界上下文之间的事件(如 “订单已创建” 需要通知库存系统、支付系统),用于跨服务 / 上下文的通信,实现分布式系统的协同。
维度 | 领域事件(Domain Event) | 集成事件(Integration Event) |
---|---|---|
作用范围 | 限界上下文内部 | 跨限界上下文 / 服务 |
通信方式 | 进程内同步 / 异步(如内存事件总线) | 跨进程异步(如消息队列:RabbitMQ、Kafka) |
数据粒度 | 包含领域内详细信息(供内部处理) | 仅包含跨服务所需的最小信息(避免耦合) |
序列化需求 | 无需序列化(进程内传递对象) | 必须可序列化(跨服务传输) |
处理目的 | 实现领域内业务响应(如状态联动、日志) | 实现跨系统协同(如库存扣减、通知) |
示例 | 订单状态变更、购物车商品数量更新 | 订单创建、支付完成、库存不足 |