20.2 战术建模:实体、值对象、聚合与仓储
进入领域建模室后,阿花发现数据库表已经替业务做了决定:谁负责维护不变量,代码里反而无人回答。
战术模式的目标是让业务不变量在模型中有唯一、可执行的归属。不是每张表对应一个 Entity,也不是所有关联对象都放进一个 Aggregate。
Entity 由身份和生命周期定义
实体即使属性变化,仍被视为同一个对象。身份必须稳定:
public record TournamentId(UUID value) {
public TournamentId {
Objects.requireNonNull(value);
}
}不要用所有可变字段生成实体的 equals/hashCode,否则对象进入集合后修改字段会破坏查找。实体相等性通常基于同一类型和稳定身份。
Value Object 由值定义
值对象没有独立身份,适合表达带规则的度量和概念:
public record Capacity(int value) {
public Capacity {
if (value < 2 || value > 1024) {
throw new IllegalArgumentException("capacity must be 2..1024");
}
}
public boolean canFit(int confirmedEntries) {
return confirmedEntries < value;
}
}值对象应优先不可变,创建时就满足约束。它比到处传裸 int 更能保留单位和业务含义。
Aggregate 是一致性边界
Aggregate 是一组在一次事务中维护一致性的对象,Aggregate Root 是外部修改它们的唯一入口。
public final class Tournament {
private final TournamentId id;
private final Capacity capacity;
private TournamentStatus status;
private final Set<PlayerId> confirmed = new HashSet<>();
public RegistrationConfirmed register(PlayerId playerId) {
if (status != TournamentStatus.OPEN) {
throw new RegistrationClosed(id);
}
if (confirmed.contains(playerId)) {
throw new PlayerAlreadyRegistered(playerId);
}
if (!capacity.canFit(confirmed.size())) {
throw new TournamentFull(id);
}
confirmed.add(playerId);
return new RegistrationConfirmed(id, playerId, confirmed.size());
}
}“人数不超过容量”和“同一玩家不重复确认”由同一个根维护。外部不能取得可变集合后直接 add。
聚合要尽量小
把整个赛事、所有比赛、玩家、奖励和支付都放进一个聚合,会导致:
- 每次操作加载巨大对象图;
- 并发命令争用同一版本;
- 事务和锁范围过大;
- 无法独立扩展和演进。
聚合之间优先通过 ID 引用,并在应用层或领域服务中协调。跨聚合规则通常接受最终一致、预留或 Saga;若某不变量确实必须立即原子成立,应重新审视边界或数据所有权。
聚合边界由不变量和并发决定,不由 UI 页面或 ORM 级联方便程度决定。
Repository 面向聚合根
public interface TournamentRepository {
Optional<Tournament> findById(TournamentId id);
void save(Tournament tournament, long expectedVersion);
}仓储提供类似集合的领域接口,隐藏查询和持久化细节。它通常按聚合根定义,不为聚合内部每个实体提供可绕过根的仓储。
复杂报表和列表查询不必强行加载聚合,可以使用专门的查询模型。写模型保护规则,读模型服务检索。
Domain Service 放置无自然归属的规则
一个业务决策若涉及多个概念,却不自然属于某个实体或值对象,可以使用无状态领域服务:
public final class SeedingPolicy {
public Bracket seed(List<QualifiedPlayer> players, RankingSnapshot ranking) {
// 领域算法,不访问 HTTP 或数据库
}
}领域服务不等于所有业务逻辑的收容所。能够放进实体和值对象的行为应靠近状态;需要事务、仓储、消息和权限编排的是应用服务。
Factory 保护复杂创建
当创建聚合需要多步校验、策略或多个值对象时,Factory 可以表达“如何得到一个有效初始聚合”。简单构造不必机械增加 Factory。
聚合测试关注状态转换
Given 赛事开放且剩余 1 个名额
When 玩家 A 报名
Then 报名确认,剩余名额为 0,并产生 RegistrationConfirmed
Given 赛事已满
When 玩家 B 报名
Then 拒绝 TournamentFull,状态不改变,不产生成功事件测试应覆盖不变量、幂等/重复命令、并发版本和失败原子性,而不只覆盖 getter。
下一课处理上下文之间的模型翻译、领域事件与集成事件,以及模型如何在生产反馈中演进。
参考资料
- Martin Fowler, Value Object
- Martin Fowler, DDD Aggregate
- Martin Fowler, Repository