250|DDD 战术设计:实体、值对象、聚合、领域服务、仓储和领域事件

工程效能、测试与DDD约 12 分钟阅读6,617 字符
知识库文档 251 / 251

阅读时间:约 45 分钟 前置知识:建议先阅读 249|DDD 战略设计 了解领域、子域、限界上下文等概念。 本文目标:掌握 DDD 战术设计的六大核心模式,能在实际项目中落地充血领域模型。

一、从战略进入战术

文章配图
文章配图

上一篇文章我们讲完了 DDD 战略设计——你划分了领域和子域,画了限界上下文和上下文映射图。现在你坐在工位上,打开 IDE,面对着空白的 Java 类文件。问题来了:代码怎么写?哪些类放在一起?"订单"这个类应该有哪些字段?库存扣减逻辑放在哪里?

这就进入了 DDD 战术设计(Tactical Design)的范畴。如果战略设计回答的是"系统长什么样",战术设计回答的就是"代码怎么写"。

Evans 在《领域驱动设计》中给出了六种核心战术模式:

code
┌─────────────────────────────────────────────────────────┐
│              DDD 战术设计六大模式                          │
│                                                         │
│   实体(Entity)     聚合(Aggregate)     仓储(Repository)    │
│   值对象(VO)       领域服务(Domain Service)               │
│                    领域事件(Domain Event)                  │
└─────────────────────────────────────────────────────────┘

它们之间的关系不是平行的,而是有层次和协作关系的。我们先从最基础的两种模式讲起:实体和值对象。它们是领域模型的"原子"构件,理解了它们,聚合、仓储、领域事件才能水到渠成。

二、实体(Entity):有身份的东西

2.1 什么是实体

实体的核心特征:拥有唯一标识(Identity),并通过这个标识来追踪其生命周期。

想象一个电商系统。用户下单后,订单的状态会变化:已创建→已支付→已发货→已签收。无论订单的金额被修改了多少次、收货地址变了几次,只要 OrderId 还是 "ORD-2026-00123",它就是同一个订单。这种**同一性(Sameness)**就是实体的本质。

java
// 实体:Order
public class Order {
    private OrderId id;          // 唯一标识——这是实体的"身份证"
    private OrderStatus status;  // 可变的
    private Money totalAmount;    // 可变的
    private Address shippingAddress; // 可变的
    
    // 实体通过标识判断相等
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Order)) return false;
        Order order = (Order) o;
        return id.equals(order.id);  // 只比 ID,不管其他字段
    }
}

判断实体 vs 值对象的经典问题:问自己——如果这个对象的"金额"从 100 变成 200,"地址"从北京变成上海,它还是它吗?

如果答案是"是"→ 实体;如果答案是"不是,它完全变了一个东西"→ 值对象。

2.2 实体的四个特征

特征 说明 反模式
唯一标识 每个实体有唯一 ID,贯穿整个生命周期 用数据库自增 ID 作为领域标识(应使用业务主键或 UUID)
可变性 实体的属性可以改变,但标识不变 把所有属性做成可变的(应区分可变属性和不变属性)
生命周期 实体经历创建、状态变迁、最终归档或删除 在实体中写 CRUD 方法(实体的方法应体现业务行为)
连续性 同一实体在整个生命周期中可以"被认出" 状态变迁丢失中间状态(使用领域事件记录)

2.3 标识生成策略

实体标识的生成有四种常见策略:

  1. 用户提供:如用户名、邮箱。简单但依赖外部。
  2. 程序生成:UUID、Snowflake。最常用的策略。
  3. 持久化机制生成:数据库自增 ID。不推荐——应用层在持久化之前拿不到 ID,无法完成业务操作。
  4. 其他限界上下文提供:通过防腐层适配外部 ID。

实践中最推荐的是策略 2:在创建实体时就分配 ID,不依赖持久化层。

java
// 推荐:构造时即分配标识
public class Order {
    private OrderId id;
    
    private Order() {} // ORM 用
    
    public static Order create(CustomerId customerId, List<OrderItem> items) {
        Order order = new Order();
        order.id = OrderId.generate();  // UUID 或雪花 ID
        order.customerId = customerId;
        order.items = new ArrayList<>(items);
        order.status = OrderStatus.CREATED;
        order.createdAt = Instant.now();
        return order;
    }
}

三、值对象(Value Object):描述性、无身份

3.1 什么是值对象

