深入理解信号槽(二)

多对多

下一个问题是,我们能够在点击一次重新载入按钮之后做多个操作吗?也就是让信号和槽实现多对多的关系?

实际上,我们只需要利用一个普通的链表,就可以轻松实现这个功能了。比如,如下的实现:

class MultiAction : public AbstractAction
    // ...an action that is composed of zero or more other actions;
    // executing it is really executing each of the sub-actions
{
public:
    // ...
    virtual void execute();
private:
    std::vector<AbstractAction*> actionList_;
    // ...or any reasonable collection machinery
};

void MultiAction::execute()
{
    // call execute() on each action in actionList_
    std::for_each( actionList_.begin(),
                   actionList_.end(),
                   boost::bind(&AbstractAction::execute, _1) );
}

这就是其中的一种实现。不要觉得这种实现看上去没什么水平,实际上我们发现这就是一种相当简洁的方法。同时,不要纠结于我们代码中的 std:: 和 boost:: 这些命名空间,你完全可以用另外的类,强调一下,这只是一种可能的实现。现在,我们的一个动作可以连接多个 button 了,当然,也可以是别的 Action 的使用者。现在,我们有了一个多对多的机制。通过将 AbstractAction* 替换成 boost::shared_ptr<AbstractAction>,可以解决 AbstractAction 的归属问题,同时保持原有的多对多的关系。

这会有很多的类!

如果你在实际项目中使用上面的机制,很多就会发现,我们必须为每一个 action 定义一个类,这将不可避免地引起类爆炸。至今为止,我们前面所说的所有实现都存在这个问题。不过,我们之后将着重讨论这个问题,现在先不要纠结在这里啦!

特化!特化!

当我们开始工作的时候,我们通过将每一个 button 赋予不同的 action,实现 Button 类的重用。这实际是一种特化。然而,我们的问题是,action 的特化被放在了固定的类层次中,在这里就是这些 Button 类。这意味着,我们的 action 很难被更大规模的重用,因为每一个 action 实际都是与 Button 类绑定的。那么,我们换个思路,能不能将这种特化放到信号与槽连接的时候进行呢?这样,action 和 button 这两者都不必进行特化了。

函数对象

将一个类的函数进行一定曾度的封装,这个思想相当有用。实际上,我们的 Action 类的存在,就是将 execute() 这个函数进行封装,其他别无用处。这在 C++ 里面还是比较普遍的,很多时候我们用 ++ 的特性重新封装函数,让类的行为看起来就像函数一样。例如,我们重载 operator() 运算符,就可以让类看起来很像一个函数:

class AbstractAction
{
public:
    virtual void operator()() = 0;
};

// using an action (given AbstractAction& action)
action();

这样,我们的类看起来很像函数。前面代码中的 for_each 也得做相应的改变:

// previously
std::for_each( actionList_.begin(),
               actionList_.end(),
               boost::bind(&AbstractAction::execute, _1) );
// now
std::for_each( actionList_.begin(),
               actionList_.end(),
               boost::bind(&AbstractAction::operator(), _1) );

现在,我们的 Button::clicked() 函数的实现有了更多的选择:

// previously
action_->execute();

// option 1: use the dereferenced pointer like a function
(*action_)();

// option 2: call the function by its new name
action_->operator()();

看起来很麻烦,值得这样做吗?

下面我们来试着解释一下信号和槽的目的。看上去,重写 operator() 运算符有些过分,并不值得我们去这么做。但是,要知道,在某些问题上,你提供的可用的解决方案越多,越有利于我们编写更简洁的代码。通过对一些类进行规范,就像我们要让函数对象看起来更像函数,我们可以让它们在某些环境下更适合重用。在使用模板编程,或者是 Boost.Function,bind 或者是模板元编程的情形下,这一点尤为重要。

这是对无需更多特化建立信号槽连接重要性的部分回答。模板就提供了这样一种机制,让添加了特化参数的代码并不那么难地被特化,正如我们的函数对象那样。而模板的特化对于使用者而言是透明的。

