DDD领域驱动的了解

代码思维:

我理解的 DDD 是一种以业务为核心驱动系统设计的方法,它强调将复杂业务建模为领域模型,并通过界限上下文将复杂性进行隔离。
架构上,会划分为用户接口层、应用层、领域层、基础设施层。其中核心是领域层,它包含聚合根、实体、值对象、领域服务、领域事件等。
我会特别关注:
  1. 明确的界限上下文(通过接口、事件、消息隔离)

  2. 聚合根与实体的行为内聚

  3. 值对象的不变性

  4. 使用领域服务表达跨实体的业务逻辑

  5. 使用防腐层与外部系统集成

实际项目中,我曾在任务调度服务中引入 DDD,将领域逻辑与基础设施解耦,提升了系统的可维护性和扩展性。”
 
业务思维:
我理解 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(聚合根)统一控制其内部的状态,防止外部“跳过业务逻辑”破坏一致性。

聚合设计的实践经验(重要!)

  1. 聚合最好不要过大,否则容易出现性能瓶颈或并发冲突。

  2. 一个事务只操作一个聚合(例如:订单聚合、商品聚合分开)。

  3. 聚合根的 ID 通常是主键,方便跨服务引用。

  4. 不允许外部直接持有聚合内部对象的引用。

总结一句话:

聚合是领域内的一致性单元,聚合根是聚合的守门人,负责所有业务行为的入口和规则校验。

 

值对象 聚合根 负责的职责

值对象(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 里

posted on 2025-07-10 10:25  白码一号  阅读(51)  评论(0)    收藏  举报