我是 DDD 新手,并且遇到了具有独特约束的问题。我遇到一个问题,聚合根上的字段之一(值对象)不能是重复值。例如,用户有唯一的用户名。
我的域层包含:
public class User {
private UUID id;
private Username username;
private User(UUID id, Username username) {
this.id = id;
this.username = username;
}
public void rename(Username username) {
if (!username.equals(username)) {
this.username = username;
EventBus.raise(new UserRenamedEvent(username));
}
}
public UUID getId() {
return id;
}
public Username getUsername() {
return username;
}
public static User create(UUID id, Username username) {
User user = new User(id, username);
EventBus.raise(new UserCreatedEvent(user));
return user;
}
}
用户名:
public record Username(String name) {
// Validation on username
}
以及一个简单的 CRUD 存储库接口(interface),在基础设施层实现。
我的应用程序层包含:
用户服务:
public interface UserService {
UUID createUser(Username username);
// Get, update and delete functions...
}
和 UserServiceImpl:
public class UserServiceImpl implements UserService {
public UUID createUser(Username username) {
// Repository returns an Optional<User>
if (userRepository.findByName(username).isPresent()) {
throw new DuplicateUsernameException();
}
User user = User.create(UUID.randomUUID(), username);
repsitory.save(user);
return user.getId();
}
}
这个解决方案感觉不对,因为防止重复的用户名是域逻辑,不应该在应用程序层中。我还尝试创建一个域服务来检查重复的用户名,但这也感觉不对,因为应用程序服务可以访问存储库并且可以自行执行此操作。
如果用户是聚合的一部分,我会在聚合根级别进行验证,但由于用户是聚合,这是不可能的。我真的很想知道验证唯一约束的最佳位置。
编辑:我决定接受 VoiceOfUnreasons 的建议,不要太担心。我将逻辑用于检查应用程序服务中的重复项,因为它可以生成可读的代码并按预期工作。
最佳答案
This solution doesn't feel right, as preventing duplicate usernames is domain logic, and should not be in the application layer.
至少有两个常见的答案。
其中之一是接受“域层”与“应用程序层”是一种人为的区别,并且不要太关注分支逻辑发生的位置。我们正在尝试发布满足业务需求的代码;我们不会因风格而获得奖励积分。
另一种方法是将检索某些信息的行为与决定如何处理信息的行为分开。
考虑:
public UUID createUser(Username username) {
return createUser(
UUID.randomUUID(),
username,
userRepository.findByName(username).isPresent()
);
}
UUID createUser(UUID userId, Username username, boolean isPresent) {
if (isPresent) {
throw new DuplicateUsernameException();
}
User user = User.create(userId, username);
repository.save(user);
return user.getId();
}
我希望在这里澄清的是,我们实际上有两种不同的问题需要解决。首先,我们希望将 I/O 副作用与逻辑分开。第二个是我们的逻辑有两种不同的结果,这些结果映射到不同的副作用。
// warning: pseudo code ahead
select User.create(userId, username, isPresent)
case User(u):
repository.save(u)
return u.getId()
case Duplicate:
throw new DuplicateUsernameException()
实际上,User::create
并不返回 User
,而是返回某种 Result
,它是所有的抽象创建操作的不同可能结果。我们需要进程的语义,而不是工厂。
因此,我们可能不会使用 User::create
拼写,而是使用 CreateUser::run
或 CreateUser::result
之类的拼写>.
您可以通过多种方式实际执行实现;您可以从域代码返回一个可区分的联合,然后在应用程序代码中使用一些 switch 语句,或者您可以返回一个接口(interface),根据域逻辑的结果具有不同的实现,或者......
这很大程度上取决于域层“纯粹”的重要性,您愿意承担多少额外的复杂性来获得它,包括您对测试设计的感受,您的开发团队喜欢哪些习惯用法与,等等。
应该指出的是,我们可以合理地认为“唯一”的定义本身属于领域,而不是应用程序。
在这种情况下,设计基本上是相同的,只是我们没有将“答案”传递给域代码,而是传递了提出问题的功能。
select User.create(
userId,
username,
SomeServiceWrapperAround(
userRepository::findByName
))
或者我们定义一个协议(protocol),其中域代码返回问题的表示,应用程序代码执行 I/O 并将答案的表示传递回域模型。
(根据我的经验,开始质疑所有这些仪式是否真的让设计“更好”)
关于java - 聚合根中的重复值应该在领域层还是应用层进行检查?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/72629588/