DDD笔记 | 实体、值对象、聚合、聚合根

我发现很多开发者对 DDD 的几个核心概念"似懂非懂",比如实体、值对象、聚合、聚合根。今天我就来把这几个最重要的概念讲透。


为什么需要这些概念?

在传统开发中,我们习惯这样思考:

  • 这个功能需要几张表?

  • 这个接口要传什么参数?

  • 这个SQL怎么写?

这是"数据驱动"的思维方式。

而 DDD 告诉我们,应该这样思考:

  • 业务中有哪些核心概念?

  • 这些概念之间是什么关系?

  • 业务规则应该放在哪里?

这是"领域驱动"的思维方式。

实体、值对象、聚合、聚合根,就是 DDD 给我们的建模工具箱


一、实体(Entity)

大白话解释

实体就是有"身份证"的对象。

想象一下,你去银行办业务:

  • 银行不关心你今天穿什么衣服

  • 银行不关心你今天心情好不好

  • 银行只关心一件事:你的身份证号是多少

只要身份证号对得上,你就是你,哪怕你整容了、改名了,银行也认你。

这就是实体的核心特征:通过唯一标识来区分,而不是通过属性。

代码示例

复制代码
public class User {
    private UserId id;        // 唯一标识
    private String name;      // 可以改
    private String email;     // 可以改
    private String phone;     // 可以改
    
    // 两个User是否相等?只看id!
    @Override
    public boolean equals(Object o) {
        if (this == o) returntrue;
        if (o == null || getClass() != o.getClass()) returnfalse;
        User user = (User) o;
        return Objects.equals(id, user.id);  // 只比较id
    }
}

实体的特征

特征 说明
有唯一标识 身份证号、订单号、用户ID等
生命周期 从创建到销毁,身份不变
可变性 属性可以变化,但身份不变
相等性判断 只看标识,不看属性

常见的实体

  • 用户(User)

  • 订单(Order)

  • 商品(Product)

  • 账户(Account)

  • 文章(Article)


二、值对象(Value Object)

大白话解释

值对象就是"没有身份证"的对象,只看内容。

举个例子:100块钱。

你问我:"这张100块和那张100块,是同一张吗?" 我会说:"什么同一张?100块就是100块,有区别吗?"

对于钱,我们不关心它的"身份",只关心它的"值"------100就是100,50就是50。

这就是值对象:通过属性值来判断相等,没有唯一标识。

代码示例

复制代码
public class Money {
    privatefinal BigDecimal amount;   // 金额
    privatefinal String currency;      // 币种
    
    public Money(BigDecimal amount, String currency) {
        this.amount = amount;
        this.currency = currency;
    }
    
    // 两个Money是否相等?看属性值!
    @Override
    public boolean equals(Object o) {
        if (this == o) returntrue;
        if (o == null || getClass() != o.getClass()) returnfalse;
        Money money = (Money) o;
        return Objects.equals(amount, money.amount) 
            && Objects.equals(currency, money.currency);  // 比较所有属性
    }
    
    // 值对象应该是不可变的
    public Money add(Money other) {
        // 不修改自己,返回新的对象
        returnnew Money(this.amount.add(other.amount), this.currency);
    }
}

值对象的特征

特征 说明
无唯一标识 不需要ID
不可变 创建后不能修改,要改就创建新的
相等性判断 所有属性都相等,才算相等
可替换 两个等价的值对象可以互换

常见的值对象

  • 金额(Money)

  • 地址(Address)

  • 日期范围(DateRange)

  • 手机号(PhoneNumber)

  • 颜色(Color)

  • 坐标(Coordinate)

实体 vs 值对象:一图看懂


三、聚合(Aggregate)

大白话解释

聚合就是"一伙东西必须在一起"。

想象一下你的购物订单:

  • 订单(Order)

  • 订单项(OrderItem):买了什么、数量、价格

  • 收货地址(ShippingAddress)

这三个东西能分开吗?

  • 订单项离开订单,就是无头苍蝇

  • 一个订单的收货地址,跟另一个订单有关系吗?没有