值对象的核心特征:没有唯一标识,通过其属性值来定义相等性,并且是不可变的(Immutable)。

继续用电商的例子。订单金额 new Money(100, "CNY") 就是一个值对象。它没有独立的"身份"——两个 new Money(100, "CNY") 是完全等价的。钱本身不需要一个 UUID。

java
// 值对象:Money
public final class Money {
    private final BigDecimal amount;
    private final Currency currency;
    
    public Money(BigDecimal amount, Currency currency) {
        this.amount = amount.setScale(2, RoundingMode.HALF_UP);
        this.currency = Objects.requireNonNull(currency);
    }
    
    // 值对象通过所有属性判断相等
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Money)) return false;
        Money money = (Money) o;
        return amount.compareTo(money.amount) == 0 
            && currency.equals(money.currency);
    }
    
    // 值对象不可变:add 返回新实例而非修改自身
    public Money add(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new IllegalArgumentException("Cannot add different currencies");
        }
        return new Money(this.amount.add(other.amount), this.currency);
    }
    
    public Money multiply(int factor) {
        return new Money(this.amount.multiply(BigDecimal.valueOf(factor)), this.currency);
    }
}

3.2 值对象 = 不可变 + 整体替换 + 值相等

文章配图
文章配图

这个公式值得背下来:

原则 含义 为什么
不可变 所有字段 final,修改返回新对象 避免副作用,安全共享
整体替换 要改就整个替换 与不可变性配套
值相等 equals 比较所有属性 因为没有标识

3.3 为什么值对象让代码更好

例1:告别基本类型偏执(Primitive Obsession)

java
// ❌ 错误:用 String 表示邮箱
public class Customer {
    private String email;  
    // 问题:这个 String 可能是"abc"、空字符串、非法格式……到处要写校验
}

// ✅ 正确:封装为值对象
public final class Email {
    private final String value;
    
    public Email(String value) {
        if (value == null || !value.matches("^[\\w.-]+@[\\w.-]+\\.[a-zA-Z]{2,}$")) {
            throw new IllegalArgumentException("Invalid email: " + value);
        }
        this.value = value;
    }
    
    public String getDomain() {
        return value.substring(value.indexOf('@') + 1);
    }
    // 校验逻辑在这里一处,所有使用者自动受益
}

public class Customer {
    private Email email; // 类型即文档,类型即约束
}

例2:PhoneNumber、Address、Quantity、Weight、Temperature 都应该是值对象。 任何你发现自己在多个地方重复写"if (amount < 0) throw..."的地方,都在暗示你该提取值对象了。

四、聚合(Aggregate):一致性边界

4.1 两根金科玉律

如果说实体和值对象是砖块,聚合就是把砖块砌成墙的那条灰缝——它定义了边界。

聚合由**聚合根(Aggregate Root)**统领,内部封装一组实体和值对象,保证一组业务不变量(Invariant)随时成立。

聚合设计有两条核心规则,被称为"聚合设计的两根金科玉律":

规则1(外部引用):外部对象只能引用聚合根,不能直接引用聚合内部的实体。 规则2(内部一致性):聚合根负责保证内部所有不变量——对聚合的任何修改,必须通过聚合根这一扇门。

4.2 第一个规则:通过 ID 引用,而非直接引用

java
// ❌ 错误:Order 直接持有 Product 实体的引用
public class Order {
    private Product product;  // 坏了!跨聚合直接引用
}

// ✅ 正确:Order 只持有 ProductId(值对象)
public class Order {
    private ProductId productId;  // 通过 ID 引用
}

为什么?因为 Product 和 Order 属于不同的聚合。如果 Order 直接持有一个 Product 引用,你可能会在 OrderRepository 中通过 order.getProduct().getName() 去懒加载 Product,导致:

  • 事务边界模糊(到底该不该一起持久化 Product?)
  • 多个聚合被锁在同一事务中,影响并发
  • 聚合内部修改时不知道哪些外部引用了自己

4.3 第二个规则:聚合根是唯一入口

java
// ✅ 聚合根控制所有内部修改
public class Order {  // <- 聚合根
    private OrderId id;
    private List<OrderItem> items;  // <- 内部实体
    private Address shippingAddress;
    private OrderStatus status;
    
