【问题标题】:Email notifications - In the domain object or a service?电子邮件通知 - 在域对象或服务中?
【发布时间】:2009-05-29 23:22:22
【问题描述】:

我正在寻找有关如何解决以下设计问题的建议(使用基于 stackoverflow 的虚构示例)。我会尽量避免使用贫乏的领域模型,并为此类案例寻求一般的“最佳实践”建议。

场景:

假设正在为 stackoverflow 开发一项新功能,该功能会在问题所有者获得 10 次赞成票时向其发送电子邮件通知。

领域对象模型是这样的:

public class Question
{
    string Question { get; set; }
    IList<Votes> Upvotes { get; set; }
    User Owner { get; set; }

    public void AddUpvote(Vote upvote)
    {
        Upvotes.Add(upvote);
    }
}

可能的实现:

  1. AddUpvote() 更改为采用IEmailerService 参数并在AddUpvote() 方法中执行逻辑。

    public void AddUpvote(Vote upvote, IEmailerService emailer)
    {
        Upvotes.Add(upvote);
        if ( Upvotes.Count == 10 )
        {
            emailer.Send(Owner.EmailAddr);
        }
    }
    
  2. AddUpvote() 中检测此状态并让 AddUpvote() 从 IoC 容器解析 IEmailService(而不是将 IEmailerService 作为参数传递)。

  3. 在调用question.AddUpvote()的外部服务对象中检测此状态。

    public void UpvoteClickHandler(Question question)
    {
        question.AddUpvote(new Upvote());
        if ( question.Upvotes.Count == 10 )
        {
            _emailer.Send(question.Owner.EmailAddr);
        }
    }
    
  4. 这里有你更好的解决方案!

【问题讨论】:

  • 感谢大家的出色回复!

标签: domain-driven-design business-logic


【解决方案1】:

您真的不想将这两者混合在一起,因为它们有不同的关注点。让 Question 类关心问题,让消息服务关心当投票达到 10、20、100 或...时该怎么做。

以下示例仅用于演示目的,但您会明白这一点。有明确的关注点分离,因此如果发送消息的要求发生变化,Question 类不必更改。请记住,根据 SOLID 原则,一个类应该只有一个改变的理由。

public class Question
{
    public string Description { get; set; }
    public Int32 Votes { get; set; }
    public User Owner { get; set; }

    public event EventHandler<QuestionEventArgs> OnUpvote;

    private void RaiseUpvoteEvent(QuestionEventArgs e)
    {
        var handler = OnUpvote;
        if (handler != null) handler(this, e);
    }

    public void Upvote()
    {
        Votes += 1;

        RaiseUpvoteEvent(new QuestionEventArgs(this));
    }
}

public class MessageService
{
    private Question _question;

    public MessageService(Question q)
    {
        _question = q;

        q.OnUpvote += (OnUpvote);
    }

    private void OnUpvote(object sender, QuestionEventArgs e)
    {
        if(e.Question.Votes > 10)
            SendMessage(e.Question.Owner);
    }
}

public class QuestionEventArgs: EventArgs
{
    public Question Question { get; set; }

    public QuestionEventArgs(Question q)
    {
        Question = q;
    }
}

所以你有它。有很多其他方法可以实现这一点,但事件模型是一个很好的方法,它实现了您在实现中想要的关注点分离,以便更早地进行维护。

【讨论】:

  • 您在 MessageService 构造函数中保存对问题的引用但对传递给 OnUpvote 的任何实例进行操作似乎很奇怪。
  • 是的,在实践中,除非绝对必要,否则我绝不会在事件中使用这样的引用类型。但是,这仅作为如何将功能与 Question 对象分离的示例。如果我在这里实现一个真正的服务,相同的处理方法可能会处理一组服务,并且事件可能会传递一个问题 ID 或其他东西。
  • @Josh 您如何处理电子邮件服务中的故障?即,upvotes 保持不变,但 MessageService 无法完成其工作(SMTP 服务器故障、服务崩溃等)
  • @wings - 绝对值得一整系列的文章,但总体思路是,如果您确实需要确保传递,则需要将发送消息的意图与实际发送分开的消息。我通常会尝试将其卸载到(SQS + Lambda)之类的服务或为我处理细节的服务,但您可以轻松地将记录保存在数据库中。然后,您需要一些东西来异步接收这些意图发送并实际发送消息。再次......需要更长的解释,但这是一个粗略的概述。
  • 感谢@Josh,我提出了同样的想法,但我看到很多人在多个渠道中创建域事件而没有将它们放入交易中(即一个渠道持续事件,但其他渠道可能失败)感觉不舒服因为服务失败将导致“丢失事件”,因此需要异步服务,如您所提到的,该服务从黄金数据源创建事件。