它们天生就是一伙的,要存一起存,要取一起取,要删一起删。

这就是聚合:一组有关联的对象,作为一个整体来看待。

为什么需要聚合?

没有聚合的世界:

复制代码
// 各种Repository,各种单独操作
orderRepository.save(order);
orderItemRepository.save(item1);
orderItemRepository.save(item2);
addressRepository.save(address);

// 问题1:如果item1保存成功,item2失败了怎么办?
// 问题2:查询订单时,要调用多少个Repository?
// 问题3:删除订单时,谁负责删除关联数据?

有聚合的世界:

复制代码
// 只有一个Repository,操作整个聚合
Order order = new Order();
order.addItem(item1);
order.addItem(item2);
order.setShippingAddress(address);

orderRepository.save(order);  // 一次搞定!

聚合的边界设计

那聚合边界应该怎么画?先问问自己:

  1. 这些对象必须一起变化吗?

    • 修改订单项,必须通过订单来操作

    • 是的 → 放一个聚合

  2. 这些对象必须保持一致性吗?

    • 订单总价 = 所有订单项价格之和

    • 是的 → 放一个聚合

  3. 这些对象可以独立存在吗?

    • 用户和订单能独立存在吗?能!

    • 订单项能脱离订单存在吗?不能!


四、聚合根(Aggregate Root)

大白话解释

聚合根就是聚合的"老大",是对外的唯一联系人。

还是拿订单举例:

规则:外部想操作聚合内的任何东西,必须通过聚合根!

代码示例

复制代码
public class Order {  // 聚合根
    private OrderId id;
    private List<OrderItem> items;          // 内部实体
    private ShippingAddress address;         // 内部值对象
    private Money totalAmount;
    
    //  外部通过聚合根来添加订单项
    public void addItem(Product product, int quantity) {
        // 业务规则:检查是否已有相同商品
        OrderItem existing = findItemByProduct(product);
        if (existing != null) {
            existing.increaseQuantity(quantity);
        } else {
            items.add(new OrderItem(product, quantity));
        }
        // 自动重新计算总价
        recalculateTotalAmount();
    }
    
    //  外部通过聚合根来移除订单项
    public void removeItem(ProductId productId) {
        items.removeIf(item -> item.getProductId().equals(productId));
        recalculateTotalAmount();
    }
    
    //  外部通过聚合根来修改地址
    public void changeShippingAddress(ShippingAddress newAddress) {
        // 业务规则:订单已发货后不能改地址
        if (this.status == OrderStatus.SHIPPED) {
            thrownew BusinessException("订单已发货,无法修改地址");
        }
        this.address = newAddress;
    }
    
    //  不要这样做!不要把内部对象直接暴露出去
    // public List<OrderItem> getItems() { return items; }  // 危险!
    
    //  可以返回只读副本
    public List<OrderItem> getItems() {
        return Collections.unmodifiableList(items);
    }
    
    private void recalculateTotalAmount() {
        this.totalAmount = items.stream()
            .map(OrderItem::getSubtotal)
            .reduce(Money.ZERO, Money::add);
    }
}

聚合根的职责

职责 说明
唯一入口 外部只能通过聚合根访问内部对象
维护一致性 保证聚合内部数据始终一致
封装业务规则 业务规则写在聚合根里
控制生命周期 聚合根存在,内部对象才存在

聚合根的判断标准

  1. 它有全局唯一标识吗?

    • Order 有 OrderId → 是聚合根

    • OrderItem 的ID只在订单内唯一 → 不是聚合根

  2. 它能被外部直接引用吗?

    • 外部可以说"查询订单123" → Order是聚合根

    • 外部不会说"查询订单项456" → OrderItem不是聚合根

  3. Repository 是为谁服务的?

    • 有 OrderRepository → Order是聚合根

    • 没有 OrderItemRepository → OrderItem不是聚合根


五、实战案例:电商订单系统

领域模型设计

完整代码示例

复制代码
// ========== 值对象 ==========

