【问题标题】:Pros and cons of a centralized event dispatch集中式事件调度的优缺点
【发布时间】:2013-09-01 12:00:10
【问题描述】:

我正在考虑在 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::signals、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. 事件在任何界面中都不可见,因此根本无法查看特定事件是否属于任何来源。

由于这些缺点,我担心随着应用程序的增长,跟踪对象之间的连接会变得非常困难(例如,我正在想象试图理解为什么某些特定事件不会触发的问题) .

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

【问题讨论】:

  • 您应该提供一些设计信息,尤其是您的性能限制、连接数和多线程。总的来说,我认为集中式方法更容易管理。
  • @dzada,没有多线程。现在的连接数大约是一百,但我认为如果创建订阅变得更容易,它会迅速增加,可能是 2 或 300。应用是小游戏,所以性能很重要;但是,到目前为止,这些事件的触发频率还不足以引起严重的性能问题。

标签: c++ events observer-pattern


【解决方案1】:

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

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

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

【讨论】:

  • “正交”是计算机编程中一个非常容易被误解的术语。为了更清楚,您可能会说这两种方法是完全独立且相互独立的。换句话说,两个不同的独立系统,即使您在同一个计算环境中拥有这两个系统。
【解决方案2】:

我将根据问题的要求关注“NotificationCenter”解决方案的优缺点(尽管我将其称为DataBusDispatcherPublisher/Subscriber)。

优点

  • 类的高度解耦
  • 事件的订阅者不需要知道事件的来源
  • 如果您使用异步NotificationCenter,并发管理将得到简化(您可以通过使用NotificationCenter 作为CPU 时间的调度程序来避免多线程)。
  • 事件驱动的架构是高度可测试/可观察的

缺点

  • 接口是隐式的(即类的接口不暴露事件)
  • 应用程序的分区很关键,以避免在类中重复状态
  • 在另一个应用程序中的可重用性变得更加困难(当您想在另一个应用程序中重用一个类时,它会自带NotificationCenter
  • 很难在从输入到输出的事件流中插入新的细化(即:如果类A 发出事件E 和类B 注册E 的通知,则不能插入类C "between" AB 来改变E 的内容让我们做一些阐述)
  • NotificationCenter 必须管理客户端的错误(即NotificationCenter 应提供异常处理程序来捕获事件处理程序中抛出的异常)。

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

【讨论】:

  • 对于“并发管理被简化”,其实我不这么认为。无论你的组件和调用者是多线程还是单线程,通知中心都需要是线程安全的(因为它需要处理其他组件事件),这需要通知中心实现需要小心,它需要尽快释放锁尽可能,否则通知中心可能会在调用慢速事件处理程序并阻止您的组件触发事件时保持锁定。
  • @ZijingWu 在很多情况下,您可以通过使用 NotificationCenter 作为调度程序来避免多线程。当您选择事件驱动架构时,这是最好的解决方案。
  • 您能否提供一个示例如何将其用作调度程序。我其实不明白如何使用它来避免多线程。
  • @ZijingWu 请参阅 boost asio 异步 I/O,了解如何避免多线程的一些好例子。在示例中,boost::asio::io_service 用作事件处理程序的调度程序。
  • 我已经检查了io_service,但我认为它与NotificationCenter不是一回事,通知中心不需要单独的线程来运行它,它运行在事件源线程中,但是io_service 需要。实际上,io_service 是一个工作项队列,但 NotificationCenter 是主题观察者设计模式的中介。
【解决方案3】:

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

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

NotificationCenter的好处是你不需要知道事件源,所以只有在同一事件有多个事件源时才使用NotificationCenter,否则源数会发生变化。

例如,在Windows平台上,你想监听所有的windows size changed事件,但同时可以创建新的窗口。

【讨论】:

  • 实际上您可以对“业务组件”进行单元测试:通过通知中心发送和接收事件。我觉得没那么难……
  • 想想 NotificationCenter 依赖其他类来提供线程安全的功能,那么它可能并不那么容易。我并不是说它总是那么难,但是任何不必要的依赖都可能使单元测试在现实中变得极其困难。
  • 看我对这个问题的回答:使用事件驱动架构,您通常可以避免多线程。
猜你喜欢
  • 2010-12-03
  • 2011-04-11
  • 2011-02-26
  • 1970-01-01
  • 1970-01-01
  • 2012-06-07
  • 2013-05-05
  • 2013-08-25
  • 2010-10-07
相关资源
最近更新 更多