松耦合

现在,让我们回顾一下我们之前的种种做法。

我们执着地寻求一种能够在同一个地方调用不同函数的方法,这实际上是 C++ 内置的功能之一,通过 virtual 关键字,当然,我们也可以使用函数指针实现。当我们需要调用的函数没有一个合适的签名,我们将它包装成一个类。我们已经演示了如何在同一地方调用多个函数,至少我们知道有这么一种方法(但这并不是在编译期完成的)。我们实现了让“信号发送”能够被若干个不同的“槽”监听。

不过,我们的系统的确没有什么非常与众不同的地方。我们来仔细审核一下我们的系统,它真正不同的是:

  • 定义了两个不同的术语:“信号”和“槽”;
  • 在一个调用点(信号)与零个或者多个回调(槽)相连;
  • 连接的焦点从提供者处移开,更多地转向消费者(也就是说,Button 并不需要知道如何做是正确的,而是由回调函数去告知 Button,你需要调用我)。

但是,这样的系统还远达不到松耦合的关系。Button 类并不需要知道 Page 类。松耦合意味着更少的依赖;依赖越少,组件的可重用性也就越高。

当然,肯定需要有组件同时知道 Button 和 Page,从而完成对它们的连接。现在,我们的连接实际是用代码描述的,如果我们不用代码,而用数据描述连接呢?这么一来,我们就有了松耦合的类,从而提高二者的可重用性。

新的连接模式

什么样的连接模式才算是非代码描述呢?假如仅仅只有一种信号槽的签名,例如 void (*signature)(),这并不能实现。使用散列表,将信号的名字映射到匹配的连接函数,将槽的名字映射到匹配的函数指针,这样的一对字符串即可建立一个连接。

然而,这种实现其实包含一些“握手”协议。我们的确希望具有多种信号槽的签名。在信号槽的简短回答中我们提到,信号可以携带附加信息。这要求信号具有参数。我们并没有处理成员函数与非成员函数的不同,这又是一种潜在的函数签名的不同。我们还没有决定,我们是直接将信号连接到槽函数上,还是连接到一个包装器上。如果是包装器,这个包装器需要已经存在呢,还是我们在需要时自动创建呢?虽然底层思想很简单,但是,真正的实现还需要很好的努力才行。似乎通过类名能够创建对象是一种不错的想法,这取决于你的实现方式,有时候甚至取决于你有没有能力做出这种实现。将信号和槽放入散列表需要一种注册机制。一旦有了这么一种系统,前面所说的“有太多类了”的问题就得以解决了。你所需要做的就是维护这个散列表的键值,并且在需要的时候实例化类。

给信号槽添加这样的能力将比我们前面所做的所有工作都困难得多。在由键值进行连接时,多数实现都会选择放弃编译期类型安全检查,以满足信号和槽的兼容。这样的系统代价更高,但是其应用也远远高于自动信号槽连接。这样的系统允许实例化外部的类,比如 Button 以及它的连接。所以,这样的系统有很强大的能力,它能够完成一个类的装配、连接,并最终完成实例化操作,比如直接从资源描述文件中导出的一个对话框。既然它能够凭借名字使函数可用,这就是一种脚本能力。如果你需要上面所说的种种特性,那么,完成这么一套系统绝对是值得的,你的信号槽系统也会从中受益,由数据去完成信号槽的连接。

对于不需要这种能力的实现则会忽略这部分特性。从这点看,这种实现就是“轻量级”的。对于一个需要这些特性的库而言,完整地实现出来就是一个轻量级实现。这也是区别这些实现的方法之一。



本文转自 FinderCheng 51CTO博客,原文链接: 

http://blog.51cto.com/devbean/424778

上一篇:[C# 基础知识系列]专题十四:深入理解Lambda表达式


下一篇:解决DataGridView在多线程中无法显示滚动条的问题