DDD学习第3课设计聚合与聚合根

这节课非常关键,它决定后续所有的:

  • 事务边界
  • 状态流转
  • 业务规则封装位置
  • 领域事件触发点
  • Repository 设计方式
  • CQRS 与事件驱动的上下文切割

“聚合设计”是 DDD 战术建模中最难也是最重要的部分。


聚合设计原则总结

1. 聚合边界由强一致性决定  
2. 聚合之间不能相互持有实体引用  
3. 事务范围 = 聚合边界  
4. 修改聚合必须通过聚合根  
5. 不变量必须在聚合内部保持  

第3课:聚合与聚合根(Aggregate, Aggregate Root)

本节目标

明确聚合边界
学会判断 Entity / Value Object
理解事务一致性边界
让模型“不会不小心写错业务规则”
设计 Order 聚合的正确结构


聚合(Aggregate)

很多人以为“聚合”就是“一组对象”,但在 DDD 中有明确定义:

聚合是:领域中保持强一致性的边界。
聚合根是:唯一允许外部修改此聚合状态的入口。

因此,一个正确设计的聚合必须满足:

  1. 所有业务不变量(Invariants)必须在聚合内部保持成立
    例如:订单支付时总金额不能为 0
  2. 聚合的修改只能通过聚合根完成
  3. 聚合之间不能直接引用内部实体,只能引用 ID
  4. 事务范围必须落在单个聚合内,不允许跨聚合事务

聚合根(Aggregate Root)

  • 作为聚合的唯一入口
  • 外部不能直接修改聚合内部其他对象
  • 持久化、仓储都以聚合根为单位

示例:我们系统中的两个聚合

聚合名聚合根内容关系
OrderOrderOrderItem 列表、状态、总金额引用 MenuItem
MenuMenuMenuItem 列表提供菜品信息

从统一语言到聚合划分

我们从第1课的统一语言中提炼出候选概念:

  • Menu
  • MenuItem
  • Order
  • OrderItem
  • Money
  • Payment
  • Inventory
  • Delivery

接下来要判断:

  • 哪些是实体(Entity)?
  • 哪些是值对象(VO)?
  • 哪些应该属于同一个聚合?

Entity vs Value Object 判断规则

Value Object 的特点

  • 不依赖 ID
  • 不可变
  • 只关心属性值
  • 组合、安全、可替换

符合这些的有:

  • Money(金额)
  • MenuItem(菜单项:当前菜品的名字、价格快照)

补充说明
MenuItem 应该是 VO,因为它属于“价格快照”,不可随菜单变化而影响历史订单。
如果把 MenuItem 设计成 Entity,你的订单历史会发生“穿越”:价格被更新、变更名称等。


Entity 的特点

  • 有 ID
  • 生命周期内可变化
  • 有领域行为
  • 同 ID 即同对象

符合这些的有:

  • Order
  • OrderItem ← 它属于 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)协作,不允许实体对象跨聚合引用。

不允许这样写:

class Order {
    private MenuItem menuItem;  // ❌ 错:跨聚合引用
}

必须这样:

class Order {
    private MenuItemSnapshot snapshot;  // VO,记录下单时的名称、价格
}

事务一致性边界(决定聚合大小)

DDD 给了一条定律:

事务只保证单个聚合内部数据一致性。
不允许更新多个聚合的跨聚合事务。

例如:

  • “下单 + 扣库存”不能放在一个聚合事务里
  • “支付订单 + 发送券”不能放在一个聚合事务里

它们必须:

  • 通过领域事件
  • 或 Saga / 业务流程编排
  • 或补偿模式

否则会导致:

  • 锁竞争放大
  • 事务冲突严重
  • 系统无法扩展

设计订单聚合

下面是一个专业 DDD 项目中 Order 聚合的结构:

Order 聚合
├─ Order(聚合根)
│   ├─ id
│   ├─ status
│   ├─ totalAmount(Money)
│   ├─ items:List<OrderItem>
│   ├─ 行为(pay / confirm / complete)
│   └─ 不变量(金额>0、状态流转正确)
└─ OrderItem(实体)
    ├─ productName
    ├─ quantity
    ├─ price(Money)
    └─ subtotal(Money)

