我以StackOverflow为例,因为很明显你知道那个网站,我的真实用例非常接近。
因此,让我们想象一个简化的So域描述:
- 有用户
- 用户可以创建新问题
- 用户可以创建这些问题的答案
- 用户可以编辑自己的问题和答案
- 如果用户的信誉超过1000,则用户可以编辑其他用户的问题(随机取该阈值)
最后一条大胆的规则对我来说很重要
我对AggregateRoot的理解是,它应该包含用于决定接受或拒绝命令的状态,而不应该查询数据库。它保证了应用程序的一致性。它应该只监听它发出的事件来更新它的状态。
现在我认为SO域有一个聚合根,叫做Question。这个问题将处理以下命令:
- CreateAnswer
- 编辑问题
问题是,当EditQuestion被触发时,Question AggregateRoot将如何决定是接受还是拒绝该命令?因为如果你还记得,如果你试图编辑另一个用户的问题,如果你有<1000声誉。
对我来说,Question AR维护所有用户声誉的列表,以便知道如何执行该命令,这似乎没有什么意义。
问题是,当我试图对我的域进行建模时,这个建模问题一次又一次地出现,最终我总是得到一个巨大的AggregateRoot
有人能告诉我我缺了什么并帮我解决这个问题吗?感谢
这个问题似乎意味着我们不应该把授权系统放在领域模型中。我同意这对于基于角色的身份验证这样的事情来说可能是实用的。然而,对我来说,"除非用户有足够的声誉,否则他们不能编辑"实际上是一条SO商业规则,那么它怎么可能在域外呢?
重要提示:在回答时,请将您视为业务专家。作为一个用户,您知道StackOverflow,并且可以自己猜测SO约束是什么。即使你对它们的看法是错误的,也没什么大不了的:只要为你错误的业务限制提出一个建议我就可以了!!!
这不是我第一次问这种问题,结果总是没有答案,只是无休止的讨论。我想知道的是,如果你必须建立这个网站,你将如何建模StackOverflow,重点是关于编辑最低信誉的商业规则。
嗯,事情很简单IMO(仅在这个SO场景中)。I就是这样做的(显然,其他开发者可能有不同的方法):
你把这个问题很好地加上了"cqrs"。在EditQuestion处理程序中,我会使用域服务(从CQRS的角度来看的查询),它会检查某个使用是否具有所需的点,然后返回true/false。类似这样的东西(或多或少的伪代码)
public class CanUserEditQuestionService
{
//constructor with deps\
public bool Handle(CanUserEditQuestion input)
{
//query the read model, maybe a query object to get us the rep of the user
var rep=getReputation.Get(input.UserId);
//we can have a dependency here which tell us the number of points required for a specific permission
return(rep>=1000);
}
}
如果查询返回true,则处理程序将对Question实体(即问题)执行更改。ChangeText()或smth(我认为SO采用了事件源方法)。
这里有一个概念"问题"的简单用例,它的命令行为"编辑",以及决定谁可以做什么的业务规则。问题是,1000代表规则从来都不是问题概念定义的一部分,因此它不属于该集合,即如何编辑问题。然而,它是用例本身的一部分,也是应用程序服务的一部分。
我相信你会问我:"如果域查询使用的读取模型在命令模型后面呢?"。在这种情况下,这无关紧要,延迟可能最多以秒为单位。这里最重要的一点是:业务规则不是Question聚合的一部分,所以它不关心是否立即一致。
另一件事是,用户代表总是一个不同于问题的概念,所以处理代表永远不应该是问题聚合的一部分。但它是应用程序服务的一部分。
如果您将应用程序视为一组使用概念(这些概念本身封装了数据和业务约束)的用例,那么很容易确定哪个是应用程序服务、聚合、域服务等。
如果您想要立即一致性,则可以使用稍微不同的方法
public class UserCommandHandler
{
public void Handle(EditQuestion command)
{
// (start transaction)
var user = userRepository.Get(command.UserId);
if (user.Reputation < 1000)
// reject command here
var edit = user.EditedQuestion(...); // or just "new QuestionEdit(...)"
questionEditRepository.Add(edit);
// (commit, saving the QuestionEdited event)
}
}
由于我们使用CQRS,页面上反映的问题状态不会包含在Question
聚合中,而是一系列QuestionEdited
事件的投影,这些事件会随着时间的推移而被侦听和累积。