电子邮件通知 - 在领域对象或服务中?

3
我希望能够得到您在以下设计问题上的建议(使用基于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. Change AddUpvote() to take an IEmailerService parameter and perform the logic within the AddUpvote() method.

    public void AddUpvote(Vote upvote, IEmailerService emailer)
    {
        Upvotes.Add(upvote);
        if ( Upvotes.Count == 10 )
        {
            emailer.Send(Owner.EmailAddr);
        }
    }
    
  2. Detect this state within AddUpvote() and have AddUpvote() resolve an IEmailService from an IoC container (instead of passing the IEmailerService as a parameter).

  3. Detect this state in the external service object that invokes question.AddUpvote().

    public void UpvoteClickHandler(Question question)
    {
        question.AddUpvote(new Upvote());
        if ( question.Upvotes.Count == 10 )
        {
            _emailer.Send(question.Owner.EmailAddr);
        }
    }
    
  4. Your better solution here!


感谢大家的出色回复! - Kevin Pullin
4个回答

5
您真的不想将这两个混在一起,因为它们有不同的关注点。让问题类关注问题,消息服务关注当投票达到10、20或100时要执行什么操作。以下示例仅用于演示目的,但您会明白。关注点有明确分离,所以如果发送消息的要求发生变化,问题类不需要改变。请记住,根据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的任何实例进行操作。 - Todd Smith
是的,在实践中,除非绝对必要,我永远不会在事件中使用这样的引用类型。然而,这只是为了演示如何将功能与问题对象分离的示例。如果我在这里实现一个真正的服务,同样的处理方法可能会处理一组服务,并且事件可能会传递一个问题ID或类似的东西。 - Josh
@Josh,你是如何处理邮件服务中的故障的?例如,点赞已经被持久化,但是MessageService无法完成其工作(SMTP服务器故障、服务崩溃等)。 - wings
1
@wings - 绝对值得拥有一整个系列的文章来讲述它,但是总的想法是,如果你真的需要确保消息传递,你需要将发送消息的意图与消息的实际发送分开。我通常会尝试将这个过程转移到像(SQS + Lambda)这样的服务中,或者使用可以为我处理细节的其他服务,但你也可以轻松地在数据库中保存记录。然后,你需要有异步的东西将那些要发送的意图取出并实际发送消息。再说一次...需要更长的解释,但这是一个大体概述。 - Josh
谢谢@Josh,我想到了同样的想法,但我看到很多人在多个通道中创建域事件,而没有将它们放入事务中(即一个通道保存事件但其他可能会失败),这让人感到不舒服,因为服务中的故障将导致“丢失事件”,因此需要异步服务,从您提到的数据源创建事件。 - wings

4
选项1和选项2都不是发送电子邮件的正确位置。问题实例不应该知道以下两个内容:
  1. 它不应该了解策略,即何时发送电子邮件。
  2. 它不应该了解有关策略通知的机制,即电子邮件服务。
我知道这是一种品味问题,但您将问题与策略以及发送电子邮件的机制紧密联系在一起。将此Question类移动到另一个项目(例如StackOverflow的姐妹站点ServerFault)将非常困难。
我对此问题感兴趣,因为我正在为自己构建的Help Desk创建通知系统。这是我在我的系统中所做的:
创建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确定它是否以及如何发送电子邮件。

1

答案取决于您对应用和对象设计的基本方法以及您视系统最重要特征的看法。看起来您有数据、问题和业务规则、投票等,根本不需要建立问题对象。所以您应该将数据视为数据,并允许数据工具对其进行处理,而不是将行为混合其中。传统的对象设计将所有的行为和数据都放在对象中,因此发送电子邮件应属于对象范畴。(选项1和2)我想这就是黑盒或自包含对象方法。现代做法,据我所学,是将对象作为简单的数据持有者。它们被设计为可以移动、持久化、转换并执行某些操作。也许只是像C语言的结构体一样简单。行为源自施加到简单对象上的服务和转换。


1
你所描述的被称为贫血领域模型,可以说是面向对象编程的反面,这也正是发帖者特别想避免的。数据 + 行为 = 对象。 - Bryan Watts
他确实说过他想避免它,但是后来又引入了一个选项3,将其带回来了。 - Martlark
1
我不认为“现代”设计只涉及一堆DTO和服务。SOA是一种哲学,自OO的早期就存在了,因此说这种方法更现代是错误的。SOA和OO是相辅相成的。确实,由于已经存在太多糟糕的OO,所以“重量级”的领域模型正在失宠。服务旨在处理合同,因此在传递信息之前必须剥离业务逻辑。为此,您需要DTO,但仅使用DTO的设计几乎没有用处。 - Josh

1

大家好,

在我看来,“每当某个问题获得10个赞时向提问者发送电子邮件通知”是领域逻辑,因此应该放入领域对象中,以避免贫血的领域模型。

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

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

最好的问候,

Stefano


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接