行为(业务规则)应该放在哪里?

DDD 规定:

领域行为必须放在聚合根 / 实体内部,而不是 Service。

例如:
订单支付:

public void pay() {
    if (status != CREATED) throw new IllegalStateException("...");

    this.status = PAID;
    raiseEvent(new OrderPaidEvent(id, totalAmount));
}

而不是:

orderService.pay(order);

应用服务应该像这样:

public void payOrder(OrderId id) {
    Order order = orderRepository.load(id);
    order.pay();            // ← 业务行为在领域对象中
    orderRepository.save(order);
}

聚合的“正确性”来自不变量(Invariants)

订单聚合必须保证:

  • 金额不能小于 0
  • OrderItem 的数量必须大于 0
  • OrderStatus 流转必须正确
  • 订单总金额应该是所有 OrderItem 小计之和
  • 已完成订单不能修改

这些规则一旦散落在多个层,系统将无法控制一致性。

DDD 的重要原则:

不变量必须由聚合根统一维护。


代码实现

项目结构新增一个聚合目录:

domain/
 ├── order/
 │    └── model/
 │         ├── Order.java
 │         ├── OrderItem.java
 │         ├── OrderStatus.java
 │         ├── OrderId.java
 │         └── ...
 └── menu/
      └── model/
           ├── Menu.java
           ├── MenuItem.java
           └── MenuId.java

Menu.java — 菜单聚合根

package com.example.ordersystem.domain.menu.model;

import java.util.ArrayList;
import java.util.List;

/**
 * 聚合根:Menu
 * 表示一个菜单集合(例如当天可售菜品)
 * 由 MenuItem 组成。
 */
public class Menu {

    private final MenuId id;
    private final List<MenuItem> items = new ArrayList<>();

    public Menu(MenuId id) {
        this.id = id;
    }

    /** 添加菜品到菜单中 */
    public void addItem(String name, double price) {
        items.add(new MenuItem(name, price));
    }

    /** 查找菜品 */
    public MenuItem findItem(String name) {
        return items.stream()
                .filter(item -> item.getName().equalsIgnoreCase(name))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("菜品不存在: " + name));
    }

    public List<MenuItem> getItems() {
        return items;
    }

    public MenuId getId() {
        return id;
    }
}

MenuItem.java — 值对象

package com.example.ordersystem.domain.menu.model;

/**
 * 值对象:MenuItem
 * 表示菜单中的单个菜品。
 * 不可变,没有标识符。
 */
public class MenuItem {
    private final String name;
    private final double price;

    public MenuItem(String name, double price) {
        if (price <= 0) throw new IllegalArgumentException("价格必须大于0");
        this.name = name;
        this.price = price;
    }

    public String getName() {
        return name;
    }

    public double getPrice() {
        return price;
    }
}

MenuId.java

package com.example.ordersystem.domain.menu.model;

import java.util.UUID;

/**
 * 值对象:MenuId
 * 用于唯一标识菜单。
 */
public class MenuId {
    private final String value;

    public MenuId(String value) {
        this.value = value;
    }

    public static MenuId newId() {
        return new MenuId(UUID.randomUUID().toString());
    }

    public String getValue() {
        return value;
    }
}

修改 Order.java — 订单聚合根关联 MenuItem

我们不直接存 MenuItem 对象,而是存储引用信息(例如菜名和价格),
保持聚合独立,防止跨聚合事务。

package com.example.ordersystem.domain.order.model;

import com.example.ordersystem.domain.menu.model.MenuItem;
import com.example.ordersystem.domain.shared.DomainException;
import java.util.ArrayList;
import java.util.List;

/**
 * 聚合根:Order
 * 管理订单内的所有状态变化和行为。
 */
public class Order {

    private final OrderId id;
    private final List<OrderItem> items = new ArrayList<>();
    private Money totalAmount = Money.zero();
    private OrderStatus status = OrderStatus.CREATED;

    public Order(OrderId id) {
        this.id = id;
    }

