4

我做了一个模块系统,如下所示:

//setting event
module->set_event("started", [](boost::any ev) {
  cout << "The module have been successfully started" << endl;
});

//somewhere else
module->start();

//impl
void Module::start() {
  //run once protection here
  this->trigger_event("start");//pre start
  this->_impl->start(); //on error, throw exception
  this->trigger_event("started"); //post start
}

void Module::trigger_event(string str,boost::any ev = boost::any() )
{
  //mutex
  //invokes every handler which have been invoked set_event
}

真的很简单,很容易理解。第一个缺点是没有“remove_event”,但是为了允许它(如果我需要它),我可以只返回一个 std::function(在 set_event 方法中),它在调用时会删除处理程序,所以如果用户想要删除事件,只需调用返回的处理程序:

function<void ()> h = module->set_event(...);
//... somewhere later
h() //removes the handler safely

但我很好奇这是否是一个好的设计?或者我会突然发现使用我的方法的非常大的缺点并且需要重写大多数代码?我可以说“trigger_event”方法也将用作模块之间的消息系统,就像 JavaScript 库 jQuery 方法:“bind”允许的那样。一个例子:

module->set_event("message", [](boost::any message) {
 //deal with the message here
});

//send message
module->trigger_event("message", MyMessageTypeHere);

在大多数情况下,这是一种公认​​的设计吗?我想要一种非常灵活的方法,因此我的想法是使用 boost::any,但总的来说这是一个好主意(可扩展性、性能、灵活性)吗?

4

2 回答 2

4

有几件事我会改变。

首先,让我们暂时忽略 return 并关注与 thatboost::any混合的内容std::string。这对于事件系统来说确实是个坏主意,因为它允许潜在的运行时类型不匹配。您只是不想要一种可能会犯此类错误的设计(因为最终会犯这些错误,并且出现需要修复的错误,从而浪费未来的时间)。

通常,您真正想要的是:

void Module::trigger_event(shared_ptr<Event> const& event)
{
    // Event is a base interface
    // pull off event->ID() to get identifier and lookup
    // dispatch to something that has the signature
    //   void (shared_ptr<SpecificEvent>)
}

做类似调度员的事情,你通常有

map<IDType, shared_ptr<Dispatcher>> dispatchDictionary_;

template <typename SpecificEvent>
class SpecificDispatcher : public Dispatcher
{
public:
    SpecificDispatcher(function<void (shared_ptr<SpecificEvent>)> handler)
        handler_(handler)
    {}

    virtual void dispatch(shared_ptr<Event> event)
    {
        auto specificEvent(static_ptr_cast<SpecificEvent>(event));

        handler_(specificEvent);
    }
};

(使用以明显方式定义的接口类和注册/注销方法来关联映射中的事件类型 ID)。关键是将 ID 与类中的 EventType 相关联,以确保它始终是相同的关联,并以处理程序本身不需要重新解释数据的方式进行调度(容易出错)。做可以在你的图书馆内自动化的工作,这样就不会在外面做错了。

好的,现在你用你的方法返回什么?返回一个对象,而不是一个函数。如果他们不需要显式调用它,那么您已经保存了另一个潜在的错误来源。就像所有 RAII 一样 - 您不希望人们必须记住调用删除、解锁或......类似地,使用“unregister_event”(或任何你称之为的)。

人们必须在程序中关注四个生命周期:表达式、函数范围、状态和程序。这些对应于它们可以存储您返回的对象的四种类型的东西:匿名(让它掉在地板上 - 它可以在表达式中使用但永远不会分配给命名对象),自动范围对象,对象它是存在于两个异步转换事件之间的 State 类(在 State 模式中)的成员,或者是在退出之前一直存在的全局范围对象。

所有生命周期管理都应使用 RAII 进行管理。这是析构函数的重点,也是面对异常时应该如何管理资源清理。


编辑:我有点留下一堆未说明的部分,指出你会“以明显的方式”连接这些点。但我想我会填写更多的部分(因为我正在构建和安装操作系统,我的错误现在已经下降到最后一个,正在等待安装......)

关键是有人应该能够打字

callbackLifetimeObject = module->set_event<StartEvent>([](shared_ptr<StartEvent> event){
    cout << "Module started: " << event->someInfo() << endl;
});

所以 set_event 需要有那种签名来接受这个,它应该将适当的调度程序插入到字典中。有几种方法可以从此处的类型中获取 ID。“显而易见”的方式是创建一个临时的并将其称为“ID”成员——但这会产生对象创建开销。另一种方法是将其转换为“虚拟静态”,然后获取静态(这也是虚拟方法所做的所有事情)。每当我有虚拟静态时,我倾向于将它们转化为特征——同样的东西,但封装稍微好一些,并且还能够修改“已经关闭的类”。然后虚方法也只是调用特征类来返回相同的东西。

所以...

template <typename EventType>
struct EventTrait
{
    // typedef your IDType from whatever - looks like you want std::string
    static IDType eventID()
    { /* default impl */ }
};

template <>
struct EventTrait<StartEvent>
{
    // same as above
    static IDType eventID()
    { return "Start"; }
};

那么你就可以

template <typename EventType>
EventRegistration set_event(function<void (shared_ptr<EventType>)> handler)
{
    auto id(EventTrait<EventType>::eventID());

    dispatchDictionary_.insert(make_pair(id, 
        make_shared<SpecificDispatcher<EventType>>(handler)));

    return EventRegistration(bind(& Module::unset_event, this, id));
}

这应该更好地说明如何将这些部分组合在一起以及您拥有的一些选项。其中一些说明了可能随每个事件重新编码的样板代码。这也是您可能想要自动化的类型。有很多先进的技术可以对这种生成进行元编程,从使用另一个需要规范的构建步骤到在 c++ 元编程系统内部工作。同样,与前面的几点一样,您可以自动化的越多,您遇到的错误就越少。

于 2012-04-06T01:14:33.653 回答
1

它有问题有两个原因。

首先,您将事件限制为只接收一个参数的处理程序,同时从您的用例判断您可能有各种不同的事件。所以,这是有限制的(是的,您可以将 anstd::tuple放入 any 类型中,以编码多个参数,但您真的想要这个吗?)。

更重要的是,削弱类型安全意味着削弱(类型)安全。如果您的事件处理程序只能与山羊一起工作,请不要给它一只羔羊。你想要的,似乎可以用模板来做(至少在 C++11 中),因为你可以创建trigger_event一个接收任意数量参数的类型函数。然后它会尝试使用此参数调用处理程序,这将产生编译错误,如果您没有提供正确数量和类型的参数。

在这里,在您的调度程序中,您可以隐藏类型解析,这有点棘手,因为您想存储任意类型的事件。这可能是使用的地方any,但保留您的调度程序实现/逻辑中,不会泄露给您的用户。

编辑:为了实现这个解决方案,我的方法是拥有一些内部抽象Event类型(您可以存储指针)和一些template <typename Hnd> classs ConcreteEvent : public Event允许您将客户事件处理程序保留为ConcreteEvent<EventType>(假设EventType是您的set_event方法的模板参数)。Event*使用 rtti 您可以稍后在检索它时检查它是否与您要调用的类型匹配(您只会将其检索为)。

于 2012-04-06T01:07:56.543 回答