集中式事件调度的优点和缺点

Pros and cons of a centralized event dispatch

本文关键字:缺点 事件 调度 集中式      更新时间:2023-10-16

我正在考虑在C++应用程序中实现事件的不同方法。有人建议通过通知中心实现集中式事件调度。另一种选择是事件的来源和目标直接沟通。不过,我对通知中心的做法有所保留。我将概述我所看到的两种方法(我很可能对它们有误解,我以前从未实现过事件处理)。

a) 直接沟通。事件是其源接口的一部分。对事件感兴趣的对象必须以某种方式获得源类的实例并订阅其事件:

struct Source
{
    Event</*some_args_here*/> InterestingEventA;
    Event</*some_other_args_here*/> InterestingEventB;
};
class Target
{
public:
    void Subscribe(Source& s)
    {
        s.InterestingEventA += CreateDelegate(&MyHandlerFunction, this);
    }
private:
    void MyHandlerFunction(/*args*/) { /*whatever*/ }
};

(据我所知,boost::信号、Qt信号/插槽和.NET事件都是这样工作的,但我可能错了。)

b) 通知中心。事件在其源的接口中不可见。所有事件都会被发送到某个通知中心,可能是作为一个单例实现的(任何关于避免这种情况的建议都将不胜感激),因为它们被解雇了。目标对象不必知道任何关于源的信息;他们通过访问通知中心来订阅某些事件类型。一旦通知中心接收到一个新事件,它就会通知所有对该特定事件感兴趣的订阅者。

class NotificationCenter
{
public:
    NotificationCenter& Instance();
    void Subscribe(IEvent& event, IEventTarget& target);
    void Unsubscribe(IEvent& event, IEventTarget& target);
    void FireEvent(IEvent& event);
};
class Source
{
    void SomePrivateFunc()
    {
        // ...
        InterestingEventA event(/* some args here*/);
        NotificationCenter::Instance().FireEvent(event);
        // ...
    }
};
class Target : public IEventTarget
{
public:
    Target()
    { 
        NotificationCenter::Instance().Subscribe(InterestingEventA(), *this); 
    }
    void OnEvent(IEvent& event) override {/**/}
};

(我从Poco那里获得了"通知中心"一词,据我所知,它实现了这两种方法)。

我看到了这种方法的一些优点;目标将更容易创建订阅,因为他们不需要访问源。此外,也不会有任何生命周期管理问题:与源不同,通知中心的寿命总是比目标长,因此它们的目标总是在其析构函数中取消订阅,而不必担心源是否仍然存在(这是我在直接通信中看到的一个主要缺点)。然而,我担心这种方法可能会导致无法维护的代码,因为:

  1. 各种各样的事件,可能彼此完全无关,都会进入这个大水槽。

  2. 实现通知中心的最明显的方式是作为一个单例,因此很难跟踪谁以及何时修改订阅者的列表。

  3. 事件在任何接口中都不可见,因此根本无法查看特定事件是否属于任何源。

由于这些缺点,我担心随着应用程序的发展,跟踪对象之间的连接会变得非常困难(例如,我在想象一些问题,试图理解为什么某个特定事件没有启动)。

我正在寻求关于"通知中心"方法的利弊的建议。它可维护吗?它适合各种应用程序吗?也许有办法改进实施?我所描述的两种方法之间的比较,以及任何其他事件处理建议,都是非常受欢迎的。

这些方法是正交的。在特定对象之间交换事件时,应使用直接通信。通知中心方法应仅用于广播事件,例如,当您希望处理给定类型的所有事件(无论其来源如何)时,或者当您希望将事件发送到某组事先不知道的对象时。

为了避免单一事件,请重用代码进行直接通信,并将通知中心对象订阅到要以这种方式处理的所有事件。为了使事情易于管理,您可以在发射对象中执行此操作。

直接通信的寿命相关问题通常通过要求订阅任何事件的每个类都必须从特定基类派生来解决;在Boost.Signals中,这个类被称为trackable。等效的CreateDelegate函数将有关给定对象的订阅的信息存储在trackable中的数据成员中。在销毁trackable时,通过调用匹配的Unsubscribe函数来取消所有订阅。请注意,这不是线程安全的,因为trackable的析构函数只有在派生类析构函数完成后才能调用——在一段时间内,部分销毁的对象仍然可以接收事件。

我将根据问题的需要重点讨论"NotificationCenter"解决方案的优缺点(尽管我会称之为DataBusDispatcherPublisher/Subscriber)。

优点

  • 类的高度解耦
  • 事件的订阅服务器不需要知道事件的来源
  • 如果使用异步NotificationCenter,则可以简化并发管理(可以通过使用NotificationCenter作为cpu时间的调度器来避免多线程)
  • 事件驱动架构具有高度可测试性/可观察性

缺点

  • 接口是隐式的(即类的接口不公开事件)
  • 为了避免类中状态的重复,应用程序的分区是至关重要的
  • 在另一个应用程序中的可重用性变得更加困难(当您想在另一应用程序中重用类时,它会带来NotificationCenter
  • 很难在从输入到输出的事件流中插入新的精化(即:如果类A发出事件E,而类B注册E的通知,则不能在"AB之间"插入类C来更改E的内容,比如说对其进行某种精化)
  • NotificationCenter必须管理客户端的错误(即NotificationCenter应该提供一个异常处理程序来捕获事件处理程序中抛出的exeption)

此外,我想指出,NotificationCenter是一个singleton并不是必不可少的。事实上,您可以有多个NotificationCenter实例,每个实例管理不同类别的事件(例如,在嵌入式系统中,您可以为低级硬件事件设置一个NotificationCenter,为高级逻辑设置另一个NotificationCenter)。

我们应该限制NotificationCenter的设计,它在代码维护方面有很多缺点。

  1. 它将对象关系隐藏到实现中,您不能从调用者代码中获得线索,调用者实际上将显式绑定观察者和目标直接通信中的对象
  2. 您无法从标题中看到偶数接口,您需要查看实施
  3. 您已经将业务逻辑与通知中心绑定,无法轻松地对业务组件进行单元测试并在其他项目中重用它

NotificationCenter的好处是您不需要知道事件源,所以只有当同一事件有多个事件源时,或者甚至源编号会更改时,才应该使用NotificationCenter。

例如,在Windows平台中,您希望侦听所有窗口大小更改事件,但同时可以创建新窗口。