    // 外部不能 order.getItems().add(item) ——破坏封装
    // 只能通过聚合根的方法:
    public void addItem(ProductId productId, Money unitPrice, int quantity) {
        // 不变量1:已支付的订单不能修改
        if (status != OrderStatus.CREATED) {
            throw new OrderAlreadyPaidException(id);
        }
        // 不变量2:同商品不能重复添加(或合并数量——看业务)
        if (items.stream().anyMatch(i -> i.getProductId().equals(productId))) {
            // 合并或拒绝,取决于业务规则
        }
        items.add(new OrderItem(productId, unitPrice, quantity));
    }
    
    public void pay() {
        // 不变量:只有已创建状态的订单能支付
        if (status != OrderStatus.CREATED) {
            throw new IllegalOrderStateTransitionException(status, OrderStatus.PAID);
        }
        this.status = OrderStatus.PAID;
    }
}

4.4 聚合的大小:宁小勿大

文章配图
文章配图

这是 DDD 中最容易犯错的点。很多人把整个订单子域做成一个聚合,包含订单、订单项、收货地址、支付信息、物流轨迹等等。结果是:

  • 一个事务锁住太多数据
  • 并发冲突严重(两个请求同时修改订单的不同行项目也会冲突)
  • 测试变得复杂

经验法则:

聚合大小 特征
太小 每个实体一个聚合——失去一致性保护
刚好 满足"一个事务只需要修改一个聚合实例"的约束
太大 多个并发修改点挤在一个聚合中,事务冲突严重

判断聚合是否过大的信号:你在写业务代码时,发现一个用例需要加载一个聚合,却只修改了它几千行代码中的一个小角落 → 聚合该拆了。

4.5 不变量 vs 最终一致性

有些业务规则必须强一致(聚合边界内),有些可以接受最终一致(跨聚合)。

code
聚合内部:强一致事务
├── 订单总金额必须等于各订单项金额之和 —— 必须强一致
├── 已支付的订单不能修改收货地址 —— 必须强一致
└── 订单创建时必须有至少一个订单项 —— 必须强一致

跨聚合:最终一致性
├── 订单支付后通知物流模块发货 —— 通过领域事件,可以异步
├── 订单取消后释放库存 —— 通过领域事件,允许短暂不一致
└── 订单金额更新后通知对账系统 —— 通过领域事件

分区划界的关键问题:如果这条规则被短暂违反,会不会造成业务损失? 如果会 → 聚合内部强一致;如果不会 → 跨聚合最终一致。

五、领域服务(Domain Service):无家可归的业务逻辑

5.1 什么时候需要领域服务

有些业务逻辑不属于任何一个实体或值对象——把它硬塞进某个实体,既别扭又破坏了单一职责。这些"无家可归"但又是纯粹领域逻辑的操作,就适合放在领域服务中。

判断标准:

特征 放在实体/值对象 放在领域服务
操作只涉及一个聚合内部 ✓ ✗
操作需要协调多个聚合 ✗ ✓
逻辑自然依附于某个实体 ✓ ✗
逻辑不依附于任何实体,但仍是领域概念 ✗ ✓
有状态 ✗ ✗(无状态)

5.2 经典场景:转账

java
// 领域服务:TransferService
// 转账涉及两个 Account 聚合,不属于任何一个 Account 实体
public class TransferService {
    
    public void transfer(AccountId from, AccountId to, Money amount) {
        Account source = accountRepository.findById(from);
        Account target = accountRepository.findById(to);
        
        source.debit(amount);   // 扣款
        target.credit(amount);  // 到账
        
        accountRepository.save(source);
        accountRepository.save(target);
    }
}

5.3 领域服务 vs 应用服务

文章配图
文章配图

这是一个高频面试题。两者的区别是:

java
// 应用服务:编排,不包含业务逻辑
@Service
public class OrderApplicationService {
    
    public void cancelOrder(OrderId orderId) {
        // 1. 事务管理(基础设施)
        // 2. 加载聚合
        Order order = orderRepository.findById(orderId);
        // 3. 调用领域逻辑(只做这一步,但不能自己写 if/else 判断)
        order.cancel();  
        // 4. 持久化
        orderRepository.save(order);
        // 5. 发布领域事件
        eventPublisher.publish(order.getEvents());
    }
}

// 领域服务:包含纯业务逻辑,不处理事务/持久化
public class PricingService {  // 领域服务
    
    public Money calculateTotalPrice(List<OrderItem> items, 
                                      DiscountPolicy policy,
                                      CouponId couponId) {
        Money subtotal = items.stream()
            .map(OrderItem::getSubtotal)
            .reduce(Money.ZERO, Money::add);
        Money afterDiscount = policy.apply(subtotal);
        Money afterCoupon = couponService.applyCoupon(afterDiscount, couponId);
        return afterCoupon;
    }
}