    /**
     * 从菜单项添加菜品(通过 MenuItem)
     * 说明:这里接收来自 Menu 聚合的“只读副本”,不跨聚合修改。
     */
    public void addMenuItem(MenuItem menuItem, int quantity) {
        if (status != OrderStatus.CREATED) {
            throw new DomainException("订单创建后才能添加菜品");
        }
        OrderItem item = new OrderItem(menuItem.getName(), quantity, new Money(menuItem.getPrice()));
        items.add(item);
        totalAmount = totalAmount.add(item.subtotal());
    }

    // 状态变更操作
    public void pay() {
        if (status != OrderStatus.CREATED) throw new DomainException("必须先创建才能支付");
        this.status = OrderStatus.PAID;
    }

    public void confirm() {
        if (status != OrderStatus.PAID) throw new DomainException("订单必须已支付才能确认");
        this.status = OrderStatus.CONFIRMED;
    }

    public void complete() {
        if (status != OrderStatus.CONFIRMED) throw new DomainException("订单未确认无法完成");
        this.status = OrderStatus.COMPLETED;
    }

    public void cancel() {
        if (status == OrderStatus.COMPLETED) throw new DomainException("已完成订单不能取消");
        this.status = OrderStatus.CANCELLED;
    }

    // Getter 方法
    public OrderId getId() { return id; }
    public List<OrderItem> getItems() { return items; }
    public Money getTotalAmount() { return totalAmount; }
    public OrderStatus getStatus() { return status; }
}

应用服务:OrderWithMenuService.java

这个服务会先构建菜单,再创建订单,展示聚合间的使用方式。

package com.example.ordersystem.application.order;

import com.example.ordersystem.domain.menu.model.Menu;
import com.example.ordersystem.domain.menu.model.MenuId;
import com.example.ordersystem.domain.order.model.Order;
import com.example.ordersystem.domain.order.model.OrderId;
import com.example.ordersystem.domain.order.repository.OrderRepository;
import org.springframework.stereotype.Service;

/**
 * 应用服务:演示 Order 聚合如何使用 Menu 聚合的数据。
 */
@Service
public class OrderWithMenuService {

    private final OrderRepository repository;

    public OrderWithMenuService(OrderRepository repository) {
        this.repository = repository;
    }

    /**
     * 创建菜单和订单(聚合之间通过数据引用,而非对象依赖)
     */
    public Order createOrderFromMenu() {
        //  创建菜单(独立聚合)
        Menu menu = new Menu(MenuId.newId());
        menu.addItem("Burger", 25.0);
        menu.addItem("Fries", 10.0);
        menu.addItem("Coke", 5.0);

        //  创建订单(独立聚合)
        Order order = new Order(OrderId.newId());
        order.addMenuItem(menu.findItem("Burger"), 1);
        order.addMenuItem(menu.findItem("Fries"), 2);

        //  保存订单
        repository.save(order);
        return order;
    }
}

控制层:MenuOrderController.java

package com.example.ordersystem.interfaces.api;

import com.example.ordersystem.application.order.OrderWithMenuService;
import com.example.ordersystem.domain.order.model.Order;
import org.springframework.web.bind.annotation.*;

/**
 * 控制层:演示从菜单创建订单
 */
@RestController
@RequestMapping("/menus")
public class MenuOrderController {

    private final OrderWithMenuService service;

    public MenuOrderController(OrderWithMenuService service) {
        this.service = service;
    }

