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

相关推荐
wanghowie11 小时前
01.08 Java基础篇|设计模式深度解析
java·开发语言·设计模式
syt_101313 小时前
设计模式之-中介者模式
设计模式·中介者模式
明洞日记13 小时前
【设计模式手册023】外观模式 - 如何简化复杂系统
java·设计模式·外观模式
游戏23人生14 小时前
c++ 语言教程——16面向对象设计模式(五)
开发语言·c++·设计模式
watersink15 小时前
Agent 设计模式
开发语言·javascript·设计模式
老朱佩琪!15 小时前
Unity策略模式
unity·设计模式·策略模式
o0向阳而生0o15 小时前
116、23种设计模式之责任链模式(23/23)(完结撒花)
设计模式·责任链模式
山沐与山1 天前
【设计模式】Python模板方法模式:从入门到实战
python·设计模式·模板方法模式
阿拉斯攀登1 天前
设计模式:责任链模式
设计模式·责任链模式