如何减少事件总线实现中的耦合

时间:2016-06-28 08:57:01

标签: c++ events decoupling

在我的应用程序中,我有几个模块不符合'is-a'或'has -a'关系,但仍然需要相互通信和传递数据。为了尝试松散地耦合这些模块,我实现了一个Event Bus类来处理从“事件海报”到“事件监听器”的消息传递。

如果类希望注册接收某些事件,则可以实现IEventListener。同样,如果需要将事件推送到总线,则类可以调用EventBus::postEvent()。当EventBus::update()被调用时,EventBus处理已调度消息的队列并将它们路由到已注册的侦听器。

EventBus.h

#pragma once

#include <queue>
#include <map>
#include <set>
#include <memory>


class IEvent
{
public:
    static enum EventType
    {
        EV_ENEMY_DIED,
        EV_ENEMY_SPAWNED,
        EV_GAME_OVER
    };

    virtual ~IEvent() {};
    virtual EventType getType() const = 0;
};


class IEventListener
{
public:
    virtual void handleEvent(IEvent * const e) = 0;
};


class EventBus
{
public:
    EventBus() {};
    ~EventBus() {};

    void update();
    void postEvent(std::unique_ptr<IEvent> &e);
    void registerListener(IEvent::EventType t, IEventListener *l);
    void removeListener(IEvent::EventType t, IEventListener *l);

private:
    std::queue<std::unique_ptr<IEvent>> m_eventBus;
    std::map<IEvent::EventType, std::set<IEventListener *>> m_routingTable;
};

EventBus.cpp

#include "EventBus.h"


using namespace std;


/**
 * Gives the EventBus a chance to dispatch and route events
 * Listener callbacks will be called from here
 */
void EventBus::update()
{
    while (!m_eventBus.empty())
    {
        // Get the next event (e_local now owns the on-heap event object)
        unique_ptr<IEvent> e_local(move(m_eventBus.front()));
        m_eventBus.pop();

        IEvent::EventType t = e_local->getType();
        auto it = m_routingTable.find(t);
        if (it != m_routingTable.end())
        {
            for (auto l : ((*it).second))
            {
                l->handleEvent(e_local.get());
            }
        }
    }
}

/**
 * Posts an event to the bus, for processing and dispatch later on
 * NB: The event bus will takes ownership of the on-heap event here
 */
void EventBus::postEvent(unique_ptr<IEvent> &e)
{
    // The EventBus now owns the object pointed to by e
    m_eventBus.push(unique_ptr<IEvent>(move(e)));
}

/**
 * Registers a listener against an event type
 */
void EventBus::registerListener(IEvent::EventType t, IEventListener *l)
{
    // Add this listener entry
    // If the routing table doesn't have an entry for t, std::map.operator[] will add one
    // If the listener is alredy registered std::set.insert() won't do anything
    m_routingTable[t].insert(l);
}

/**
 * Removes a listener from the event routing table
 */
void EventBus::removeListener(IEvent::EventType t, IEventListener *l)
{
    // Check if an entry for event t exists
    auto keyIterator = m_routingTable.find(t);
    if (keyIterator != m_routingTable.end())
    {
        // Remove the given listener if it exists in the set
        m_routingTable[t].erase(l);
    }
}

正如您所看到的,在我当前的实现中,我为每个要传递的事件类型创建了具体的IEvent实现。我这样做是为了让每个事件都附加自定义数据(这是我的情况要求)。不幸的是,这意味着我的EventBus系统必须知道系统的所有用户,增加了我的EventBus类和类的用户之间的耦合。此外,IEvent接口需要将所有事件类型的列表保存为枚举,这具有相同的问题(增加耦合)。

  1. 有没有办法修改这个实现,以便EventBus可以完全通用(不需要知道EventBus的用户),但仍允许我为每个事件传递自定义数据?我查看了C ++ 11可变参数模板函数,但在这种情况下无法弄清楚如何使用它们。
  2. 作为一个问题,我在这里正确使用std::unique_ptr吗?

1 个答案:

答案 0 :(得分:1)

问题1 “有没有办法修改此实现,以便EventBus可以完全通用”

简短的回答,是的。

更长的答案:有很多方法可以实现这一目标。一个在这里描述:

事件的生产者和消费者都需要就类型/数据达成一致,但EventBus本身并不需要知道。实现此目的的一种方法是使用boost::signals2::signal<T>作为事件类型。这将为您提供经过验证的,灵活的,类型安全的信号/插槽实现。然而,它不会提供排队槽回调并从EventBus::update() - 函数处理它们的可能性。

但是,这也可以弥补。通过将事件类型EventBus::postEvent()作为参数std::function<void()>并像这样调用postEvent()

boost::signals2::signal<int> signal;
...
eventbus.postEvent(boost::bind(signal, 42));
// note: we need to use boost::bind (not std::bind) for boost::signals to be happy

EventBus会看到std::function<void()>并发送到广告位。数据(本例中为42)将由boost::bind的结果保存,并在调用插槽时用作参数。

问题2 “我正确使用std::unique_ptr”:

几乎。我会删除EventBus::postEvent的引用:

void EventBus::postEvent(std::unique_ptr<IEvent> e);

通过执行此操作,您可以强制调用者将std::unique_ptr<IEvent>主动移动到EventBus。这将使用户意识到EventBus拥有所有权,并且使阅读代码的人明白了目的是什么以及如何转移所有权。

  

CppCoreGuidelines R.32

     

“使用unique_ptr参数表示某个函数假设小部件的所有权”