我正在练习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
我想在这里开发一个轮询系统,所以我认为我们可以将其称为轮询域。我们有会员,类别,品牌,型号和投票。每个成员只能对模型进行一次投票,每个模型都有一个品牌和一个类别。例如,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
和
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
份。让我们坚持使用ModelAddtion
。
如果我想检查categoryId和brandId是否有效,我必须调用CategoryRepository.isCategoryWithIdAdded(categoryId)
和BrandRepository.isBrandWithIdAdded(brandId)
方法。是否允许从ModelRepository
访问这些方法?我应该注入容器并使用getCategoryRepository() -> CategoryRepository
和getBrandRepository() -> BrandRepository
方法吗?如何通过DDD正确解决这个问题?
更新
如果您确实需要外键约束并且您的数据库引擎不具备此功能,您将如何在域中解决此验证?
答案 0 :(得分:3)
计算机科学中存在两个难题:缓存失效,命名事物,关闭一个错误以及归因于引用......我会再次进入。
存储库,如DDD本身无处不在的语言中所使用的,通常不代表您在此处表达的内容。
Eric Evans写道(蓝皮书,第6章)。另一个暴露技术复杂性的转变可以淹没域设计,即转换到存储和从存储转换。此转换是另一个域设计构造REPOSITORY
的责任这个想法是隐藏客户端的所有内部工作,因此无论数据存储在对象数据库中,存储在关系数据库中,还是仅存储在内存中,客户端代码都是相同的。
换句话说,存储库的接口定义了由持久性组件实现的契约。
MemberRepository
MemberRepository(eventBus, eventStorage)
registerMember(id, uri)
-> MemberRegistered(id, uri, date)
-> MemberRegistrationFailed //when id or uri is not unique
另一方面,这看起来像是对您的域模型的修改。 " registerUser"具有命令的语义,MemberRegistered,MemberRegistrationFailed看起来像域事件,强烈暗示这个东西是聚合,也就是说保护域内特定不变量的实体。
命名您的一个聚合" Repository"会让所有人感到困惑。聚合的名称应该来自有界语境的普遍存在的语言,而不是我们用来描述实现的模式语言。
如果我想检查categoryId和brandId是否有效,我必须调用
CategoryRepository.isCategoryWithIdAdded(categoryId)
和BrandRepository.isBrandWithIdAdded(brandId)
方法。是否允许从ModelRepository访问这些方法?
如上所述,假设CategoryRepository
,BrandRepository
和ModelRepository
都是聚合,答案是否,否和否。
否:如果您已正确建模了域,那么确保更改与业务不变量一致所需的所有状态都应包含在正在更改的聚合的边界内。例如,考虑在此线程中添加模型意味着什么,而在 线程中删除模型所需的品牌。这些是单独的事务,这意味着模型不能保持一致性不变。
否:如果检查它的动机是通过清理输入来减少错误的发生率,那么逻辑真的属于应用程序组件,而不是域模型。域模型的责任是确保命令的参数引起对模型状态的有效改变;应用程序负责确保传递正确的参数。完整性检查属于域模型
之外那说
否:域模型中的聚合不应该直接相互访问;而不是传入聚合,传递代表域模型需要运行的查询的域服务。
Model.addModel(brandId, brandLookupService) {
if (brandLookupService.isValid(brandId)) {
// ...
}
}
这个额外的间接位消除了在给定事务中更改聚合的任何歧义。 BrandLookupService
本身就可以从BrandRepository中加载Brand的只读表示。
当然,即使模型引用品牌,它仍然无法解决品牌可能发生变化的担忧。换句话说,由于绘制了交易边界,因此本设计中存在潜在的数据竞争。
如果您确实需要外键约束并且您的数据库引擎不具备此功能,您将如何在域中解决此验证?
两个选项:
1)重新绘制聚合边界。
如果您需要域模型强制执行的外键约束,那么它不是" foreign"键;它是包含两个状态位的聚合的本地密钥。
2)更改要求
我认为,在this talk中,Udi Dahan指出,有时候业务(当前)运行的方式根本无法正常扩展,业务本身可能需要更改以获得结果他们想要。我不确定聚合在这里是什么。
让我们以不同的方式尝试 - 我们如何实现这一点?
例如inf3rno可以对鞋子进行投票:Mizuno - Wave Rider 19 with 10,因为他非常喜欢它。
在上面的设计中,您使用了VoteRepository
来执行此操作。我们不想使用" repository",因为这个名词并不是来自无所不在的语言。您之前称之为轮询域,因此请尝试Poll
作为实体。 Poll
实体将负责执行“一人一票”#34;不变的。
所以它会看起来像
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);
}
}
记录投票的代码看起来像
// 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);
答案 1 :(得分:1)
从纯粹的角度来看,您不需要访问2个存储库。我说纯粹是因为可能需要一段时间才能理解域的哪些缺失位将简化这一点。
从我的头脑中,我会问自己以下几点:
isCategoryWithIdAdded
和isBrandWithIdAdded
)。根据您的存储引擎,您可以强制执行此操作(例如,所需的外键)。我认为这将是我的方法,因为从性能的角度来看它也更快。只是一个评论......在你的域名中有一些名为Model的东西是如此令人困惑,因为它是DDD词汇的一部分。 = d