如何避免贫乏的领域模型,或者何时将方法从实体转移到服务中

Posted

技术标签:

【中文标题】如何避免贫乏的领域模型,或者何时将方法从实体转移到服务中【英文标题】: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 类型的一个明显的方法。

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

【问题讨论】:

【参考方案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”都会消费该消息并做出相应的响应;在您的特定情况下,电子邮件通知程序将监听这些事件并被配置为这样做。这样,文章发布位就不需要知道有关电子邮件通知位的任何信息。

【讨论】:

以上是关于如何避免贫乏的领域模型,或者何时将方法从实体转移到服务中的主要内容,如果未能解决你的问题,请参考以下文章

统一建模语言

Lind.DDD.Domain领域模型介绍

货币交易领域模型

避免贫血域模型如何与 ORM、依赖注入和可靠方法一起使用

如何将整个 iTunes Connect 帐户的应用程序组合转移到另一个实体?

领域模型和实体框架之间的存储库模式和映射