关键区别:应用服务是"胶水代码"——它把领域对象、仓储、事务管理粘在一起,但不包含任何业务判断。如果应用服务里有 if 语句判断业务规则,那它就在越位。

六、仓储(Repository):为聚合提供"集合"语义

6.1 仓储不是一个 DAO

DAO 是面向数据库表的数据访问对象。仓储是面向聚合的持久化抽象,它模拟了一个内存中的集合,隐藏了持久化机制的细节。

code
DAO 思维:      "给我一条 order 表的记录"
仓储思维:      "给我 ID 为 X 的 Order 聚合"

6.2 仓储的两个核心职责

java
public interface OrderRepository {
    // 1. 通过聚合根 ID 获取完整聚合
    Optional<Order> findById(OrderId id);
    
    // 2. 保存整个聚合(而非逐个实体保存)
    void save(Order order);
    
    // 3. 面向集合的查询(不是 CRUD)
    List<Order> findByCustomerId(CustomerId customerId, Pageable page);
    
    // ❌ 不要暴露:updateStatus(), deleteById(), updateAmount()
    // 这些是 DAO 思维——对聚合的修改应该通过 load→modify→save 完成
}

仓储只操作聚合根。OrderRepository 不应该有 saveOrderItem() 方法——OrderItem 是 Order 聚合的内部实体,它的持久化由 Order 聚合根全权负责。

6.3 仓储接口放在领域层,实现放在基础设施层

文章配图
文章配图

code
领域层:domain/
└── repository/
    └── OrderRepository.java    ← 接口(纯领域概念)

基础设施层:infrastructure/
└── persistence/
    └── MyBatisOrderRepository.java  ← 实现(依赖 MyBatis/JPA)

这种"依赖倒置"让领域层不依赖任何框架——你可以从 MyBatis 换到 JPA,从 MySQL 换到 MongoDB,领域代码一行不动。

七、领域事件(Domain Event):聚合的"事后通知"

7.1 领域事件是什么

领域事件是领域中已经发生的重要事情——注意"已经发生"这四个字。事件的名字总是过去式:OrderPaid、ItemShipped、AccountDebited。

java
// 领域事件
public class OrderPaidEvent implements DomainEvent {
    private final OrderId orderId;
    private final CustomerId customerId;
    private final Money paidAmount;
    private final Instant paidAt;
    
    // 事件的构造函数记录"发生了什么"——是事实,不可修改
    public OrderPaidEvent(OrderId orderId, CustomerId customerId, 
                          Money paidAmount, Instant paidAt) {
        this.orderId = orderId;
        this.customerId = customerId;
        this.paidAmount = paidAmount;
        this.paidAt = paidAt;
    }
}

7.2 在聚合中生成并发布事件

java
public class Order {
    private List<DomainEvent> events = new ArrayList<>();
    
    public void pay() {
        // 1. 先执行业务逻辑
        if (status != OrderStatus.CREATED) {
            throw new IllegalOrderStateTransitionException(status, OrderStatus.PAID);
        }
        this.status = OrderStatus.PAID;
        this.paidAt = Instant.now();
        
        // 2. 再生成领域事件
        events.add(new OrderPaidEvent(this.id, this.customerId, 
                                       this.totalAmount, this.paidAt));
    }
    
    // 应用服务在 save 后调用此方法收集事件
    public List<DomainEvent> getEvents() {
        return Collections.unmodifiableList(events);
    }
}

7.3 领域事件的三个用途

文章配图
文章配图

  1. 跨聚合的最终一致性:Order 支付后,Inventory 扣库存、Logistics 创建运单——各干各的,事件驱动。
  2. 审计与追溯:OrderPaid、OrderShipped、OrderDelivered 形成一条完整的事件流,任何时候可以重放事件回到任意历史状态。
  3. 与限界上下文集成:多个限界上下文通过领域事件松耦合通信——甚至一个上下文完全不需要知道其他上下文的存在。

Event Sourcing(事件溯源)是领域事件的进阶用法:不存储聚合的当前状态,而是存储所有事件的序列,通过重放事件来重建当前状态。这是提升到架构层面的技术模式,超出了战术设计的范畴,但在面试中经常被问到。

八、六大模式协作全景

文章配图
文章配图

