【问题标题】:How to avoid anemic domain models, or when to move methods from the entities into services如何避免贫乏的领域模型,或者何时将方法从实体转移到服务中
【发布时间】:2010-12-02 02:29:37
【问题描述】:

我有一个常见的场景,我正在向在 DDD 和领域建模方面更有经验的人寻求指导。

假设我开始构建一个博客引擎,第一个要求是文章发布后,用户可以开始对其发表评论。这开始很好,并导致以下设计:

public class Article
{
    public int Id { get; set; }

    public void AddComment(Comment comment)
    {
        // Add Comment
    }
}

我的 MVC 控制器是这样设计的:

public class ArticleController
{
    private readonly IRepository _repository;

    public ArticleController(IRepository repository)
    {
        _repository = repository;
    }

    public void AddComment(int articleId, Comment comment)
    {
        var article = _repository.Get<Article>(articleId);
        article.AddComment(comment);
        _repository.Save(article);
        return RedirectToAction("Index");
    }
}

现在一切正常,符合要求。下一次迭代我们要求每次发布评论时,博客作者都应该收到一封电子邮件通知他。

在这一点上,我有两个我能想到的选择。 1) 修改 Article 以要求 IEmailService(在 ctor 中?)或从对我的 DI 容器的静态引用中获取 EmailService

1a) 看起来很丑。我认为它违反了我的实体知道服务的一些域模型规则?

public class Article
{
    private readonly IEmailService _emailService;

    public Article(IEmailService emailService)
    {
        _emailService = emailService;
    }

    public void AddComment(Comment comment)
    {
        // Add Comment

        // Email admin
        _emailService.SendEmail(App.Config.AdminEmail, "New comment posted!");
    }
}

1b) 看起来也很丑,我现在需要一个静态访问的已配置 DI 容器。

public class Article
{
    public void AddComment(Comment comment)
    {
        // Add Comment

        // Email admin
        var emailService = App.DIContainer.Resolve<IEmailService>();
        emailService.SendEmail(App.Config.AdminEmail, "New comment posted!");
    }
}

2) 创建一个 IArticleService 并将 AddComment() 方法移动到此服务而不是文章实体本身。

我相信这个解决方案更简洁,但是现在添加评论不太容易被发现,并且需要 ArticleService 来执行这项工作。似乎 AddComment 应该属于 Article 类本身。

public class ArticleService
{
    private readonly IEmailService _emailService;

    public ArticleService(IEmailService emailService)
    {
        _emailService = emailService;
    }

    public void AddComment(Article article, Comment comment)
    {
        // Add comment

        // Email admin
        _emailService.SendEmail(App.Config.AdminEmail, "New comment posted!");
    }

}


public class ArticleController
{
    private readonly IRepository _repository;
    private readonly IArticleService _articleService;

    public ArticleController(IRepository repository, IArticleService articleService)
    {
        _repository = repository;
        _articleService = articleService;
    }

    public void AddComment(int articleId, Comment comment)
    {
        var article = _repository.Get<Article>(articleId);
        _articleService.AddComment(article, comment);
        _repository.Save(article);
        return RedirectToAction("Index");
    }
}

所以我基本上是向在域建模方面更有经验的人寻求建议。如果我缺少更明显的解决方案,请告诉我:)

老实说,我通常不喜欢这两种解决方案,因为“服务”选项不太容易被发现。如果没有可用的 ArticleService,我无法再向 Article 实例添加评论。它也感觉不太自然,因为 AddComment 似乎是 Article 类型的一个明显的方法。

无论如何,我期待阅读输入。提前致谢。

