inf*_*rno 1 domain-driven-design ddd-repositories
我正在练习 DDD,我有一个非常简单的例子,目前看起来像这样:
Polling
getEventBus() -> Bus
getEventStorage() -> Storage
getMemberRepository() -> MemberRepository
getCategoryRepository() -> CategoryRepository
getBrandRepository() -> BrandRepository
getModelRepository() -> ModelRepository
getVoteRepository() -> VoteRepository
MemberRepository
MemberRepository(eventBus, eventStorage)
registerMember(id, uri)
-> MemberRegistered(id, uri, date)
-> MemberRegistrationFailed //when id or uri is not unique
isMemberWithIdRegistered(id)
isMemberWithUriRegistered(uri)
CategoryRepository
CategoryRepository(eventBus, eventStorage) {
addCategory(id, name)
-> CategoryAdded(id, name, date)
-> CategoryAdditionFailed //when id or name is not unique
isCategoryWithIdAdded(id)
isCategoryWithNameAdded(name)
};
BrandRepository
CategoryRepository(eventBus, eventStorage) {
addBrand(id, name)
-> BrandAdded(id, name, date)
-> BrandAdditionFailed //when id or name is not unique
isBrandWithIdAdded(id)
isBrandWithNameAdded(name)
};
ModelRepository
ModelRepository(eventBus, eventStorage)
addModel(id, name, categoryId, brandId)
-> ModelAdded(id, name, categoryId, brandId, date)
-> ModelAdditionFailed //when id or name is not unique and when category or brand is not recognized
isModelWithIdAdded(id)
isModelWithNameAdded(name)
VoteRepository
VoteRepository(eventBus, eventStorage)
addVote(memberId, modelId, vote, uri)
-> MemberVoted(memberId, modelId, vote, uri, date)
-> VoteFailed //when the member already voted on the actual model and when memberId or modelId is not recognized
Run Code Online (Sandbox Code Playgroud)
我想在这里开发一个轮询系统,所以我认为我们可以将其称为轮询域。我们有会员、品类、品牌、型号和投票。每个成员只能对一个模型投票一次,每个模型都有一个品牌和一个类别。例如inf3rno可以在投票Shoe:Mizuno-Wave Rider 19有10,因为他真的很喜欢它。
我的问题是
addModel(id, name, categoryId, brandId)
-> ModelAdded(id, name, categoryId, brandId, date)
-> ModelAdditionFailed //when id or name is not unique and when category or brand is not recognized
Run Code Online (Sandbox Code Playgroud)
和
addVote(memberId, modelId, vote, uri)
-> MemberVoted(memberId, modelId, vote, uri, date)
-> VoteFailed //when the member already voted on the actual model and when memberId or modelId is not recognized
Run Code Online (Sandbox Code Playgroud)
部分。让我们坚持使用ModelAddtion.
如果我想检查 categoryId 和 brandId 是否有效,我必须调用CategoryRepository.isCategoryWithIdAdded(categoryId)和BrandRepository.isBrandWithIdAdded(brandId)方法。是否允许从ModelRepository? 我应该注入容器并使用getCategoryRepository() -> CategoryRepository和getBrandRepository() -> BrandRepository方法吗?如何通过 DDD 正确解决这个问题?
更新:
如果您确实需要外键约束而您的数据库引擎没有此功能,您将如何在域中解决此验证问题?
计算机科学中有 2 个难题:缓存失效、命名事物、一个错误和归因引号......我会再来的。
Repository,在 DDD 本身无处不在的语言中使用,通常并不意味着您在这里试图表达的意思。
Eric Evans 写道(蓝皮书,第 6 章)。
另一个暴露出可能淹没域设计的技术复杂性的转换是与存储的转换。这种转变是另一个领域设计结构的责任,REPOSITORY
这个想法是对客户端隐藏所有内部工作,这样无论数据是存储在对象数据库中、存储在关系数据库中还是简单地保存在内存中,客户端代码都是相同的。
换句话说,存储库的接口定义了要由持久性组件实现的契约。
MemberRepository
MemberRepository(eventBus, eventStorage)
registerMember(id, uri)
-> MemberRegistered(id, uri, date)
-> MemberRegistrationFailed //when id or uri is not unique
Run Code Online (Sandbox Code Playgroud)
另一方面,这看起来像是对域模型的修改。“registerUser”具有命令的语义,MemberRegistered、MemberRegistrationFailed看起来像域事件,这强烈暗示这个东西是一个aggregate,也就是保护域内特定不变量的实体。
将您的聚合之一命名为“存储库”会让每个人都感到困惑。聚合的名称应该真正取自有界上下文的无处不在的语言,而不是我们用来描述实现的模式语言。
如果我想检查 categoryId 和 brandId 是否有效,我必须调用
CategoryRepository.isCategoryWithIdAdded(categoryId)和BrandRepository.isBrandWithIdAdded(brandId)方法。是否允许从 ModelRepository 访问这些方法?
假设,如上述,即CategoryRepository,BrandRepository和ModelRepository均聚集体,答案是否定的,没有和没有。
否:如果您对域进行了正确建模,那么确保更改与业务不变量一致所需的所有状态都应包含在正在更改的聚合的边界内。例如,考虑在该线程中添加模型意味着什么,而该模型需要的品牌正在该线程中被删除。这些是单独的事务,这意味着模型无法保持一致性不变。
否:如果检查的动机是通过清理输入来减少错误的发生率,那么该逻辑确实属于应用程序组件,而不是域模型。域模型有责任确保命令的参数引起模型状态的有效更改;确保传递正确的参数是应用程序的责任。健全性检查属于域模型之外
那说
否:域模型中的聚合不应直接相互访问;不是传入聚合,而是传入代表域模型需要运行的查询的域服务。
Model.addModel(brandId, brandLookupService) {
if (brandLookupService.isValid(brandId)) {
// ...
}
}
Run Code Online (Sandbox Code Playgroud)
这个额外的间接位消除了在给定事务中更改哪个聚合的任何歧义。在BrandLookupService幕后,它本身很可能正在加载 BrandRepository 中品牌的只读表示。
当然,即使模型引用了品牌,它仍然没有解决品牌可能会发生变化的担忧。换句话说,由于绘制事务边界的位置,此设计中存在潜在的数据竞争。
如果您确实需要外键约束而您的数据库引擎没有此功能,您将如何在域中解决此验证问题?
两种选择:
1) 重绘聚合边界。
如果您需要域模型强制执行的外键约束,那么它不是“外”键;它是包含两个状态位的聚合的本地密钥。
2) 更改要求
我认为在本次演讲中,Udi Dahan指出,有时业务(当前)的运行方式根本无法正常扩展,业务本身可能需要改变才能获得他们想要的结果。
我不确定这里的聚合是什么。
让我们以不同的方式尝试 - 我们如何实现它?
比如inf3rno可以给Shoe投票:Mizuno - Wave Rider 19 with 10,因为他真的很喜欢。
在上面的设计中,您使用了 aVoteRepository来执行此操作。我们不想使用“repository”,因为该名词并非取自无处不在的语言。您之前将其称为轮询域,因此让我们尝试Poll作为实体。该Poll实体将负责执行“一人一票”不变量。
所以它看起来像
class Poll {
private PollId id;
private Map<MemberId,Vote> recordedVotes;
public void recordVote(MemberId memberId, Vote vote) {
if (recordedVotes.containsKey(memberId)) {
throw VoteFailed("This member already voted. No backsies!");
}
recordedVotes.put(memberId, vote);
}
}
Run Code Online (Sandbox Code Playgroud)
记录投票的代码看起来像
// Vote is just a value type, we can create one whenever we need to
Vote vote = Vote.create(10);
// entity ids are also value types that we can create whenever
// we want. In a real program, we've probably done both of these
// lookups already; Poll and Member are entities, which implies that
// their identity is immutable - we don't need to worry that
// MemberId 3a7fdc5e-36d4-45e2-b21c-942a4f68e35d has been assigned
// to a different member.
PollId pollId = PollId.for("Mizuno - WaveRider 19")
MemberId memberId = MemberId.for("inf3rno");
Poll thePoll = pollRepository.get(pollId);
thePoll.recordVote(memberId, vote);
pollRepository.save(thePoll);
Run Code Online (Sandbox Code Playgroud)