如何避免贫乏的领域模型,或者何时将方法从实体转移到服务中
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”都会消费该消息并做出相应的响应;在您的特定情况下,电子邮件通知程序将监听这些事件并被配置为这样做。这样,文章发布位就不需要知道有关电子邮件通知位的任何信息。
【讨论】:
以上是关于如何避免贫乏的领域模型,或者何时将方法从实体转移到服务中的主要内容,如果未能解决你的问题,请参考以下文章