【问题标题】:How to define aggregate in DDD correctly?如何正确定义 DDD 中的聚合?
【发布时间】:2019-07-26 11:19:11
【问题描述】:

我一直在阅读有关 DDD 的内容,但仍然对聚合根感到困惑。 想象一下,我有一个类似于博客的情况,人们可以在其中创建帖子并将 cmets 添加到其他帖子。

规则: - 每个人都需要有一个帐户才能添加帖子或评论 - 用户只能删除自己的 cmets

考虑到这一点,我需要以下对象: -邮政 -发表评论 -用户

所以,我只创建了 Post 对象作为聚合根,并为其添加了一些业务逻辑

    public class User : EntityBase
    {
        public string Name { get; set; }
        public string Avatar { get; set; }
    }
    public class Post : EntityBase, IAggregate
    {        
        public string Title { get; set; }
        public string Content { get; set; }
        public User Creator { get; set; }
        private IList<PostComment> Comments { get; set; }

        public void AddComment(PostComment comment)
        {
            this.Comments.Add(comment);
        }
        public void DeleteComment(PostComment comment, int userId)
        {
            if (comment.Creator.Id != userId)
                throw new Exception("You cannot delete a comment that is not yours. blablabla");
            this.Comments.Add(comment);
        }
        public IList<PostComment> GetComments()
        {
            return this.Comments;
        }

    }
    public class PostComment : EntityBase
    {        
        public string Comment { get; set; }
        public User Creator { get; set; }
    }

我这样做正确吗?我的意思是,业务逻辑是否在正确的位置?或者我也应该将 PostComment 设为聚合根并在其中添加添加/删除的逻辑?