【解决方案2】:

选项 1) 和 2) 都作为发送电子邮件的错误位置跳出来。 Question 实例不应该知道这两件事:

  1. 它不应该知道策略,即何时发送电子邮件。
  2. 它不应该知道策略的通知机制,即电子邮件服务。

我知道这是一个品味问题,但您在问题中与政策以及发送电子邮件的机制密切相关。将这个 Question 类移动到另一个项目(例如 ServerFault,它是 StackOverflow 的姊妹站点)真的很难

我对这个问题很感兴趣,因为我正在为我正在构建的帮助台创建一个通知系统。这就是我在我的系统中所做的:

创建一个 NotificationManager(基本上,将通知的关注点完全移到一个单独的类中)。

public Class NotificationManager
{
    public void NotificationManager(NotificationPolicy policy, IEmailService emailer)
    {
    }
}

然后我按照这个思路做了一些事情(UpvoteClickHandler 依赖于 NotificationManager 实例):

public void UpvoteClickHandler(Question question)
{
    question.AddUpvote(new Upvote());
    _notificationManager.Notify(Trigger.UpvoteAdded, question);
}

UpvoteClickHandler 所做的只是告诉 NotificationManager 已向问题添加了赞成票,并让 NotificationManager 确定是否以及如何发送电子邮件。

【讨论】:

    【解决方案3】:

    答案取决于您对应用程序和对象设计的基本方法。并且(在此处编辑)您认为系统最重要的特征。看起来你有数据、问题和业务规则,赞成票。根本不质疑对象。因此,您应该将数据视为数据,并允许数据工具对其进行处理,而不是将行为混入其中。传统的对象设计将所有行为和数据都包含在对象中,因此发送电子邮件将属于对象。 (选项 1 和 2)我猜这是黑盒或自包含对象方法。正如我所了解的那样,现代实践将对象作为简单的数据持有者。这意味着要四处移动,持久化,转换并对其进行处理。在 C 语言中,也许只是结构而已。行为来自应用于简单对象的服务和转换。

    【讨论】:

    • 您所描述的被称为贫血域模型,可以说是 OO 的对立面,并且是发帖人特别试图避免的。数据 + 行为 = 对象。
    • 好吧,他确实说过他想避免它,但随后引入了一个选项 3,它把它带回来了。
    • 我不会说“现代”设计只涉及一堆 DTO 和服务。 SOA 是一种自 OO 早期就存在的哲学,所以说这种方法在某种程度上更现代是错误的。 SOA 和 OO 齐头并进。确实,“重”域模型正在失宠,但这只是因为已经存在太多糟糕的 OO。服务旨在处理合同,因此在传递该信息之前必须剥离业务逻辑。为此,您需要 DTO,但仅使用 DTO 的设计几乎没有用处。
    【解决方案4】:

    大家好,

    在我看来,“每当他/她的问题收到 10 个赞成票时,向他/她的问题的所有者发送电子邮件通知”是域逻辑,因此它应该进入域对象,以避免贫血域。

    必须进入基础设施层的是发送电子邮件(即与 smtp 服务器通信)的操作。

    所以我认为选项 1 并非完全错误。请记住,您始终可以通过传递 IEmailerService 的模拟实现来测试您的对象。

    最好的问候,

    斯特凡诺

    【讨论】:

      猜你喜欢
      • 2010-09-21
      • 1970-01-01
      • 1970-01-01
      • 2014-10-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多