我们以一个订单取消的完整流程为例,看看六大模式如何协作:

code
1. OrderApplicationService.cancelOrder(orderId)
   │
2. OrderRepository.findById(orderId)     ← 仓储
   │  返回 Order 聚合
   │
3. order.cancel()                         ← 聚合根(内部协调实体和值对象)
   │  - 检查状态不变量
   │  - 生成 OrderCancelledEvent          ← 领域事件
   │
4. OrderRepository.save(order)            ← 仓储
   │
5. eventPublisher.publish(order.getEvents())
   │  - InventoryService.onOrderCancelled()  ← 跨聚合通过领域服务
   │    inventory.releaseReserved(orderId)    ← 另一个聚合根
   │  - NotificationService.send(orderId)

在这个流程中:

  • 实体:Order、OrderItem 各自有 ID
  • 值对象:Money、Address——不可变、值比较
  • 聚合:Order 是聚合根,保证不变量
  • 领域服务:InventoryService 协调跨聚合逻辑
  • 仓储:OrderRepository 封装持久化
  • 领域事件:OrderCancelledEvent 驱动下游

九、DDD 战术设计的常见误区

这些是实践中最常见的踩坑点:

9.1 误区一:贫血模型

java
// ❌ 贫血模型——DDD 的反面教材
public class Order {
    private Long id;
    private String status;
    // getters + setters...
}

@Service
public class OrderService {
    public void cancel(Order order) {
        if (!"PAID".equals(order.getStatus())) {  // 业务逻辑泄漏到 Service
            throw new RuntimeException("...");
        }
        order.setStatus("CANCELLED");
    }
}

// ✅ 充血模型
public class Order {
    public void cancel() {
        if (status != OrderStatus.PAID) {
            throw new IllegalOrderStateTransitionException(status, OrderStatus.CANCELLED);
        }
        this.status = OrderStatus.CANCELLED;
    }
}

9.2 误区二:聚合过大或过小

  • 整个系统只有一个"用户聚合"→ 太大
  • 每个数据库表一个聚合 → 太小
  • 正确:以业务不变量为边界

9.3 误区三:用 Application Service 冒充 Domain Service

java
// ❌ 这个包含了业务逻辑 if/else,应该在 Domain Service 或实体中
public class OrderService { // 实际上是应用服务
    public void process(Order order) {
        if (order.isVIP()) {
            order.setDiscount(0.8);
        }
        if (order.getAmount() > 10000) {
            // 大额订单审计
        }
    }
}

9.4 误区四:仓储返回 DTO 而非聚合

java
// ❌ 仓储返回 PO/DTO
public interface OrderRepository {
    OrderPO findById(Long id); // PO 不是领域对象
}

// ✅ 仓储返回领域聚合
public interface OrderRepository {
    Order findById(OrderId id); // 返回完整的 Order 聚合
}

9.5 误区五:跨聚合事务

文章配图
文章配图

java
// ❌ 把两个聚合锁在一个事务里
@Transactional
public void payAndReduceInventory(...) {
    order.pay();
    inventory.reduce(...);  // 不同的聚合!
}

// ✅ 先提交 Order,再通过领域事件异步处理 Inventory
@Transactional
public void pay(OrderId id) {
    order.pay();
    orderRepository.save(order);
    eventPublisher.publish(new OrderPaidEvent(...));  // 事件驱动
}

// 另一个事务
@Transactional
public void onOrderPaid(OrderPaidEvent event) {
    inventory.reduce(event.getProductId(), event.getQuantity());
}

十、在项目中落地:一个可操作的步骤清单

如果你想在现有项目中引入 DDD 战术设计,这里有一个务实的路径:

  1. 识别实体和值对象:浏览现有代码,标记所有贫血实体和原始类型滥用 → 提取值对象(Money、Email、PhoneNumber 等最容易上手)
  2. 找出聚合边界:画出你的领域对象关系图,按"一致性边界"圈出聚合
  3. 重构为充血模型:把 Service 层的 if/else 业务判断迁入实体方法中
  4. 引入仓储接口:在领域层定义仓储接口,实现移到基础设施层
  5. 识别领域事件:对每个聚合的状态转换,判断哪些需要通知其他模块
  6. 引入领域服务:把残留的跨聚合逻辑迁入领域服务

不需要一次性完成。从值对象开始,收益最大、风险最低。


🎤 面试官视角:高频追问

Q1:实体和值对象到底怎么区分?给一个可操作的标准。

