提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
目前,大厂的后端框架已很少使用MVC架构,多用DDD架构。主包在面试的过程中也被询问过是否了解DDD架构,所以学习DDD架构在面试中时很有优势滴。本篇文章适合有MVC架构基础的同学食用OVO!主包尽量用通俗易懂的语言结合例子来讲清楚DDD架构。若有不准确的地方欢迎大家批评指正!━(`∀´)ノ亻!
一、DDD框架和MVC框架
为什么现在大厂不怎么用MVC架构了?
传统MVC(Controller-Service-DAO)以数据表驱动开发,易导致“贫血模型”(业务逻辑散落在Service层),Service层较为臃肿;简单来说,我们在开发过程中使用到的各种实体类(PO\BO\DTO等)是不是只向外提供get、set方法而不实现其他方法?当我们需要实现一个接口的时候就会在service去完成相应的业务逻辑。当业务越来越庞大,我们的service层也会越来越臃肿。如果是中小业务的话MVC架构当然是个不错的选择,但是对于复杂业务来说,MVC架构会导致业务逻辑散落在Service层,而DDD架构能实现业务的内聚。
为什么要用DDD架构?
DDD(领域驱动设计)和MVC架构不同的点就在于,它会将一些方法直接封装在实体类中,而service层只负责组合这些方法进行“流程调度”。这也叫做“充血模型”。
核心区别在于:MVC 按 “技术职责” 分层,DDD 按 “业务领域” 拆分。
1.1场景举例
场景:实现 “外卖订单支付超时自动取消” 功能
业务规则如下:
1. 订单创建后,若 30 分钟未支付,自动取消;
2. 取消时需检查是否已出餐(已出餐则不取消,仅标记 “支付超时”);
3. 取消成功后,恢复商品库存。
MVC 架构实现(按技术分层)
MVC 通常分为:Controller(接收请求)、Service(业务逻辑)、DAO(数据访问)。业务逻辑会集中在 Service 层,直接依赖数据操作。
1. 实体类(仅作为数据载体,无业务行为)
// 订单实体(仅存数据,无业务逻辑)
public class Order {
private Long orderId;
private Long productId;
private Integer quantity;
private String status; // 待支付、已出餐、已取消、支付超时
private String cancelReason;
// getter/setter省略
}
// 库存实体(仅存数据)
public class Stock {
private Long productId;
private Integer quantity;
// getter/setter省略
}
2. 数据访问层(DAO)
// 订单DAO(负责数据库操作)
public interface OrderDao {
Order getById(Long orderId);
void update(Order order);
}
// 库存DAO
public interface StockDao {
Stock getByProductId(Long productId);
void update(Stock stock);
}
3. 服务层(Service,包含所有业务逻辑)
@Service
public class OrderService {
@Autowired
private OrderDao orderDao;
@Autowired
private StockDao stockDao;
// 处理支付超时逻辑
public String cancelOnTimeout(Long orderId) {
// 1. 查询订单(技术操作)
Order order = orderDao.getById(orderId);
if (order == null) {
return "订单不存在";
}
// 2. 检查是否已出餐(业务规则)
if ("已出餐".equals(order.getStatus())) {
order.setStatus("支付超时");
orderDao.update(order); // 技术操作
return "已出餐,不取消订单";
}
// 3. 取消订单(业务规则)
order.setStatus("已取消");
order.setCancelReason("支付超时");
orderDao.update(order); // 技术操作
// 4. 恢复库存(业务规则)
Stock stock = stockDao.getByProductId(order.getProductId());
stock.setQuantity(stock.getQuantity() + order.getQuantity());
stockDao.update(stock); // 技术操作
return "订单已取消,库存已恢复";
}
}
DDD 架构实现(按业务领域拆分)
1. 领域模型(实体类封装业务行为)
// 订单领域模型(包含业务规则和行为)
public class Order {
private Long orderId;
private Long productId;
private Integer quantity;
private String status; // 待支付、已出餐、已取消、支付超时
private String cancelReason;
// 构造方法
public Order(Long orderId, Long productId, Integer quantity, String status) {
this.orderId = orderId;
this.productId = productId;
this.quantity = quantity;
this.status = status;
}
// 核心业务行为:处理支付超时(封装业务规则)
public boolean handlePaymentTimeout() {
if ("已出餐".equals(this.status)) {
this.status = "支付超时";
return false; // 不取消订单
}
// 符合取消条件
this.status = "已取消";
this.cancelReason = "支付超时";
return true; // 已取消订单
}
// getter(仅暴露必要字段,不提供setter避免外部随意修改状态)
public Long getOrderId() { return orderId; }
public Long getProductId() { return productId; }
public Integer getQuantity() { return quantity; }
public String getStatus() { return status; }
}
// 库存领域模型(封装库存行为)
public class Stock {
private Long productId;
private Integer quantity;
public Stock(Long productId, Integer quantity) {
this.productId = productId;
this.quantity = quantity;
}
// 业务行为:恢复库存
public void restore(Integer quantity) {
this.quantity += quantity;
}
// getter
public Long getProductId() { return productId; }
public Integer getQuantity() { return quantity; }
}
2. 仓储(Repository,负责领域模型的持久化)
// 订单仓储(隔离技术实现,返回领域模型)
public interface OrderRepository {
Order findById(Long orderId);
void save(Order order); // 将领域模型持久化到数据库
}
// 库存仓储
public interface StockRepository {
Stock findByProductId(Long productId);
void save(Stock stock);
}
3. 领域服务(协调多个领域模型)
@Service
public class OrderDomainService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private StockRepository stockRepository;
// 协调订单取消流程(不包含业务规则,仅调用模型行为)
public String cancelOnTimeout(Long orderId) {
// 1. 获取领域模型
Order order = orderRepository.findById(orderId);
if (order == null) {
return "订单不存在";
}
// 2. 调用订单模型的业务行为(核心逻辑由模型实现)
boolean isCanceled = order.handlePaymentTimeout();
// 3. 若取消成功,调用库存模型的行为
if (isCanceled) {
Stock stock = stockRepository.findByProductId(order.getProductId());
stock.restore(order.getQuantity());
stockRepository.save(stock); // 持久化库存
}
// 4. 持久化订单
orderRepository.save(order);
return isCanceled ? "订单已取消,库存已恢复" : "已出餐,不取消订单";
}
}
1.2核心差异对比
| 维度 | MVC架构 | DDD架构 |
|---|---|---|
| 业务逻辑位置 | 集中在Service层,与数据库操作混合 | 封装在领域模型(Order/Stock)中 |
| 实体角色 | 仅作为数据载体(DTO性质) | 包含业务行为(“活的”业务对象) |
| 可维护性 | 业务变更需修改Service多个步骤 | 业务变更只需修改对应领域模型 |
| 业务可读性 | 需通读Service代码理解规则 | 直接通过领域模型的方法名理解业务 |
DDD的核心价值在于:让代码结构贴合业务逻辑,当业务复杂度上升时(如新增“预售订单不恢复库存”“超时前5分钟提醒”等规则),只需扩展领域模型的行为,无需重构整个流程,大幅降低维护成本。
1.3各层对应关系
MVC以“技术职责”划分(接收请求、处理逻辑、数据访问),DDD以“业务价值+技术职责”划分(核心是领域层,其他层为领域层服务)。二者的映射是“功能对应”而非“严格对等”,具体如下表:
| MVC层级 | 核心职责 | 对应DDD层级 | DDD对应层级的核心职责 | 映射说明 |
|---|---|---|---|---|
| Controller | 接收请求、参数校验、返回响应 | 应用层(Application Layer) | 封装用户用例、协调领域层执行、不包含业务规则(如调用领域服务完成“订单取消”流程) | 功能最接近,都是“对外接口层”,但DDD应用层更聚焦“业务用例”,而非单纯的请求转发 |
| Service | 处理业务逻辑、调用DAO操作数据 | 1. 领域层(Domain Layer) 2. 应用层 | 1. 领域层:封装核心业务规则(如Order类的handlePaymentTimeout方法)2. 应用层:协调领域层执行流程(不实现业务逻辑,仅做流程调度) | MVC的Service职责被DDD拆分为两层,核心业务逻辑归领域层,流程协调归应用层 |
| DAO | 执行数据库CRUD、隔离数据访问细节 | 基础设施层(Infrastructure Layer) | 1. 实现领域层的仓储(Repository)接口(如OrderRepositoryImpl)2. 提供技术能力(数据库、缓存、消息队列) | MVC的DAO是DDD基础设施层的“子集”,DDD仓储更强调“为领域模型服务”而非单纯的CRUD |
| (无对应层) | - | 领域层(Domain Layer) | 整个架构的核心,包含领域模型(实体、值对象)、领域服务、仓储接口,封装业务本质 | MVC无对应层,这是DDD与MVC最核心的区别,也是DDD解决复杂业务的关键 |
可以用一个图来大致表示

二、DDD各层级的定义与核心职责
2.1 DDD架构完整层级

1. 用户接口层(User Interface Layer)
- 职责:协议适配、权限校验、数据转换
- 核心组件:
Controller:接收HTTP/RPC等请求(如SpringMVC的@RestController)DTO(Data Transfer Object):定义入参(Request)和出参(Response)契约,例如MemberRequestAssembler:将DTO与领域对象互相转换(避免领域模型泄露到外部)
- 典型操作:参数校验 → 调用应用层服务 → 返回DTO
2. 应用层(Application Layer)
- 职责:用例编排、事务控制、跨领域服务协调(无业务逻辑)
- 核心组件:
Application Service:如MemberCommandService,封装一个用户用例(如“更新会员姓名”)- 事务管理:通过
@Transactional注解声明事务边界
- 关键原则:薄层设计(仅协调,业务规则下沉到领域层)
3. 领域层(Domain Layer)
- 职责:表达业务概念、规则、状态(系统核心)
- 核心组件:
Entity(实体):有唯一ID和生命周期(如Member包含ID、姓名、状态)Value Object(值对象):无ID的不可变对象(如Address包含省市区)Domain Service(领域服务):处理跨实体逻辑(如TransferService处理转账)Domain Event(领域事件):如MemberNameChangedEvent(事件携带旧/新姓名)Repository Interface:仓储接口定义(如MemberRepository.findByID())
- 设计关键:实体充血模型(含行为方法,如
Member.updateName())
4. 基础设施层(Infrastructure Layer)
- 职责:技术细节实现,支持上层功能
- 核心组件:
Repository Impl:实现领域层的仓储接口(如MemberRepositoryImpl操作MySQL)Domain Event Publisher:事件发布(如将事件发到Kafka)- 工具类:缓存、短信、文件存储等SDK封装
- 依赖方向:依赖倒置 → 实现领域层定义的接口(解耦技术细节)
依赖关系:用户接口层 → 应用层 → 领域层 ← 基础设施层
领域层不依赖任何层!也就是说领域模型向其它层提供的方法都是高内聚,低耦合的。
在架构设计中,设计领域模型是最重要的一个环节!
三.DDD架构中领域模型的组成与设计
场景:电商平台的订单退款流程
业务流程如下
校验退款条件(如订单状态为 “已支付”“未发货” 可全额退;“已发货” 需扣运费);
创建退款单,生成唯一退款单号;
退款审核通过后,更新订单状态为 “已退款”;
恢复商品库存(仅针对未发货订单);
触发 “退款成功” 事件,通知财务系统处理退款打款。
3.1领域模型设计
1. 聚合根(Aggregate Root)
聚合根是领域模型的入口,负责维护聚合内的一致性,这里以Order(订单)和Refund(退款单)为核心聚合根。
(1)订单聚合根(Order)
// 订单聚合根(包含订单的核心信息和行为)
public class Order {
private OrderId id; // 订单ID(值对象)
private UserId userId; // 用户ID(值对象)
private List<OrderItem> items; // 订单项项(实体,聚合内的子实体)
private Money totalAmount; // 总金额(值对象)
private OrderStatus status; // 订单状态(枚举:已创建、已支付、已发货、已完成、已取消)
private ShippingFee shippingFee; // 运费(值对象)
// 业务行为:判断是否可申请退款
public boolean canApplyRefund() {
// 规则:已支付且未完成的订单可申请退款
return status == OrderStatus.PAID || status == OrderStatus.SHIPPED;
}
// 业务行为:更新订单为“已退款”状态
public void markAsRefunded() {
if (status == OrderStatus.COMPLETED) {
throw new IllegalStateException("已完成的订单无法退款");
}
this.status = OrderStatus.REFUNDED;
}
// getter(仅暴露必要字段,禁止外部直接修改状态)
public OrderId getId() { return id; }
public List<OrderItem> getItems() { return Collections.unmodifiableList(items); } // 禁止外部修改集合
public Money getTotalAmount() { return totalAmount; }
public ShippingFee getShippingFee() { return shippingFee; }
public OrderStatus getStatus() { return status; }
}
(2)退款单聚合根(Refund)
// 退款单聚合根(负责退款流程的核心逻辑)
public class Refund {
private RefundId id; // 退款单ID(值对象)
private OrderId orderId; // 关联订单ID
private Money refundAmount; // 退款金额(值对象)
private RefundReason reason; // 退款原因(值对象)
private RefundStatus status; // 退款状态(枚举:待审核、审核通过、审核拒绝)
private LocalDateTime applyTime; // 申请时间
// 构造方法:创建退款单(封装退款金额计算规则)
public Refund(RefundId id, Order order, RefundReason reason, RefundService refundService) {
if (!order.canApplyRefund()) {
throw new IllegalArgumentException("当前订单状态不支持退款");
}
this.id = id;
this.orderId = order.getId();
this.reason = reason;
this.applyTime = LocalDateTime.now();
this.status = RefundStatus.PENDING_REVIEW;
// 调用领域服务计算退款金额(跨聚合逻辑)
this.refundAmount = refundService.calculateRefundAmount(order, reason);
}
// 业务行为:审核通过退款
public RefundSuccessEvent approve() {
if (this.status != RefundStatus.PENDING_REVIEW) {
throw new IllegalStateException("只有待审核的退款单可操作");
}
this.status = RefundStatus.APPROVED;
// 触发“退款成功”领域事件
return new RefundSuccessEvent(this.id, this.orderId, this.refundAmount);
}
// 业务行为:拒绝退款
public void reject() {
if (this.status != RefundStatus.PENDING_REVIEW) {
throw new IllegalStateException("只有待审核的退款单可操作");
}
this.status = RefundStatus.REJECTED;
}
// getter
public RefundId getId() { return id; }
public OrderId getOrderId() { return orderId; }
public Money getRefundAmount() { return refundAmount; }
}
2. 值对象(Value Object)
值对象是“无唯一标识、不可变”的对象,用于描述事物的属性。
| 值对象 | 作用 | 核心代码示例 |
|---|---|---|
OrderId | 订单唯一标识 | public class OrderId { private final String value; /* 构造器+getter,无setter */ } |
RefundId | 退款单唯一标识 | public class RefundId { private final String value; /* 同上 */ } |
UserId | 用户唯一标识 | public class UserId { private final Long value; /* 同上 */ } |
Money | 金额(包含币种、数值) | public class Money { private final BigDecimal amount; private final String currency; /* 提供加法、比较等方法 */ } |
ShippingFee | 运费(继承Money,附加规则) | public class ShippingFee extends Money { /* 重写退款时的计算规则 */ } |
RefundReason | 退款原因(包含类型和描述) | public class RefundReason { private final RefundType type; private final String description; } |
值对象的本质就是:固定不变的属性组合+统一的操作规则
可以发现所有的值对象的属性都是用final修饰的,一旦赋值就不可更改。
怎么理解无唯一标识呢?
比如有两个订单order对象,所有的属性都一样,但订单的Id是不一样的,所以order不属于值对象,这两个order也不相等,是否唯一用Id来区分。而order里的地址Adress,如果两个address对象的属性值是一样的,那我们就认为这两个地址是一致的,Adress就属于值对象,无唯一标识,比较时只比较值,是否唯一用值来区分。
3. 实体(Entity,聚合内的子实体)
子实体依赖聚合根存在,有局部唯一标识(如订单项在订单内的ID)。
// 订单项(订单聚合内的子实体)
public class OrderItem {
private ItemId id; // 订单项ID(仅在订单内唯一)
private ProductId productId; // 商品ID(值对象)
private Integer quantity; // 数量
private Money unitPrice; // 单价(值对象)
// 业务行为:计算订单项总金额
public Money getTotalPrice() {
return new Money(unitPrice.getAmount().multiply(new BigDecimal(quantity)), unitPrice.getCurrency());
}
// getter
public ProductId getProductId() { return productId; }
public Integer getQuantity() { return quantity; }
}
4. 领域服务(Domain Service)
处理跨实体/聚合的业务逻辑(单一实体无法完成的规则)。
// 退款领域服务(处理跨订单和退款单的逻辑)
public class RefundService {
// 计算退款金额(核心规则:未发货订单退全额,已发货订单扣运费)
public Money calculateRefundAmount(Order order, RefundReason reason) {
if (order.getStatus() == OrderStatus.PAID) { // 未发货(仅支付未发货)
return order.getTotalAmount(); // 退全额(含运费)
} else if (order.getStatus() == OrderStatus.SHIPPED) { // 已发货
// 退商品金额,扣除运费
Money productTotal = order.getTotalAmount().subtract(order.getShippingFee());
return productTotal;
}
throw new IllegalArgumentException("不支持的订单状态");
}
// 恢复库存(跨订单和库存聚合的逻辑)
public void restoreStock(Order order, StockRepository stockRepository) {
// 规则:仅未发货订单退款时恢复库存
if (order.getStatus() == OrderStatus.PAID) {
for (OrderItem item : order.getItems()) {
Stock stock = stockRepository.findByProductId(item.getProductId());
stock.increase(item.getQuantity()); // 调用库存实体的行为
stockRepository.save(stock);
}
}
}
}
5. 领域事件(Domain Event)
记录领域中发生的重要事件,用于解耦跨领域的操作(如通知财务系统)。
// 退款成功事件
public class RefundSuccessEvent {
private final RefundId refundId;
private final OrderId orderId;
private final Money refundAmount;
private final LocalDateTime occurredTime; // 事件发生时间
public RefundSuccessEvent(RefundId refundId, OrderId orderId, Money refundAmount) {
this.refundId = refundId;
this.orderId = orderId;
this.refundAmount = refundAmount;
this.occurredTime = LocalDateTime.now();
}
// getter
public RefundId getRefundId() { return refundId; }
public OrderId getOrderId() { return orderId; }
public Money getRefundAmount() { return refundAmount; }
}
// 事件处理器(基础设施层实现,解耦领域层与外部系统)
public class RefundSuccessEventHandler {
private final FinancialService financialService; // 财务系统接口
public void handle(RefundSuccessEvent event) {
// 通知财务系统打款
financialService.transfer(
event.getOrderId().getValue(),
event.getRefundAmount().getAmount()
);
}
}
3.2 模型关系与依赖说明
-
聚合边界:
- 订单聚合:
Order(根)+OrderItem(子实体)+ 关联的值对象(OrderId、Money等)。 - 退款聚合:
Refund(根)+ 关联的值对象(RefundId、RefundReason等)。
聚合间通过ID关联(如Refund关联OrderId,而非直接引用Order对象),避免跨聚合修改。
- 订单聚合:
-
依赖方向:
- 领域服务(
RefundService)可调用多个实体/聚合的行为(如Order、Stock)。 - 领域事件(
RefundSuccessEvent)由聚合根(Refund)在状态变更时产生,由基础设施层的事件处理器处理。
- 领域服务(
-
业务规则内聚:
- 订单是否可退款 → 封装在
Order.canApplyRefund()。 - 退款金额计算 → 封装在
RefundService.calculateRefundAmount()。 - 库存恢复规则 → 封装在
RefundService.restoreStock()。
- 订单是否可退款 → 封装在
3.3 应用层调用示例(协调流程)
@Service
public class RefundApplicationService {
private final OrderRepository orderRepository;
private final RefundRepository refundRepository;
private final RefundService refundService;
private final EventPublisher eventPublisher; // 事件发布器(基础设施层提供)
// 处理退款申请
public void applyRefund(RefundApplyDTO dto) {
// 1. 获取领域模型
Order order = orderRepository.findById(new OrderId(dto.getOrderId()));
// 2. 创建退款单(通过构造器校验规则)
Refund refund = new Refund(
new RefundId(generateRefundNo()),
order,
new RefundReason(dto.getReasonType(), dto.getDescription()),
refundService
);
// 3. 保存退款单
refundRepository.save(refund);
}
// 处理退款审核通过
public void approveRefund(String refundId) {
// 1. 获取领域模型
Refund refund = refundRepository.findById(new RefundId(refundId));
Order order = orderRepository.findById(refund.getOrderId());
// 2. 执行审核通过逻辑,触发领域事件
RefundSuccessEvent event = refund.approve();
// 3. 更新订单状态
order.markAsRefunded();
// 4. 恢复库存(调用领域服务)
refundService.restoreStock(order, stockRepository);
// 5. 保存变更
refundRepository.save(refund);
orderRepository.save(order);
// 6. 发布事件(通知外部系统)
eventPublisher.publish(event);
}
}
通过这个设计,所有的业务规则都封装在领域模型中,应用层只负责流程的协调,就如同写流程一样,方便相关人员快速梳理业务逻辑。当业务变更时(如新增“7天无理由退款”规则)时,只需要修改Order.canApplyRefund()和RefundService的计算逻辑,无需重构整个流程,体现了DDD“高内聚,低耦合”的优势
3.4 Question
为什么要存在聚合根?
聚合根存在的意义就是解决复杂领域模型如何保持一致性问题。
- 如果没有聚合根
我们将Order和OrderItem拆分成两个独立的实体,它们是 “整体 - 部分” 关系(一个订单包含多个订单项),如果没有聚合根,会有三个问题。
1.关系失控:订单项是订单的一部分,但可以被单独修改,导致 “订单不知道自己的订单项被改了”(比如订单项数量改大了,订单总金额却没更新)。
2.规则失效:订单可能有 “最多点 5 道菜” 的规则,但因为订单项能被单独添加,这个规则根本拦不住。
3.数据不一致:删除订单时,可能忘了删关联的订单项,导致数据库里出现 “孤儿订单项”。
// 没有聚合根的设计:Order和OrderItem都是独立实体,可被单独修改
public class Order {
private Long id;
private List<Long> orderItemIds; // 只记录订单项ID,不直接关联对象
}
public class OrderItem {
private Long id;
private Long orderId; // 关联订单ID
private String dishName;
private int quantity;
// 订单项可以被单独修改(比如直接改数量)
public void setQuantity(int quantity) {
this.quantity = quantity;
}
}
// 业务操作可能出现的混乱:
public class BadService {
public void updateOrderItem(Long orderItemId, int newQuantity) {
// 直接修改订单项,完全绕过订单的规则校验
OrderItem item = orderItemRepo.findById(orderItemId);
item.setQuantity(newQuantity); // 这里可能改出负数,或超过库存
orderItemRepo.save(item);
}
}
-聚合根存在的意义:给 “一组相关实体” 画个圈,圈住规则和一致性
聚合根本质是 “一组相关实体 / 值对象的管理者”,它划了一个 “聚合边界”:
1.边界内的实体(如OrderItem)只能通过聚合根(Order)访问,外部不能直接操作。
2.聚合根负责维护边界内的所有业务规则(比如 “订单项数量不能为负”“总菜数不超过 5 道”)。
3.聚合根是这一组对象的 “唯一入口”,外部只能通过它与内部实体交互。
// 聚合根:Order(管理整个订单聚合)
public class Order {
private OrderId id; // 聚合根必须有全局唯一ID
private List<OrderItem> items = new ArrayList<>(); // 直接包含订单项(边界内)
// 外部只能通过聚合根的方法添加订单项(规则由聚合根控制)
public void addItem(Dish dish, int quantity) {
// 规则1:最多点5道菜
if (items.size() >= 5) {
throw new RuntimeException("最多点5道菜");
}
// 规则2:数量不能为负
if (quantity <= 0) {
throw new RuntimeException("数量必须为正");
}
// 规则3:检查库存
if (dish.getStock() < quantity) {
throw new RuntimeException(dish.getName() + "售罄");
}
// 符合规则才添加
items.add(new OrderItem(dish.getId(), dish.getName(), quantity));
}
// 外部只能通过聚合根修改订单项(比如改数量)
public void updateItemQuantity(ProductId dishId, int newQuantity) {
OrderItem item = findItem(dishId);
// 规则:新数量必须为正
if (newQuantity <= 0) {
throw new RuntimeException("数量必须为正");
}
item.setQuantity(newQuantity); // 内部实体的修改仍受聚合根管控
}
// 禁止外部直接获取订单项列表(避免绕过聚合根修改)
public List<OrderItem> getItems() {
return Collections.unmodifiableList(items); // 返回不可修改的视图
}
// 内部方法:查找订单项(仅聚合根内部可用)
private OrderItem findItem(ProductId dishId) { ... }
}
// 子实体:OrderItem(依赖聚合根存在,无全局唯一ID,只有局部标识)
public class OrderItem {
private ItemId id; // 仅在当前订单内唯一(局部ID)
private ProductId dishId;
private String dishName;
private int quantity;
// 子实体的setter仅允许聚合根调用(可通过访问控制修饰符限制)
void setQuantity(int quantity) {
this.quantity = quantity;
}
}
- 小观察:
我们看上面的代码能看到,聚合根里的实体是用类来进行关联的,而聚合和聚合之间的关联是通过聚合ID来关联的。我们知道聚合的主要目的就是为了保证数据修改的一致性,所以才用实体类来进行关联。当我们用ID进行关联的时候,实际上是为了防止修改一个类的对象对另外一个关联的类的对象造成影响,而这恰恰就是我们聚合类之间所需要的,我们需要防止跨聚合类产生的意外修改。所以需要保持一致性的实体我们要用对象进行关联,不需要保证一致性的我们用ID进行关联。
为什么要存在值对象?
值对象其实是解决 “零散属性不好管、业务规则藏不住” 的问题
比如价格Money
- 不用值对象
如果不用Money值对象,会怎么写?大概率是把 “金额” 和 “币种” 拆成两个独立字段,散在Dish或OrderItem里
// 不用值对象:价格拆成两个字段,问题很多
public class Dish {
private Long id;
private String name;
private BigDecimal priceAmount; // 金额(比如19.9)
private String priceCurrency; // 币种(比如CNY)
private int stock;
// 要算“两个菜品总价”,得自己拼字段
public BigDecimal calculateTotal(Dish anotherDish) {
// 1. 先判断币种是否一样(每次计算都要写这段)
if (!this.priceCurrency.equals(anotherDish.priceCurrency)) {
throw new RuntimeException("币种不一样,不能相加!");
}
// 2. 再相加金额
return this.priceAmount.add(anotherDish.priceAmount);
}
}
会有三个问题
1.属性 “散养”,容易漏规则:比如算总价时,忘了判断币种(直接把 19.9 美元和 19.9 人民币加起来),业务错误就出现了;
2.重复代码到处堆:每次用到 “价格计算”(比如算订单总价、优惠折扣),都要写一遍 “判断币种 + 相加”,代码又臭又长;
3.属性可随便改:比如有人误把priceCurrency改成 “USD”,但priceAmount还是人民币的 19.9,数据就乱了(19.9 美元和 19.9 人民币完全不是一回事)。
- 用了值对象
值对象的核心作用,就是把 “相关联的属性 + 业务规则” 打包成一个 “不可变的整体”。比如把priceAmount和priceCurrency拼成Money值对象:
// 用值对象:把价格的属性和规则“打包”
public class Money {
// 1. 不可变:创建后就不能改(没有setter)
private final BigDecimal amount;
private final String currency;
// 2. 构造时就校验规则(比如金额不能为负)
public Money(BigDecimal amount, String currency) {
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new RuntimeException("金额不能为负数!");
}
if (!"CNY".equals(currency) && !"USD".equals(currency)) {
throw new RuntimeException("不支持的币种!");
}
this.amount = amount;
this.currency = currency;
}
// 3. 把“计算逻辑”封装进来(不用到处写重复代码)
public Money add(Money other) {
// 币种校验规则只写一次
if (!this.currency.equals(other.currency)) {
throw new RuntimeException("币种不一样,不能相加!");
}
return new Money(this.amount.add(other.amount), this.currency);
}
// 只提供getter,不提供setter(保证不可变)
public BigDecimal getAmount() { return amount; }
public String getCurrency() { return currency; }
}
// 现在Dish里只用放一个Money对象,干净多了
public class Dish {
private Long id;
private String name;
private Money price; // 直接用值对象,不用拆字段
private int stock;
// 算总价时,直接调用值对象的方法,不用自己写规则
public Money calculateTotal(Dish anotherDish) {
return this.price.add(anotherDish.price); // 一行搞定,还不会错
}
}
这样就解决了上面的三个问题:
1.规则 “焊死” 在值对象里:币种校验、金额不能为负,只在Money里写一次,不管哪里用价格,都自动遵循规则;
2.代码不重复:算总价、算折扣、算退款,直接调用Money.add()/Money.multiply(),不用每次都写判断;
3.数据不会乱:Money是不可变的(没有 setter),一旦创建,金额和币种就改不了,不会出现 “币种和金额不匹配” 的情况。
聚合根和实体有什么联系和区别?
聚合根(Aggregate Root)和实体(Entity)是 “特殊与一般” 的关系 ——聚合根是实体的 “升级版”,所有聚合根都是实体,但不是所有实体都是聚合根。核心联系是 “都有唯一身份、状态可变”,核心区别是 “聚合根多了‘管理聚合、维护一致性’的职责”。
- 聚合根和实体的联系
不管是聚合根(如Order)还是普通实体(如OrderItem),都满足 “实体” 的核心定义:
1.有唯一身份标识:都有 ID(聚合根是全局唯一 ID,普通实体可能是局部唯一 ID),通过 ID 区分 “是不是同一个对象”;
2.状态可变:都有业务行为来修改自身状态(如Order.markAsRefunded()修改订单状态,OrderItem.increaseQuantity()修改订单项数量);
3.封装属性和行为:都不会把属性暴露给外部直接修改,而是通过自身方法(行为)修改状态,保证规则不被绕过。
// 1. 普通实体(订单项OrderItem):有ID、状态可变、封装行为
public class OrderItem { // 普通实体
private ItemId id; // 局部唯一ID(仅在订单内唯一)
private int quantity; // 状态可变
// 封装行为:修改数量(有规则校验)
public void increaseQuantity(int num) {
if (num <= 0) throw new RuntimeException("数量不能为负");
this.quantity += num;
}
// 相等性判断:基于ID(实体特征)
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
OrderItem item = (OrderItem) o;
return Objects.equals(id, item.id);
}
}
// 2. 聚合根(订单Order):本质是“特殊实体”,满足实体所有特征
public class Order { // 聚合根(也是实体)
private OrderId id; // 全局唯一ID(实体特征)
private OrderStatus status; // 状态可变(实体特征)
// 封装行为:修改状态(有规则校验)(实体特征)
public void markAsRefunded() {
if (status == OrderStatus.COMPLETED) throw new RuntimeException("已完成订单不能退款");
this.status = OrderStatus.REFUNDED;
}
// 相等性判断:基于ID(实体特征)
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Order order = (Order) o;
return Objects.equals(id, order.id);
}
}
- 关键区别:聚合根多了 “管理职责” 和 “边界属性”
聚合根的核心额外职责是:作为 “聚合的管理者”,维护聚合内所有实体 / 值对象的一致性,同时作为外部访问聚合的唯一入口。普通实体没有这个职责,只管好自己的属性和行为。
| 对比维度 | 聚合根(Aggregate Root) | 普通实体(Entity) |
|---|---|---|
| ID范围 | 全局唯一ID(整个系统中唯一,如订单号ORD123) | 局部唯一ID(仅在所属聚合内唯一,如订单项ITEM001只在ORD123中唯一) |
| 核心职责 | 1. 管理聚合内所有对象(实体+值对象) 2. 维护聚合内业务规则(如“订单最多5个订单项”) 3. 作为外部访问聚合的唯一入口 | 仅封装自身的属性和行为(如订单项计算总价、修改数量),不管理其他对象 |
| 外部可见性 | 对外部可见(是聚合的“代表”,外部通过它访问聚合内对象) | 对外部不可见(只能被所属聚合根访问,外部不能直接操作) |
| 存在依赖 | 可独立存在(如订单可以单独创建、删除,不依赖其他对象) | 依赖聚合根存在(如订单项不能脱离订单单独存在,删除订单时订单项也要删除) |
| 跨对象协作 | 可协调聚合内多个实体的行为(如订单计算总价,需要调用所有订单项的getTotalPrice()) | 仅能操作自身,不能协调其他实体(订单项不能直接调用其他订单项的方法) |
// 聚合根(Order)的“管理职责”体现
public class Order { // 聚合根
private OrderId id; // 全局唯一ID
private List<OrderItem> items = new ArrayList<>(); // 管理聚合内的普通实体
// 职责1:作为外部访问的唯一入口(外部不能直接创建OrderItem)
public void addItem(Product product, int quantity) {
// 职责2:维护聚合内规则(订单最多5个订单项)
if (items.size() >= 5) throw new RuntimeException("最多添加5个商品");
// 职责3:协调聚合内对象(创建订单项并添加到集合)
OrderItem item = new OrderItem(new ItemId(generateLocalId()), product.getId(), quantity, product.getPrice());
items.add(item);
}
// 职责4:维护聚合内数据一致性(计算总价,需要调用所有订单项的行为)
public Money calculateTotal() {
return items.stream()
.map(OrderItem::getTotalPrice)
.reduce(new Money(BigDecimal.ZERO, "CNY"), Money::add);
}
// 禁止外部直接修改聚合内对象(仅返回不可修改集合)
public List<OrderItem> getItems() {
return Collections.unmodifiableList(items);
}
}
// 普通实体(OrderItem)的“无管理职责”体现
public class OrderItem { // 普通实体
private ItemId id; // 局部唯一ID
private ProductId productId;
private int quantity;
private Money unitPrice;
// 仅管好自己的行为(计算自身总价),不管理其他对象
public Money getTotalPrice() {
return new Money(unitPrice.getAmount().multiply(new BigDecimal(quantity)), unitPrice.getCurrency());
}
// 仅允许聚合根修改(访问控制限制,如包级私有setter)
void setQuantity(int quantity) {
this.quantity = quantity;
}
}
- 实际应用:如何判断“谁是聚合根”?
记住一个简单原则:谁需要“统筹管理一组相关对象”,谁就是聚合根。
比如电商场景中:
- 订单(
Order)需要管理订单项(OrderItem),控制“添加商品数量”“计算总价”等跨对象规则 →Order是聚合根; - 订单项(
OrderItem)只需要管好自己的数量、单价,不用管其他对象 → 普通实体; - 退款单(
Refund)需要管理退款金额、退款原因,还要关联订单(通过ID) →Refund是聚合根; - 购物车(
ShoppingCart)需要管理购物车项(CartItem),控制“修改商品数量”“清空购物车”等规则 →ShoppingCart是聚合根。
再举一个反例:如果一个实体不需要管理其他对象,也没有跨对象规则,那它就是普通实体(或独立实体,非聚合内)。比如“商品(Product)”:它只需要管好自己的价格、库存、名称,不用管理其他对象 → 是独立实体(非聚合根,也不是聚合内的普通实体)。
常见面试题
因为DDD是一种软件设计思想,通常会结合项目来问,可以把项目丢给AI让AI结合你的项目来做出回答,以下回答仅供参考。
1.DDD是什么?
DDD(领域驱动设计)是一种以业务领域为核心的软件设计思想:先深入理解业务规则、拆分核心概念(如实体、值对象、聚合根),用领域模型映射真实业务,再围绕模型组织代码;核心是让代码结构贴合业务逻辑,而非技术框架,同时通过聚合边界、领域服务、事件等设计,解决复杂业务的一致性、可维护性问题,最终实现 “业务能懂、易扩展、低耦合”。
2.DDD的使用场景和解决的问题是什么?
1. 使用场景:
核心适用于 业务逻辑复杂、规则多变、生命周期长 的系统,比如电商(订单/退款/库存)、金融(支付/风控)、ERP/CRM、供应链管理等需要深度贴合业务的场景;不适用于纯CRUD、业务简单(如静态展示系统)或性能极致优先(如高频交易底层)的场景。
2. 解决的核心问题:
- 业务与代码“脱节”:避免技术框架主导设计,让代码结构直接映射业务逻辑,业务人员也能看懂;
- 复杂业务一致性难保障:通过聚合根、边界隔离,确保核心规则(如订单退款条件)不被绕过;
- 系统耦合高、难扩展:通过领域服务、事件解耦,业务变更时(如新增退款类型)不用牵一发而动全身;
- 需求沟通成本高:用统一的领域概念(实体/值对象)对齐技术与业务人员的认知。
3. AI Agent项目中DDD实现的核心步骤
-
领域建模(核心):
拆解AI Agent业务核心概念——识别聚合根(如Agent、Task、Conversation)、实体(如ToolCall、Message)、值对象(如AgentId、TaskConfig、ModelParam),明确聚合边界(如Agent包含ToolCall/Message,Task关联AgentId)。 -
封装领域行为:
在聚合根中封装核心业务逻辑(如Agent.executeTask():调度工具、生成响应;Task.updateStatus():维护任务生命周期规则;Conversation.appendMessage():保证消息时序一致性)。 -
抽象跨域逻辑:
用领域服务处理跨聚合/实体的逻辑(如AgentTaskDispatcher:协调Agent与Task的关联;ToolInvokeService:统一处理工具调用的权限、参数校验)。 -
解耦外部依赖:
定义仓储接口(如AgentRepository、TaskRepository)隔离数据访问,通过领域事件(如TaskCompletedEvent、ToolInvokeFailedEvent)解耦Agent执行与外部通知(如前端推送、日志记录)。 -
分层落地:
应用层(如AgentApplicationService)接收请求,协调领域对象完成业务;基础设施层实现仓储(对接数据库/缓存)、外部服务适配(如LLM接口、工具API封装),不侵入领域逻辑。 -
迭代优化:
基于业务反馈调整领域模型(如新增AgentSkill聚合、扩展ModelParam值对象字段),确保模型始终贴合Agent的核心能力(如多轮对话、工具链调度、任务拆解)。
4.DDD领域识别与划分核心思路
一般会和项目结合起来问,建议把项目丢给豆包。
5.DDD架构在项目应用中的真实收益
DDD的核心收益是让系统“贴合业务、易维护、能扩展”,而非纯技术优化,以下是落地后可感知的真实价值(结合电商、AI Agent、金融等实际场景):
1. 业务与代码对齐,沟通&迭代效率翻倍
- 真实价值:用“实体、值对象、聚合根”等统一概念,对齐业务人员与技术人员的认知,避免“需求理解偏差”;代码结构直接映射业务逻辑(如订单聚合对应订单业务),新同事接手或业务迭代时,不用先拆技术框架,直接按业务逻辑找代码。
- 落地场景:电商退款需求变更(新增“已发货订单扣运费”规则),技术人员直接定位
Refund聚合根和RefundService领域服务,修改核心逻辑即可,无需改动其他模块;业务人员也能通过“退款单聚合”理解代码设计。
2. 复杂业务一致性有保障,减少线上故障
- 真实价值:通过“聚合边界+聚合根管控”,把核心业务规则(如订单退款条件、库存扣减限制)封装在领域层,避免规则被绕过;跨对象/跨模块的逻辑通过领域服务、事件统一协调,减少“数据不一致”“规则漏校验”的线上问题。
- 落地场景:AI Agent项目中,“任务执行必须关联已配置的工具”规则,封装在
Task聚合根的execute()方法中,外部无法直接调用工具API,确保所有任务执行都经过权限、参数校验,避免非法工具调用导致的故障。
3. 低耦合+边界清晰,系统抗变更能力强
- 真实价值:聚合边界隔离了不同业务模块(如订单聚合与库存聚合、Agent聚合与Task聚合),业务变更时(如新增退款类型、扩展Agent技能),仅修改对应领域代码,不会“牵一发而动全身”;领域间通过ID、事件解耦,技术选型可灵活替换(如数据库从MySQL迁PostgreSQL,仅改仓储实现,不影响领域逻辑)。
- 落地场景:金融支付系统新增“数字人民币支付”方式,仅需扩展
PaymentType值对象和PaymentService领域服务,订单、退款等其他领域无需改动;后续替换支付网关,也只需修改基础设施层的支付适配类。
4. 代码复用率提升,减少重复开发
- 真实价值:领域层的核心逻辑(如
Money值对象的计算、Address的校验、Agent的工具调度逻辑)可跨模块复用,避免不同业务场景重复写相同规则;领域服务封装跨聚合逻辑(如“订单支付后扣库存”),无需在多个应用服务中重复编码。 - 落地场景:电商系统中,
Money值对象的“币种校验、折扣计算”方法,可复用在订单创建、退款计算、优惠券抵扣等多个场景;AI Agent的ToolInvokeService领域服务,统一处理工具调用的超时重试、参数格式化,复用在任务执行、多轮对话工具调度等场景。
5. 长期演进成本降低,支撑系统规模化
- 真实价值:DDD的“领域驱动”设计,让系统核心能力(如订单管理、Agent调度)独立于技术框架,随着业务规模化(如电商从单品类到多品类、Agent从单任务到多租户),可通过拆分聚合、扩展领域服务平滑扩容,无需重构核心架构。
- 落地场景:电商系统从“单店”扩展到“多租户平台”,仅需新增
Tenant聚合根,在原有订单、库存聚合中关联TenantId,即可实现租户隔离;AI Agent项目新增“多租户权限控制”,仅需在Agent聚合中添加TenantConfig值对象,不影响任务执行、工具调用等核心逻辑。
关键提醒:DDD不是“银弹” 以上收益需在“业务复杂、规则多变、生命周期长”的系统中才能充分体现(如电商、金融、ERP、AI Agent);若为纯CRUD、业务简单的系统(如静态展示、简单表单提交),DDD的设计成本可能高于收益,无需强行落地。
65万+

被折叠的 条评论
为什么被折叠?



