【问题标题】:How to model the StackOverflow website with DDD如何使用 DDD 对 StackOverflow 网站进行建模
【发布时间】:2015-11-22 18:46:37
【问题描述】:

我以 StackOverflow 为例,因为显然你知道那个网站,而且我的真实用例非常接近。

让我们想象一个简化的 SO 域描述:

  • 有用户
  • 用户可以创建新问题
  • 用户可以为这些问题创建答案
  • 用户可以编辑自己的问题和答案
  • 如果用户的声望超过 1000 个,则用户可以编辑其他用户的问题(随机采用该阈值)

最后一条粗体规则对我来说很重要。

我对 AggregateRoot 的理解是,它应该包含用于决定接受或拒绝命令的状态,并且它不应该查询数据库来这样做。它保证了应用程序的一致性。它应该只监听它发出的事件来更新它的状态。

现在我认为 SO 域有一个名为 Question 的聚合根。 然后该问题将处理以下命令:

  • 创建答案
  • 编辑问题

问题是,当 EditQuestion 被触发时,Question AggregateRoot 将如何决定是接受还是拒绝该命令?因为如果你记得的话,如果你有

问题 AR 维护所有用户声誉的列表,以便能够知道如何执行该命令,这对我来说似乎没有意义。

问题在于,在尝试对我的域进行建模时,我一遍又一遍地遇到这个建模问题,而且我总是以一个大的 AggregateRoot 结束

有人可以告诉我我缺少什么并帮助我解决这个问题吗?谢谢

这个question 好像在说我们不应该把授权系统放在域模型里面。我同意这对于基于角色的身份验证之类的事情可能是实用的。但是,对我来说,“除非有足够的声誉,否则用户不能编辑”确实是一个 SO 业务规则,那么它怎么可能在域之外呢?

重要提示:在回答时,请考虑您是业务专家。您作为用户了解 StackOverflow,并且可以自己猜测 SO 约束是什么。即使您对它们有误,也没什么大不了的:只需针对您错误的业务限制提出建议即可!!!

这不是我第一次问这种问题,而且总是以没有答案而结束,只是无休止的讨论。我想知道的是,如果您必须构建此站点,您将如何为 StackOverflow 建模,重点是关于要编辑的最低声誉的业务规则。

【问题讨论】:

  • 你不能总是依赖强一致性。如果用户是 1000,但在他编辑问题的同一秒内失去 50 声望,这真的很重要吗?它真正产生了什么样的业务影响?
  • 它归结为 AR 大小,但聚合越大,您的争用就越多。正如 plalx 正确地说的那样,如果代表减少真的很重要吗?因此,请检查问题编辑方法中的 user.Reputation 属性(传入完整的用户对象)。如果您对此非常担心,那么可以使用 saga 将编辑提交实现为外部事务,该事务将完成或回滚并进入某个已知状态并通知相关方
  • @g18c 甚至不确定这将如何使用最终一致性进行建模?您是否会处理UserReputationChanged 事件,然后检查用户是否在其声誉降至某个阈值以下后修改了问题。如果确实如此,那么您将对问题执行补偿操作,例如撤消编辑并通知用户?我想您还必须处理 QuestionEdited 事件,以防事件出现故障?
  • 确实如此。我同意这种思路。但是,如果企业可以接受用户点在编辑开始和结束之间下降的可能性,那么它会变得更简单
  • @SebastienLorber 对您的设计而言,一个决定性的问题是,对于拥有 >1000 rep 的用户对 Q 的并发更改,您的政策是什么?最后一个赢了?合并?乐观锁定 ?等等

标签: domain-driven-design cqrs event-sourcing


【解决方案1】:

好吧,IMO 事情很简单(仅在这种 SO 场景中)。 会这样做(显然,其他开发人员可能有不同的方法):

您用“cqrs”很好地标记了问题。在 EditQuestion 处理程序中,我将使用域服务(从 CQRS 的角度来看的查询),它将检查某个用途是否具有所需的点,然后返回真/假。像这样的东西(或多或少的伪代码)

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 实体执行更改,即 question.ChangeText() 或 smth(我认为 SO 采用事件溯源方法)。

这里有一个简单的概念“问题”用例,它的命令行为“编辑”和规定谁可以做什么的业务规则。问题是,1000 rep 规则从来不是问题概念定义的一部分,因此它不属于该聚合,即问题是如何编辑的。然而,它是用例本身的一部分,也是应用程序服务的一部分。

我相信你会问我:“如果域查询使用的读取模型落后于命令模型怎么办?”。在这种情况下,它的影响很小,延迟最多可能以秒为单位。这里的主要内容是:业务规则不是 Question 聚合的一部分,因此它不关心是否立即一致。

另一件事是,用户代表始终是与问题不同的概念,因此处理代表永远不应成为问题聚合的一部分。但它是应用服务的一部分。

如果您将应用程序视为一组使用概念(它们本身封装数据和业务约束)做事的用例,则很容易识别哪个是应用程序服务、聚合、域服务等。

【讨论】:

  • 感谢您的回答。我认为应该在域中编码的所有业务规则(我的意思是在聚合中)。所以我的理解是,当我们不需要立即一致性并且规则涉及多个概念时,业务规则可以在Aggregate之外进行编码,这是您的意思吗?
  • 业务规则是域的一部分,但聚合只关心确保其(即时)一致性的规则,即业务作为一个整体看到的内容,一个单元。但是,在实现用例时,除了聚合之外,还可以将其他域行为实现为域服务,并由用例的“经理”应用程序服务要求。应用服务的最终角色是负责授权,但域服务包含实际的实现。
  • 谢谢它对我有很大帮助。出于好奇,如果此用例的即时一致性是业务需求,您会怎么做?
  • 纯授权和域规则之间有一条细线。我同意 ACL 等通用授权不应成为域的一部分,但基于信誉的系统是 SO 域的核心。它的工作方式和平衡方式会对业务产生直接影响。不过我同意支票可能不属于Question AR 也不属于User AR。
  • @SebastienLorber 您似乎错误地假设必须有一个User 聚合和一个Question 聚合。事实并非如此。 CQRS 正是让我们摆脱了这一点。
【解决方案2】:

如果您想要立即保持一致性,请使用稍微不同的方法

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 事件的投影中。

【讨论】:

  • 仍然不是强一致,除非user.EditedQuestion 也修改了user AR,这将防止user 被另一个事务修改。
  • @guillaume31 是的,但您假设我可以同时对用户和问题存储库使用事务,但出于可扩展性的原因,实际上通常不是这种情况(或不需要)。
  • (对不起,我删除了我的评论,我真正想说的是,根据您管理事务的方式,您不必修改User 即可获得即时一致性)
  • 但是您也可以将 QuestionEdits 封装在 User(或 UserActivity)聚合中,而不是根据需要为其提供自己的聚合。
  • @SebastienLorber 为什么“超过用户和问题”?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-01-03
相关资源
最近更新 更多