【问题讨论】:

    标签: c# inversion-of-control domain-driven-design bdd


    【解决方案1】:

    我认为,每当领域专家使用“何时”这个词时,都必须考虑领域事件或事件总线,从这个意义上说,这是一个经典的例子。

    我已经写了一个详细的answer,它描述了何时使用事件总线,围绕这个主题可能是一个很好的阅读

    【讨论】:

      【解决方案2】:

      不使用域事件,您可以使用双重调度模式并将 AddComment 逻辑放入域服务中。

      这就是它的样子:

      public class Article
      {
          public void AddComment(Comment comment, IAddCommentProcessor commentProcessor)
          {
              commentProcessor.AddComment(this, comment);
          }
      }
      
      public interface IAddCommentProcessor
      {
          void AddComment(Article article, Comment comment);
      }
      
      public class AddCommentAndEmailProcessor : IAddCommentProcessor
      {
          private readonly _emailService;
          public AddCommentAndEmailProcessor(EmailService emailService)
          {
              _emailService = emailService;
          }
      
          public void AddComment(Article article, Comment comment)
          {
              // Add Comment
      
              // Email
              _emailService.SendEmail(App.Config.AdminEmail, "New comment posted!");
          }
      }
      
      public class ArticleController
      {
          private readonly IRepository _repository;
          private readonly IArticleService _articleService;
      
          public ArticleController(IRepository repository, IArticleService articleService)
          {
              _repository = repository;
              _articleService = articleService;
          }
      
          public void AddComment(int articleId, Comment comment)
          {
              var article = _repository.Get<Article>(articleId);
              article.AddComment(comment, new AddCommentAndEmailProcessor(ServiceLocator.GetEmailService())); // Or you can use DI to get the Email Service, or any other means you'd prefer
              _repository.Save(article);
              return RedirectToAction("Index");
          }
      }
      

      如果您愿意,您可以将添加评论逻辑保留在文章的 AddComment 上,而是使用 CommentAdded() 方法将域服务变成类似 ICommentAddedProcessor 的东西,并让文章上的 AddComment 接受评论和 ICommentAddedProcessor。

      【讨论】:

      • 我会警惕直接在请求处理程序中直接添加任何电子邮件功能。这可能是系统中的瓶颈。对于单用户场景或不会有很多用户的场景可能没问题。同样,您可以抽象地将电子邮件 SendEmail 放入某种形式的队列处理中,而不是立即发送。
      【解决方案3】:

      通过这个优秀的问题,我阅读了 MSDN 上 Udi 的 Employing the Domain Model Pattern

      HTH 帮助其他用户。

      我一直在想办法问同样的问题,但几次都把自己弄糊涂了。你的问题肯定不是!谢谢

      【讨论】:

        【解决方案4】:

        我认为这个特殊问题可以通过Domain Event 优雅地解决。

        【讨论】:

        • 阅读了Udi的三篇文章后,我认为这将很好地解决我的问题。但是,对于应该在哪里注册域事件,我仍然有些困惑。我的控制器应该在ctor中注册它们吗?域事件是否应该在 Application_Start 中注册?
        • 好吧,如果您仔细研究该帖子的所有 cmets,您会发现我和 Udi 之间关于事件服务是否应该是静态的小讨论。就个人而言,我会将其作为实例服务并将其作为依赖项注入到 Controller 中。我猜 Udi 会在 Global.asax 中注册它...
        • 这是一个非常基本的场景,而领域事件甚至在 Evans 的书中都没有。那么人们是如何应对的呢?
        • @aaimnr 查看我的答案以获取没有域事件的替代方案
        【解决方案5】:

        您是否考虑过让文章控制器本质上是向上传递消息/发布事件?然后任何“article-posted-event-listeners”都会消费该消息并做出相应的响应;在您的特定情况下,电子邮件通知程序将监听这些事件并被配置为这样做。这样,文章发布位就不需要知道有关电子邮件通知位的任何信息。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2011-12-19
          • 2012-02-04
          • 2015-12-30
          • 1970-01-01
          • 2011-02-20
          • 1970-01-01
          • 2011-06-17
          • 2010-12-20
          相关资源
          最近更新 更多