【问题讨论】:

    标签: c# domain-driven-design


    【解决方案1】:

    警告:使用玩具问题很难推断 DDD。特别是在您的核心域中,所有这些工作的重点是您可以自定义内容以满足您的本地需求。如果您不需要定制解决方案,您只需购买一些现成的解决方案,集成并继续使用它。

    或者我也应该将 PostComment 作为聚合根并在其中添加添加/删除的逻辑?

    也许吧。最好将聚合视为原子,您可以加载整个聚合、进行更改、保存结果。

    因此,如果您发现自己有许多 并发 尝试修改 same 聚合,那么您必须处理一堆争用问题。当 Bob 改变他的评论时,Alice 不能改变她的评论;我们必须一次做一个(以避免丢失更改)。

    另一方面,如果每个评论都是它自己的聚合,那么 Bob 和 Alice 可以并行进行更改,而无需重新运行“业务逻辑”,因为对方的更改首先发生。

    免费的时候很棒。但它不是免费的——你付出的代价是信息现在已经分发,而且你必须处理变化有不同时间的事实。您有时会看到此处使用的“最终一致性”——因为权威信息是分布式的,所以有时并非所有观察者都有相同组的更改。

    在大多数域中,这很好:race conditions don't exist。但试图在分布式数据中执行全部或全部更改是一场噩梦。

    另一方面,如果您愿意接受变化发生在不同的时间,那么将聚合分开就可以了。

    例如:推特。鲍勃发了一些愚蠢的推文。爱丽丝在推特上说鲍勃很笨,并附有指向他的推文的链接。 Bob 删除了他的推文。这一切都很好,因为我们对 Alice 的推文中包含一个不再可用的链接这一事实感到满意。

    通常来自外部世界的信息可以是它自己的聚合,因为我们在那个阶段真正在做的是缓存数据,当我们使用这些数据时,这些数据已经过时了。收到。

    您可能还想查看 Mauro Servienti 的演讲 All Our Aggregates Are Wrong,其中讨论了将聚合分解为更小部分的启发式方法。

    【讨论】:

      【解决方案2】:

      我这样做正确吗?我的意思是,业务逻辑是否在正确的位置?或者我也应该将 PostComment 设为聚合根并在其中添加添加/删除的逻辑?

      部分!我认为逻辑在正确的位置, PostComment 不应该是聚合根。但是,如果您想了解更多关于 DDD 的信息,我认为在继续之前还有一些其他要点需要查看。我希望我能在下面的解释中帮助你。

      我已经查看了代码并对其进行了重构,以解释您可以重新考虑的一些要点。在阅读下面我的解释之前,请尝试阅读、比较和理解。

      // you can simplify your DomainModel removing the IAggregate plus adding generics
      public abstract class Entity<T>
      {
          public T Id { get; set; }
      }
      
      // this is an Aggregate Root
      public class Person : Entity<int>
      {
          public string Name { get; set; }
          public string Avatar { get; set; }
      
          public override string ToString()
          {
              return Name;
          }
      }
      
      //this is an Aggregate Root
      public class Post : Entity<int>
      {
          private List<Comment> _comments = new List<Comment>();
      
          public string Title { get; set; }
          public string Content { get; set; }
          public Person Author { get; set; }
          public IReadOnlyList<Comment> Comments => _comments;
      
          public void Reply(Comment comment)
          {
              _comments.Add(comment);
          }
          public void Delete(Comment comment, int personId)
          {
              if (!AreSame(comment.Author, personId))
                  throw new Exception("You cannot delete a comment that is not yours. blablabla");
      
              _comments.Add(comment);
          }
      
          private bool AreSame(Person author, int personId)
          {
              return author.Id == personId;
          }
      
          public override string ToString()
          {
              return Title;
          }
      }
      
      // this is a Value Object part of Post Aggregate
      public struct Comment
      {
          public DateTime Date;
          public string Text;
          public Person Author;
      
          public Comment(DateTime date, string text, Person author)
          {
              Date = date;
              Text = text;
              Author = author;
          }
      
          public override string ToString()
          {
              return $"{Date} - {Author}: {Text}";
          }
      }
      

      如果 PostComment 是 Post Aggregate 的一部分,则它不能是 EntityBase,因为每个 Aggragate 应该只有一个根 (Id)。您正在建模一个帖子可能有 N 条评论的域。您可以将 PostComment 视为一个值对象,而不是一个删除其 Id 的实体。

      您应该注意您使用的名称。尝试听起来更自然。它被称为无处不在的语言,每个人都会说的话。

      用户是在系统上下文中有意义的描述,换句话说,如果您处理安全或身份验证上下文,您应该有一个用户,在博客上下文中您有一个充当作者的人。

      使用用户所说的术语提高可读性。回复可能比 AddComment 更自然。

          public void Reply(Comment comment)
          {
              _comments.Add(comment);
          }
      

      为您的条件添加名称以提高可读性:

          public void Delete(Comment comment, int personId)
          {
              if (!AreSame(comment.Author, personId))
                  throw new Exception("You cannot delete a comment that is not yours. blablabla");
      
              _comments.Add(comment);
          }
      
          private bool AreSame(Person author, int personId)
          {
              return author.Id == personId;
          }
      

      【讨论】:

      • 谢谢你的例子,但我有一个问题。作为您的示例, Comment 不是聚合根,因此,它没有存储库。由于评论由 Post 控制,如果有人删除他们的评论,则必须通过 IPostRepository 进行,对吗?调用 IPostRepository 删除评论听起来很奇怪? IPostRepository.Delete(评论)
      • 好问题!恕我直言,设计您的域模型时考虑持久性是一个错误。实体是独立的。如果您删除帖子,有什么理由保留评论?你不应该使用IPostRepository.Delete(Comment),你会:var post = IPostRepository.Find(int); post.Delete(Comment); IPostRepository.Update(post);你怎么看?
      • 最后一个问题,按照你的例子。 IPostRepository 如何知道删除了哪个评论?您如何跟踪已删除/更新的项目?
      • 即使 Comment 值对象没有 Id,它仍然可以是可比较对象,覆盖 Equals 和 GetHashCode 方法。此更新/删除逻辑的实现可以通过多种方式完成。如果您有 github 存储库,请在此处告诉我,我们可以尝试一些替代方案。
      猜你喜欢
      • 2018-12-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多