    @GetMapping("/order")
    public Order createOrderFromMenu() {
        return service.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或值对象)交互,而不是直接调用方法
  • 每个聚合都独立持久化

内容概要:本文围绕微电网群的双层优化分布式优化问题,提出基于交替方向乘子法(ADMM)的分布式协同优化控制策略,并通过Matlab代码实现仿真验证。研究构建了上层为微电网间能量协调优化调度、下层为各微电网内部源-荷-储精细化运行管理的双层优化模型。采用ADMM算法将集中式优化问题分解为多个可并行求解的子问题,实现了计算的分布式化信息隐私保护,显著提升了系统的可扩展性鲁棒性。文中系统阐述了模型构建原理、ADMM算法设计流程及其收敛性分析,并通过仿真实验验证了该方法在降低系统综合运行成本、提高可再生能源消纳能力以及维持系统稳定运行方面的有效性。; 适合人群:具备电力系统分析、优化理论基础,熟悉Matlab编程,从事微电网、分布式能源系统、智能电网等领域研究的研究生、科研人员及工程技术人员。; 使用场景及目标:① 学习和掌握基于ADMM的分布式优化方法在微电网群协同能量管理中的具体应用;② 实现微电网群多主体参下的经济调度仿真分析;③ 深入理解双层优化架构的设计理念分布式求解算法的实现机制。; 阅读建议:建议结合所提供的Matlab代码进行动手实践,重点剖析模型建立算法实现的关键细节,可通过调整系统参数、改变运行场景等方式,深入探究ADMM算法的收敛特性及其对优化效果的影响。
内容概要:本文聚焦于风电出力不确定性的精确建模问题,提出采用拉丁超立方抽样(LHS)方法生成具有统计代表性的风电场景,并结合先进的场景缩减技术以降低计算复杂度。通过Matlab编程实现了LHS在高维随机变量空间中的均匀采样,有效克服了传统蒙特卡洛方法样本收敛慢、效率低的问题。在此基础上,引入基于聚类分析或概率距离度量的场景缩减算法,对初始大规模场景集进行优化合并,保留关键概率特征出力趋势,构建出精简且具代表性的典型场景集。该方法显著提升了电力系统随机优化模型(如机组组合、储能调度、微电网能量管理)的求解效率数值稳定性,同时确保输入场景的合理性真实性,具备良好的可复现性工程应用价值。; 适合人群:适用于具备一定电力系统分析基础和Matlab编程能力,从事新能源并网、随机规划、场景生成、电力市场及综合能源系统优化等方向研究的研究生、科研人员及工程技术人员。; 使用场景及目标:①掌握拉丁超立方抽样在风电不确定性建模中的理论原理Matlab实现技巧;②学习并应用场景生成缩减技术,提升随机优化问题的建模求解效率;③为含高比例风电的电力系统调度、风险评估决策分析提供高质量的输入场景支撑。; 阅读建议:建议读者结合所提供的Matlab代码逐模块调试运行,深入理解抽样策略、距离计算、聚类缩减等核心算法的实现细节,并尝试将其集成到具体的优化模型中进行验证拓展,以强化科研实践能力创新思维。
内容概要:本文围绕基于交替方向乘子法(ADMM)的多主体综合能源系统分布式协同优化展开研究,提出了一种利用ADMM算法实现多个能源主体间高效协同优化的解决方案。该方法将集中式优化问题分解为多个可并行求解的子问题,各主体在保护自身数据隐私的前提下,仅通过交换少量边界信息即可完成全局协同优化,有效解决了传统集中式方法存在的通信负担重、隐私泄露风险高等问题。研究涵盖了模型构建、算法设计、收敛性分析及仿真验证全过程,并以Matlab代码实现了算法原型,展示了其在提升系统运行效率、促进可再生能源消纳方面的潜力。该研究不仅提供了完整的算法实现框架,还深入探讨了ADMM在多区域电网、多微网及产消者等复杂场景下的适用性扩展能力,为现代综合能源系统的分布式管理提供了理论依据和技术支持。; 适合人群:具备一定电力系统、优化理论及Matlab编程基础的研究生、科研人员或从事综合能源系统相关工作的工程技术人员。; 使用场景及目标:①应用于多区域电网、多微网、产消者(Prosumer)等多主体参的综合能源系统协同调度;②实现数据隐私保护下的分布式优化,避免中心节点收集全局敏感信息;③学习ADMM算法在电力系统中的具体建模实现方法,掌握其收敛特性参数整定技巧。; 阅读建议:读者应结合提供的Matlab代码进行实践,重点关注ADMM算法的迭代流程、惩罚因子设定及其对收敛速度的影响,同时可通过修改系统规模参数设置,进一步探究算法在不同场景下的适应性性能表现。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值