publicclass Money {
    publicstaticfinal Money ZERO = new Money(BigDecimal.ZERO, "CNY");
    
    privatefinal BigDecimal amount;
    privatefinal String currency;
    
    public Money(BigDecimal amount, String currency) {
        if (amount.compareTo(BigDecimal.ZERO) < 0) {
            thrownew IllegalArgumentException("金额不能为负");
        }
        this.amount = amount;
        this.currency = currency;
    }
    
    public Money add(Money other) {
        if (!this.currency.equals(other.currency)) {
            thrownew IllegalArgumentException("币种不同,无法相加");
        }
        returnnew Money(this.amount.add(other.amount), this.currency);
    }
    
    public Money multiply(int quantity) {
        returnnew Money(this.amount.multiply(BigDecimal.valueOf(quantity)), this.currency);
    }
}

publicclass Address {
    privatefinal String province;
    privatefinal String city;
    privatefinal String district;
    privatefinal String street;
    privatefinal String zipCode;
    
    public Address(String province, String city, String district, 
                   String street, String zipCode) {
        // 验证逻辑
        this.province = province;
        this.city = city;
        this.district = district;
        this.street = street;
        this.zipCode = zipCode;
    }
    
    public String getFullAddress() {
        return province + city + district + street;
    }
}

// ========== 实体 ==========

publicclass OrderItem {
    privatefinal OrderItemId id;
    privatefinal ProductId productId;
    privatefinal String productName;  // 快照,防止商品改名后订单显示错误
    privatefinal Money unitPrice;     // 快照,下单时的价格
    privateint quantity;
    
    public OrderItem(ProductId productId, String productName, 
                     Money unitPrice, int quantity) {
        this.id = OrderItemId.generate();
        this.productId = productId;
        this.productName = productName;
        this.unitPrice = unitPrice;
        this.quantity = quantity;
    }
    
    public Money getSubtotal() {
        return unitPrice.multiply(quantity);
    }
    
    void increaseQuantity(int delta) {  // 包级别访问,只有Order能调
        this.quantity += delta;
    }
}

// ========== 聚合根 ==========

publicclass Order {
    privatefinal OrderId id;
    privatefinal CustomerId customerId;
    privatefinal List<OrderItem> items;
    private Address shippingAddress;
    private Money totalAmount;
    private OrderStatus status;
    privatefinal LocalDateTime createdAt;
    
    // 创建订单的工厂方法
    public static Order create(CustomerId customerId, Address shippingAddress) {
        Order order = new Order();
        order.id = OrderId.generate();
        order.customerId = customerId;
        order.shippingAddress = shippingAddress;
        order.items = new ArrayList<>();
        order.totalAmount = Money.ZERO;
        order.status = OrderStatus.CREATED;
        order.createdAt = LocalDateTime.now();
        return order;
    }
    
    // 添加商品
    public void addItem(Product product, int quantity) {
        if (status != OrderStatus.CREATED) {
            thrownew BusinessException("订单已确认,无法添加商品");
        }
        if (quantity <= 0) {
            thrownew BusinessException("数量必须大于0");
        }
        
        // 查找是否已有相同商品
        Optional<OrderItem> existing = items.stream()
            .filter(item -> item.getProductId().equals(product.getId()))
            .findFirst();
            
        if (existing.isPresent()) {
            existing.get().increaseQuantity(quantity);
        } else {
            items.add(new OrderItem(
                product.getId(),
                product.getName(),
                product.getPrice(),
                quantity
            ));
        }
        
        recalculateTotalAmount();
    }
    
    // 移除商品
    public void removeItem(ProductId productId) {
        if (status != OrderStatus.CREATED) {
            thrownew BusinessException("订单已确认,无法移除商品");
        }
        items.removeIf(item -> item.getProductId().equals(productId));
        recalculateTotalAmount();
    }
    
    // 修改收货地址
    public void changeShippingAddress(Address newAddress) {
        if (status == OrderStatus.SHIPPED || status == OrderStatus.DELIVERED) {
            thrownew BusinessException("订单已发货,无法修改地址");
        }
        this.shippingAddress = newAddress;
    }
    
    // 确认订单
    public void confirm() {
        if (items.isEmpty()) {
            thrownew BusinessException("订单不能为空");
        }
        if (status != OrderStatus.CREATED) {
            thrownew BusinessException("订单状态不正确");
        }
        this.status = OrderStatus.CONFIRMED;
    }
    
    // 发货
    public void ship() {
        if (status != OrderStatus.PAID) {
            thrownew BusinessException("订单未支付,无法发货");
        }
        this.status = OrderStatus.SHIPPED;
    }
    
    private void recalculateTotalAmount() {
        this.totalAmount = items.stream()
            .map(OrderItem::getSubtotal)
            .reduce(Money.ZERO, Money::add);
    }
    
    // Getters(返回只读数据)
    public OrderId getId() { return id; }
    public List<OrderItem> getItems() { return Collections.unmodifiableList(items); }
    public Money getTotalAmount() { return totalAmount; }
    public OrderStatus getStatus() { return status; }
}

