领域驱动设计(DDD)实践疑问:用户归档业务规则应置于聚合根还是服务层?
你的这种实现方式不符合领域驱动设计(DDD)的核心原则,核心问题在于把本该属于领域层的业务逻辑泄露到了基础设施层的服务类中,接下来我会详细解释问题所在,并给出合理的重构方案。
为什么当前实现不正确?
领域逻辑泄漏,聚合根失去控制权
业务规则「用户名不符合格式则归档并删除」属于核心领域逻辑,本该由UserAggregator聚合根来封装和管控,但你把判断逻辑和后续的归档删除操作都放在了基础设施层的MyService中,导致聚合根变成了一个单纯的数据载体,失去了对自身业务规则的掌控能力,违反了DDD中聚合根封装领域状态与行为的核心原则。业务逻辑与技术细节耦合
MyService中直接操作两个仓库的持久化逻辑,把业务规则和数据库操作绑定在了一起。后续如果业务规则变化(比如修改用户名格式要求),或者持久化方式调整(比如更换数据库),你都需要修改这个服务类,不符合「关注点分离」的设计思想。测试成本高
测试这个业务规则时,你需要依赖Spring Data的仓库实现,无法单独对领域逻辑进行单元测试,增加了测试的复杂度和维护成本。
正确的处理方案
根据你的业务场景,我分两种常见情况给出重构建议:
场景1:规则在更新用户名时触发
如果这个归档删除动作是在更新用户名后触发的(即修改用户名后发现格式不符合,立即执行归档删除),可以这样重构:
1. 领域层:聚合根封装更新与校验逻辑
给UserAggregator增加用户名更新方法,把格式校验的业务逻辑封装进去,不符合规则时抛出领域异常:
public class UserAggregator { private UUID userId; private String userName; private String country; // Getter方法保留 public UUID getUserId() { return userId; } public String getUserName() { return userName; } // 封装用户名更新逻辑,包含格式校验 public void updateUserName(String newUserName) { this.userName = newUserName; if (!isUserNamePatternCorrect()) { throw new InvalidUserNamePatternException("用户名格式错误,必须以'TOTO'开头"); } } private boolean isUserNamePatternCorrect() { return this.userName.startsWith("TOTO"); } } // 自定义领域异常,明确表达业务语义 public class InvalidUserNamePatternException extends RuntimeException { public InvalidUserNamePatternException(String message) { super(message); } }
2. 应用层:创建应用服务协调流程
把原来的MyService调整为应用服务(不属于基础设施层),负责调用聚合根的方法,并处理异常触发归档删除:
@Service public class UserApplicationService { private final UserRepository userRepo; private final ArchiveUserRepository archiveUserRepo; // 优先使用构造注入,提升可测试性 public UserApplicationService(UserRepository userRepo, ArchiveUserRepository archiveUserRepo) { this.userRepo = userRepo; this.archiveUserRepo = archiveUserRepo; } public void updateUserName(UUID userId, String newUserName) { UserAggregator userAgg = userRepo.findUser(userId); if (Objects.nonNull(userAgg)) { try { userAgg.updateUserName(newUserName); userRepo.save(userAgg); // 更新成功则保存 } catch (InvalidUserNamePatternException e) { // 格式不符合,执行归档删除 userRepo.deleteUser(userAgg); archiveUserRepo.archiveUser(userAgg.getUserId()); } } } }
场景2:规则是独立的检查任务
如果这个归档删除是定时扫描或批量检查触发的(比如定期清理不符合格式的用户),可以这样处理:
1. 领域层:聚合根提供规则判断方法
给UserAggregator增加一个语义化的方法,明确表达是否需要执行归档删除:
public class UserAggregator { // 原有字段与Getter public boolean shouldBeArchivedAndDeleted() { return !isUserNamePatternCorrect(); } private boolean isUserNamePatternCorrect() { return this.userName.startsWith("TOTO"); } }
2. 应用层:应用服务负责协调执行
应用服务查询用户后,调用聚合根的判断方法,符合条件则执行仓库操作:
@Service public class UserApplicationService { private final UserRepository userRepo; private final ArchiveUserRepository archiveUserRepo; public UserApplicationService(UserRepository userRepo, ArchiveUserRepository archiveUserRepo) { this.userRepo = userRepo; this.archiveUserRepo = archiveUserRepo; } public void processInvalidFormatUsers() { // 假设仓库提供批量查询不符合格式用户的方法 List<UserAggregator> invalidUsers = userRepo.findUsersWithInvalidUserName(); for (UserAggregator user : invalidUsers) { if (user.shouldBeArchivedAndDeleted()) { userRepo.deleteUser(user); archiveUserRepo.archiveUser(user.getUserId()); } } } }
额外的优化建议
- 基础设施层只负责实现仓库接口(比如Spring Data的Repository实现),不要编写任何业务逻辑。
- 优先使用构造注入代替
@Autowired,让依赖关系更清晰,也方便单元测试时Mock依赖。 - 用领域异常表达业务规则的违反,比单纯的布尔判断更具语义性,也能避免业务逻辑被忽略。
内容的提问来源于stack exchange,提问作者BERGUIGA Mohamed Amine