考察点:是否真正理解了"同一性"的概念,还是只会背定义。

参考回答思路:问自己一个问题——如果这个对象的所有属性都变了,它还是它吗? 如果不靠任何属性、靠一个 ID 就能认出它 → 实体。如果属性变了它就不是它了 → 值对象。

举例:一个人的姓名从"张三"改成"李四",他身份证号没变,还是同一个人 → 人是实体,身份证号是标识。一个人钱包里的 100 元变成 50 元 + 50 元的零钱,你不能说"这是原来的 100 元" → 钱是值对象。

技术层面补充:值对象推荐 @Embeddable(JPA)或直接 JSON 列存储;实体必须有独立的表或文档。

Q2:聚合设计中最难的点是什么?你怎么判断一个聚合"刚刚好"?

考察点:有没有在真实项目中被聚合设计坑过。

参考回答思路:最难点是不变量识别——什么规则必须强一致、什么可以最终一致,需要和领域专家深入讨论。很多开发者的错误是凭"数据关系"而非"业务规则"来分聚合。

判断聚合是否"刚刚好"的标准:一个典型用例是否只修改一个聚合。如果一个用例需要加载聚合 A 却只动了 A 的一个浅层属性,或者一个用例需要同时锁住 A 和 B 来保证一致性 → 聚合粒度有问题。

Q3:领域服务和 Application Service 的区别,用一个实际例子说明。

考察点:分层是否清楚,会不会把业务逻辑堆在 Application Service 里。

参考回答思路:用"计算运费"举例:

  • PricingDomainService.calculateShipping(Address, Weight, ShippingMethod) → 这是纯业务计算,不依赖基础设施,多个场景复用 → 领域服务
  • OrderApplicationService.placeOrder(OrderRequest) → 这个做了:校验输入 → 调仓储加载 → 调领域对象/领域服务 → 保存 → 发事件 → 返回结果 → 应用服务

一句话:领域服务里只有领域逻辑,没有**@Transactional**,没有**@Autowired Repository**,没有 HTTP/RPC 调用。

Q4:Repository 和 DAO 的区别是什么?为什么 DDD 要单独提 Repository?

考察点:是否理解"面向集合"和"面向表"的范式差异。

参考回答思路:

  • DAO 是数据库表的一层薄封装,方法名通常是 insert/update/delete/selectByXxx
  • Repository 假装聚合放在一个内存集合里,方法名是 find/save,表达的是"找"和"放回"的意图

根本区别:DAO 暴露了持久化机制(表/字段),Repository 隐藏了持久化机制。当你从单体拆微服务、从 MySQL 换 MongoDB 时,Repository 的调用者无需感知——这是依赖倒置带来的灵活性。

Q5:领域事件如何保证可靠投递?如果发事件失败了怎么办?

考察点:事件驱动架构的可靠性设计。

参考回答思路:这是一个重要的工程问题。几种方案:

  1. 事务发件箱(Transactional Outbox):把事件写入与聚合同一个数据库的 outbox 表,在一个本地事务中完成"改状态 + 写事件"。独立进程轮询 outbox 表发送到 MQ。这是最可靠的方案。
  2. Event Sourcing:只写事件流,当前状态是事件重放的投影——天然不丢事件。
  3. 反查 + 补偿:事件消费幂等 + 定时对账,发现不一致时补偿。

不要简单地说"加个重试就行"——如果重试之前进程重启了呢?需要有一个持久化的中间态。

Q6:你在一个已有的大型项目中推行 DDD 战术设计,第一步应该做什么?为什么?

考察点:落地经验 + 务实的工程思维。

参考回答思路:绝对不是说"全部重写"。推荐路径:

  1. 先找代码中最臭的"基本类型偏执"——String 电话、String 邮箱、BigDecimal 金额配 String 币种 → 提取为值对象。这是纯增量的改进,不破坏现有行为,风险为零。
  2. 从最重要的核心域开始识别聚合边界——不要动支撑子域和通用子域。
  3. 在新需求中应用聚合和领域事件,老代码维持现状——这就是经典的"绞杀者模式"。

关键是让团队看到收益:引入 Money 值对象后,金额计算 bug 大幅减少——有了信服力再推下一步。


📚 延伸阅读


本文是 Java 技术专家/架构师面试 250 个高频考点系列的第 250 篇,也是本系列的最后一篇。恭喜你完成了全套 250 个知识点的学习!🎉