没有服务定位器的领域事件

7

提供领域事件的默认实现:

表示领域事件的接口:

public interface IDomainEvent { }

代表通用域事件处理程序的接口:

public interface IEventHandler<T> where T : IDomainEvent

中心访问点以提出新事件:

public static class DomainEvents
{
    public static void Raise<T>(T event) where T : IDomainEvent
    {
        //Factory is a IoC container like Ninject. (Service Location/Bad thing)
        var eventHandlers = Factory.GetAll<IEventHandler<T>>();

        foreach (var handler in eventHandlers )
        {
            handler.Handle(event);
        }
    }
}

使用:

public class SaleCanceled : IDomainEvent
{
    private readonly Sale sale;

    public SaleCanceled(Sale sale)
    {
        this.sale = sale;
    }

    public Sale Sale
    {
        get{ return sale; }
    }
}

触发事件的服务:

public class SalesService
{
     public void CancelSale(int saleId)
     {
          // do cancel operation

          // creates an instance of SaleCanceled event

          // raises the event
          DomainEvents.Raise(instanceOfSaleCanceledEvent);
     } 
}

有没有另一种方法在不使用服务定位反模式的情况下使用领域事件?

3个回答

7
我想在您的情况下,确实不需要这样做。使用依赖注入,您可以将一个IDomainEventDispatcher实现注入到您的服务中。
我认为这样的单例之所以成为主流,是因为一些著名的开发人员最先提出了这种实现方式,并且一开始并不觉得有什么问题。另一个原因是事件可能需要从域内部引发:
public class Customer
{
    public void Enable()
    {
        _enabled = true;

        DomainEvents.Raise(new CustomerEnabledEvent(_id));
    }
}

在某个阶段,我看到了Jan Kronquist发布的这篇文章:http://www.jayway.com/2013/06/20/dont-publish-domain-events-return-them/

这已经是我第三次在回答中添加该链接了,因为我必须要感谢它改变了我的思维方式。不过,我想现在我不会再这么做了。抱歉,Jan :)

所以,关键点是我们可以将实现更改为以下内容:

public class Customer
{
    public CustomerEnabledEvent Enable()
    {
        _enabled = true;

        return new CustomerEnabledEvent(_id);
    }
}

现在我们的服务可以改用注入式分发器:

public class CustomerService
{
    private IDomainEventDispatch _dispatcher;
    private ICustomerRepository _customerRepository;

    public CustomerService(ICustomerRepository customerRepository, IDomainEventDispatch dispatcher)
    {
        _customerRepository = customerRepository;
        _dispatcher = dispatcher;
    }

    public void Enable(Guid customerId)
    {
        _dispatcher.Raise(_customerRepository.Get(customerId).Enable());
    }
}

因此,不需要单例模式,您可以愉快地注入依赖项。

2

我从未使用过静态的DomainPublisher,并且我的处理方式与@Eben有所不同。这只是我的个人经验,以下是一些我想分享的原因:

  • 因为聚合根会产生事件,而经验法则是您不应在实体内注入任何内容,当您使用可注入的Domain Publisher时,您必须引入一个域服务来调用聚合上的域逻辑并使用Domain Publisher触发事件,或者您可以像@Eben在应用程序服务中解释的那样处理它,否则您将违反经验法则并使实体可注入。
  • 如果您选择在不需要时使用域服务,仅仅为了注入域发布者让您的代码变得更加混乱。业务意图不太明显,还增加了比必要更多的对象和它们的依赖项。

我的做法是,在聚合根实体中拥有一个事件集合以发布事件。每次应该发布事件时,只需将其添加到集合中。我将域发布者注入到聚合根存储库中。因此,事件的发布可以由存储库中的域发布者在基础设施级别处理。因为域发布者实现通常需要处理诸如队列和总线之类的中间件,所以在基础设施级别正确处理它是正确的选择。当例如将实体保存到数据库时出现异常时,您可以更轻松地处理如何处理事件发布策略。您不希望发布事件但未将实体保存到数据库中,或者反之。


使用“双重分派”方法也可以,但这意味着调度程序接口在域中。由于域事件是工件,这意味着调度程序也可以在那里定义。然而,这确实意味着可能需要特定的基础架构实现。并不一定需要域服务,因为总会有一些层与域交互。我认为返回事件将简化测试。 - Eben Roux
关于包含的事件列表,我的个人意见在这里提到:https://dev59.com/O4jca4cB1Zd3GeqPtDTi#28736530 :) - Eben Roux
你的领域中不需要一个域调度器。每个聚合根只有一组事件。这在基础设施(存储库实现)中处理。 - Tomasz Jaskuλa

1
如果您创建了一个名为EventHandlerFactory的类,并使用泛型方法Create<T>,并且T受到IEventHandler<T>类型的约束,则此类将不是服务定位器,而是工厂,因为您只创建IEventHandler<T>实例。同时,服务定位器就像上帝对象,他知道一切。
更多关于此的信息在这里

这里使用工厂模式是否有些过度?您并不一定希望每次事件出现时都实例化一个全新的事件处理程序。我通常在消费者对象需要控制其依赖项的整个生命周期 - 在需要时进行新建和处理(通常在using()子句中)时才使用工厂。 - guillaume31
@guillaume31 控制生命周期是一项实现细节。你可以将这个细节隐藏在一个工厂后面,比如对象池模式。你可以将抽象工厂和对象池结合起来,有些事件处理程序可以每次创建,另一个处理程序可以从对象池中弹出,但重要的是将这些细节封装在工厂后面。正如Eben Roux所说,将EventDispatcher注入到你的服务中,没有必要将Raise逻辑保留在静态类中。 - Sattar Imamov
我不同意。虽然 DI 容器可能确实在内部拥有类似于抽象工厂 + 对象池的东西,但如果我们使用 DI 容器,我们不需要在依赖图的构成代码(组合根)中了解这些概念,更不用说在图的远端的消费者对象层面了。 - guillaume31
通过 DI 容器的组合根,手工编写的组合根 - 这都是一样的。这取决于任务。如果客户端控制创建对象的生命周期,则工厂将只创建新实例。但在当前情况下,据我所知,我们只是触发并忘记事件,客户端不知道谁接收事件以及如何处理此事件。 - Sattar Imamov

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