我是 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 存盤庫介面。
我的應用層包含:
用戶服務:
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 的建議,不要太擔心。我將邏輯用于檢查應用程式服務中的重復項,因為它可以生成可讀代碼并按預期作業。
uj5u.com熱心網友回復:
這個解決方案感覺不對,因為防止重復用戶名是域邏輯,不應該在應用層。
至少有兩個常見的答案。
一種是接受“領域層”與“應用層”是一種有點人為的區別,并且不要過于關注分支邏輯發生的位置。我們正在嘗試發布滿足業務需求的代碼;我們沒有獲得風格的獎勵積分。
另一種方法是將檢索某些資訊的行為與決定如何處理它的行為分開。
考慮:
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::runor的東西CreateUser::result。
您可以通過多種方式實際執行實作;您可以從域代碼回傳一個有區別的聯合,然后在應用程式代碼中使用一些 switch 陳述句,或者您可以回傳一個介面,根據域邏輯的結果具有不同的實作,或者....
這在很大程度上取決于領域層“純”的重要性,您愿意承擔多少額外的復雜性來獲得它,包括您對測驗設計的感受,您的開發團隊對哪些習慣用法感到滿意,以及很快。
應該注意的是,可以合理地爭論“唯一”的定義本身屬于領域,而不是應用程式。
在這種情況下,設計基本相同,只是我們沒有將“答案”傳遞給域代碼,而是傳遞了提出問題的能力。
select User.create(
userId,
username,
SomeServiceWrapperAround(
userRepository::findByName
))
或者我們定義一個協議,其中域代碼回傳問題的表示,應用程式代碼執行 I/O 并將答案的表示傳遞回域模型。
(并且,根據我的經驗,開始質疑所有這些儀式是否真的讓設計“更好”)
uj5u.com熱心網友回復:
這個解決方案感覺不對,因為防止重復用戶名是域邏輯,不應該在應用層。
正確的。
我也嘗試創建一個域服務來檢查重復的用戶名,但這也感覺不對,因為應用程式服務可以訪問存盤庫并且可以自己執行此操作。
是的,應用程式服務可以自己完成作業,但是您已經有意識地決定為那些“棘手”位創建一個專用層,在這些位中域聚合無法自行完成作業并且您不想泄露領域知識進入應用服務。
這沒有什么問題。只要事情看起來有點棘手,就不要讓“域服務”成為您的默認方法。你最終會得到一個貧乏的域模型和所有的邏輯都位于域服務中。但是,有時,域服務是唯一實用的解決方案,當其他所有方法都失敗時,可以隨意使用它們。
另一種選擇是搜索“域事件”。在我看來,它們保持更好的分離,但需要更多的努力和管道。
這是一個 C# 介紹,但同樣適用于 Java 世界。
https://docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/domain-events-design-implementation
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/492113.html