// ========== Repository(只为聚合根服务)==========

publicinterface OrderRepository {
    void save(Order order);
    Order findById(OrderId id);
    List<Order> findByCustomer(CustomerId customerId);
}

六、常见问题 FAQ

Q1:实体和值对象怎么区分?

问自己三个问题:

  1. 我关心它的"身份"还是"内容"?

    • 关心身份 → 实体

    • 关心内容 → 值对象

  2. 两个属性完全相同的对象,是"同一个"吗?

    • 不是 → 实体

    • 是 → 值对象

  3. 它需要独立的生命周期管理吗?

    • 需要 → 实体

    • 不需要 → 值对象

Q2:聚合边界怎么划分?

遵循这些原则:

  1. 小聚合原则:聚合越小越好,大聚合会有并发问题

  2. 一致性边界:需要强一致性的放一起

  3. 事务边界:一个事务只修改一个聚合

  4. 业务不变量:需要始终保持的业务规则,决定了聚合边界

Q3:聚合之间怎么关联?

通过ID引用,而不是对象引用!

复制代码
//  错误:直接引用
public class Order {
    private Customer customer;  // 直接持有Customer对象
}

//  正确:通过ID引用
public class Order {
    private CustomerId customerId;  // 只保存ID
}

为什么?因为每个聚合应该独立加载,跨聚合查询是应用层的事。

Q4:值对象一定要不可变吗?

强烈建议不可变!

  • 不可变对象是线程安全的

  • 更容易推理和测试

  • 可以安全地共享

  • 修改就创建新对象,符合函数式思想


七、总结

一张图总结

核心要点速记

概念 一句话总结
实体 有身份证的对象,ID相同就是同一个
值对象 没身份证的对象,内容相同就相等,不可变
聚合 一伙必须绑在一起的对象,保持内部一致性
聚合根 聚合的老大,对外的唯一窗口

最终建议

  1. 先理解业务:这些概念是为业务服务的,不是为了用而用

  2. 从小开始:先在一个模块试点,不要一上来就全盘DDD

  3. 持续演进:第一版设计不可能完美,边做边调整

  4. 团队共识:统一语言比技术实现更重要


📝 DDD 不是银弹,但它确实能帮助我们更好地理解和表达业务。掌握这些核心概念,是迈向领域驱动设计的第一步。

相关推荐
杨充19 小时前
10.可测试性实战设计
设计模式·开源·代码规范
杨充20 小时前
9.重构十二式的实战
设计模式·开源·代码规范
杨充20 小时前
6.设计原则的全景图
设计模式·开源·全栈
杨充20 小时前
2.面向对象的特性
设计模式
杨充20 小时前
7.SOLID原则案例汇
设计模式·开源·全栈
杨充20 小时前
8.反模式与坏味道
设计模式·开源·代码规范
杨充20 小时前
3.接口vs抽象类比较
设计模式
咖啡八杯1 天前
文法、BNF与AST
java·设计模式·解释器模式·ast·文法
咖啡八杯1 天前
GoF设计模式——解释器模式
java·后端·spring·设计模式