第 3 章:战术设计
学习目标
- 掌握战术设计的核心构件:实体、值对象、聚合、领域服务、仓储
- 学会区分"放进实体"和"放进领域服务"
- 理解仓储(Repository)封装持久化的边界
- 避开贫血模型和上帝对象的陷阱
一、战术设计构件清单
| 构件 | 特点 | 例子 |
|---|---|---|
| 实体(Entity) | 有唯一 ID,可变 | Order、User |
| 值对象(VO) | 无 ID,不可变 | Money、Address |
| 聚合(Aggregate) | 一组实体的强一致边界 | Order + OrderItem |
| 聚合根(ARoot) | 聚合的入口,对外只暴露它 | Order |
| 领域服务(Domain Service) | 跨实体的业务规则 | PricingService |
| 仓储(Repository) | 聚合的持久化抽象 | OrderRepository |
| 工厂(Factory) | 复杂对象的创建 | OrderFactory |
| 领域事件(Event) | 已发生的事 | OrderPaidEvent |
二、实体:有 ID 的可变对象
java
public class User {
private final UserId id; // 唯一标识,终身不变
private String nickname; // 可变属性
private String avatar;
private UserStatus status;
public void changeNickname(String nickname) {
if (nickname == null || nickname.isBlank()) {
throw new IllegalArgumentException("昵称不能为空");
}
if (nickname.length() > 20) {
throw new IllegalArgumentException("昵称最长 20");
}
this.nickname = nickname;
}
}⚠️ 坑 1:用
Long id当 ID,魔法数字到处传。封装值对象UserId,所有需要 ID 的地方都强类型,避免和OrderId混用。
三、值对象:无 ID 的不可变对象
java
// 金额:用类替代 BigDecimal,防止单位混用
public final class Money {
private final BigDecimal amount;
private final String currency;
public Money(BigDecimal amount, String currency) {
this.amount = amount;
this.currency = currency;
}
public Money add(Money other) {
if (!currency.equals(other.currency)) {
throw new CurrencyMismatchException(currency, other.currency);
}
return new Money(amount.add(other.amount), currency);
}
// 省略 equals/hashCode/getter
}
// 用法
Money price = new Money(new BigDecimal("99.00"), "CNY");
Money total = price.add(new Money(new BigDecimal("8.00"), "CNY"));
// total = 107.00 CNY⚠️ 坑 2:金额用
BigDecimal但到处传原始数值,出现过"元/分"混用导致少收 100 倍的线上事故。用Money封装,加toFen()/fromFen()显式转换。
四、聚合根:数据修改的入口
聚合根 = 聚合的"门卫",外部只能通过聚合根修改内部对象:
java
public class Order {
private final OrderId id;
private OrderStatus status;
private final List<OrderItem> items = new ArrayList<>();
private final Address shippingAddress;
// 唯一修改入口
public void addItem(SkuId skuId, int quantity, Money unitPrice) {
if (status != OrderStatus.CREATED) {
throw new OrderImmutableException("订单已提交,不能改");
}
// 不暴露 items.add,只能通过这条命令
items.stream()
.filter(i -> i.skuId().equals(skuId))
.findFirst()
.ifPresentOrElse(
OrderItem::increase,
() -> items.add(new OrderItem(skuId, quantity, unitPrice))
);
}
public Money totalAmount() {
return items.stream()
.map(OrderItem::subtotal)
.reduce(Money.zero("CNY"), Money::add);
}
public List<OrderItem> items() {
return Collections.unmodifiableList(items); // 只读视图
}
}⚠️ 坑 3:把
items暴露成getItems()(可写),外部 Service 拿到 list 直接add,聚合边界形同虚设。要么不暴露,要么返回unmodifiableList。
五、领域服务:不属于任何实体的规则
java
// 跨实体的业务规则 → 领域服务
@Service
public class PricingService {
public Money calculateOrderPrice(Cart cart, Member member, Coupon coupon) {
Money subtotal = cart.items().stream()
.map(CartItem::subtotal)
.reduce(Money.zero("CNY"), Money::add);
Money memberDiscount = member.level().discount(subtotal);
Money afterMember = subtotal.subtract(memberDiscount);
Money couponDiscount = coupon != null ? coupon.apply(afterMember) : Money.zero("CNY");
return afterMember.subtract(couponDiscount);
}
}⚠️ 坑 4:把领域服务当"工具类"使用,所有计算都丢进去,实体反而干干净净。先尝试放进实体,实体内部无法表达或跨多个对象时,才用领域服务。
六、仓储:屏蔽持久化细节
java
// 领域层只关心"接口"
public interface OrderRepository {
Order findById(OrderId id);
void save(Order order);
List<Order> findByUser(UserId userId, int page, int size);
}
// 基础设施层提供实现
@Repository
public class OrderRepositoryImpl implements OrderRepository {
@Resource
private OrderJpaRepository jpaRepository;
@Override
public Order findById(OrderId id) {
return jpaRepository.findById(id.value())
.map(OrderMapper::toDomain)
.orElseThrow(() -> new OrderNotFoundException(id));
}
@Override
@Transactional
public void save(Order order) {
OrderPO po = OrderMapper.toPo(order);
jpaRepository.save(po);
}
}⚠️ 坑 5:在领域层直接
@Resource注入 JPA Repository,导致领域层依赖 Spring Data。领域层只定义接口,基础设施层实现,这是 DDD 四层架构的精髓。
七、领域事件:解耦的桥梁
java
// 1. 定义事件
public record OrderPaidEvent(OrderId orderId, Money amount, Instant paidAt) {}
// 2. 聚合内触发
public class Order {
private final List<DomainEvent> domainEvents = new ArrayList<>();
public void markPaid() {
this.status = OrderStatus.PAID;
domainEvents.add(new OrderPaidEvent(id, totalAmount(), Instant.now()));
}
public List<DomainEvent> pullEvents() {
List<DomainEvent> events = List.copyOf(domainEvents);
domainEvents.clear();
return events;
}
}
// 3. 应用服务发布
@Service
public class OrderApplicationService {
@Resource private OrderRepository orderRepository;
@Resource private ApplicationEventPublisher publisher;
@Transactional
public void pay(OrderId orderId) {
Order order = orderRepository.findById(orderId);
order.markPaid();
orderRepository.save(order);
order.pullEvents().forEach(publisher::publishEvent);
}
}本章小结
| 构件 | 判断标准 |
|---|---|
| 实体 | 有 ID,生命周期可变 |
| 值对象 | 无 ID,不可变,用值相等 |
| 聚合 | 强一致边界,聚合根统一入口 |
| 领域服务 | 跨实体规则,无状态 |
| 仓储 | 抽象持久化,接口在领域层 |
| 领域事件 | 已发生的事,异步协调 |
动手练习
- 充血改造:把
User实体加上changeNickname()和deactivate()业务方法 - 值对象:实现
Money类的multiply(BigDecimal factor)和subtract(Money) - 聚合根:给
Order增加addItem/removeItem/cancel,状态检查统一定义 - 领域事件:定义
OrderCreatedEvent并在Order构造函数里发布
下一章:第 4 章:实体与值对象 →