我发现很多开发者对 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); // 一次搞定!
聚合的边界设计
那聚合边界应该怎么画?先问问自己:
-
这些对象必须一起变化吗?
-
修改订单项,必须通过订单来操作
-
是的 → 放一个聚合
-
-
这些对象必须保持一致性吗?
-
订单总价 = 所有订单项价格之和
-
是的 → 放一个聚合
-
-
这些对象可以独立存在吗?
-
用户和订单能独立存在吗?能!
-
订单项能脱离订单存在吗?不能!
-
四、聚合根(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);
}
}
聚合根的职责
| 职责 | 说明 |
|---|---|
| 唯一入口 | 外部只能通过聚合根访问内部对象 |
| 维护一致性 | 保证聚合内部数据始终一致 |
| 封装业务规则 | 业务规则写在聚合根里 |
| 控制生命周期 | 聚合根存在,内部对象才存在 |
聚合根的判断标准
-
它有全局唯一标识吗?
-
Order 有 OrderId → 是聚合根
-
OrderItem 的ID只在订单内唯一 → 不是聚合根
-
-
它能被外部直接引用吗?
-
外部可以说"查询订单123" → Order是聚合根
-
外部不会说"查询订单项456" → OrderItem不是聚合根
-
-
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:实体和值对象怎么区分?
问自己三个问题:
-
我关心它的"身份"还是"内容"?
-
关心身份 → 实体
-
关心内容 → 值对象
-
-
两个属性完全相同的对象,是"同一个"吗?
-
不是 → 实体
-
是 → 值对象
-
-
它需要独立的生命周期管理吗?
-
需要 → 实体
-
不需要 → 值对象
-
Q2:聚合边界怎么划分?
遵循这些原则:
-
小聚合原则:聚合越小越好,大聚合会有并发问题
-
一致性边界:需要强一致性的放一起
-
事务边界:一个事务只修改一个聚合
-
业务不变量:需要始终保持的业务规则,决定了聚合边界
Q3:聚合之间怎么关联?
通过ID引用,而不是对象引用!
// 错误:直接引用
public class Order {
private Customer customer; // 直接持有Customer对象
}
// 正确:通过ID引用
public class Order {
private CustomerId customerId; // 只保存ID
}
为什么?因为每个聚合应该独立加载,跨聚合查询是应用层的事。
Q4:值对象一定要不可变吗?
强烈建议不可变!
-
不可变对象是线程安全的
-
更容易推理和测试
-
可以安全地共享
-
修改就创建新对象,符合函数式思想
七、总结
一张图总结

核心要点速记
| 概念 | 一句话总结 |
|---|---|
| 实体 | 有身份证的对象,ID相同就是同一个 |
| 值对象 | 没身份证的对象,内容相同就相等,不可变 |
| 聚合 | 一伙必须绑在一起的对象,保持内部一致性 |
| 聚合根 | 聚合的老大,对外的唯一窗口 |
最终建议
-
先理解业务:这些概念是为业务服务的,不是为了用而用
-
从小开始:先在一个模块试点,不要一上来就全盘DDD
-
持续演进:第一版设计不可能完美,边做边调整
-
团队共识:统一语言比技术实现更重要
📝 DDD 不是银弹,但它确实能帮助我们更好地理解和表达业务。掌握这些核心概念,是迈向领域驱动设计的第一步。