第九章 泛化信号模式

9.1 泛化信号模式简述

我的最初看到信号模式是来自Boost::Signals,在其中说道“The Boost.Signals library is an implementation of a managed signals and slots system. Signals represent callbacks with multiple targets, and are also called publishers or events in similar systems. Signals are connected to some set of slots, which are callback receivers (also called event targets or subscribers), which are called when the signal is "emitted."” 翻译过来意思是:“Boost.Signals 库是一个可控的信号插槽(signals and slots)系统的实现。信号代表了具有多个目标的回调,在类似的系统中也称为发布者(publisher)或事件(event)。 信号与某组插槽相连接,插槽即回调的接收者(也称为事件目标,event target,或订阅者,subscriber),它们在信号“发出”时被调用。”

其实简单的说就是储存很多事件(event)的一个容器,在需要的时候可以执行容器中所有的这些事件。有点类似一个学校里一个班上有很多学生,但是当上课铃响起的时候,所有的学生都统一回到教室坐到自己的位置上,无论学生有再多,我们只需统一的一个上课铃声就可以向所有学生发出信号。但是学校外面的小孩或者其他学校的学生就不会响应这个铃声信号。这在概念上也就是说所有响应这个信号的学习必须是这个学校登记管理的学生,学校也就成了学生的容器,而信号也正是学校这个容器发出的。

虽然看过Boost::Signals后会发觉其没有什么太大缺点,但是由于我们后面的观察者模式的泛化需要用到信号模式,而且Boost::Signals中对于不定模版参数的处理总是不怎么理想,所以这里我想采用我自己的方法泛化信号模式,最主要的是会用到我们前面一直用到的数据类型容器(TypeVector)。这看起来比Boost::Signals中采用的方法会先进一些。

 

9.1.1 定义

       通过将一系列事件放入信号插槽中,当信号发出时背调用。

 

9.1.2 泛化目的

       传统的信号模式必须事件之间具有一定得耦合性(继承关系),这时候会很大的降低代码的复用性,使得一些已经设计好的老代码无法直接加入到信号插槽中。泛化信号模式可以很大程度的降低事件之间的耦合性,使得老代码也可以直接空啊日大炮信号插槽中。

 

9.1.3 适用场合

 

       一些时候我们需要对一些事件进行统一的调用(响应信号),这时候我们就可以用到泛化信号模式。

 

9.2 泛化信号模式不足

       以容器存储信号就会牵扯到一个问题,所有的信号也许都会有一个返回值,由于为了让程序看上去不是更复杂,所以我没有对返回值进行专门的存储管理,但是对于一个完整的大程序,这里必须注意到返回值的处理。如果不需要使用返回值,这个程序足矣。但是返回值的问题始终还是其不足,即便是即将加入C++标准库的boost库中的信号模式也没能很好的解决返回值问题。

       返回值还不是唯一困扰我们的,由于泛化的限制,使得所有的信号必须保证拥有一样的参数类型以及参数个数,但是在实际应用中也许我们并不能保证总是满足这一点。不过解决方法是有的,在十一章介绍的泛化仿重载模式就是如何应对不同参数类型以及参数个数的回调。

 

9.3实作泛化信号模式

9.3.1 我们心中的信号模式

       信号模式其实就是一系列算法的一个统一调度,现在我们先来看看泛化信号模式会达到一个什么样的效果:

//定义一个信号

struct HelloWorld

{

  void operator()() const

  {

    std::cout << "Hello, World!" << std::endl;

  }

};

 

//构造一个信号容器

Signal<void> sig;

 

//创建信号对象

HelloWorld hello;

//将信号放入插槽

sig.connect(hello);

//调用信号容器

sig();

 

       我们先做出上面的代码的目的是要规划我们将怎么实作泛化信号模式。在创建信号容器时候我们必须给定几个模板参数(返回值,信号参数),其中一些可以藉由缺省模板参数的技法简化,就像我们代码中的实际只给了一个模板参数。

当信号容器创建以后我们必须维护这个信号容器,可以随时将信号放入插槽,这里我们还可以做得更多,而不光是我们看到的那样。

在我们需要的时候我们随时可以通过信号容器的一个成员仿函数调用所有的信号,这是比较简单的方法。

 

9.3.2 通过模板判断类型

       “万事俱备,只欠东风”,在实作中我们会用到一个技法,在这里我提前讲解。C++的模板带来了很大的便利,有时候我们也可以不光常规的使用模板,我们可以通过编译器特化模板时对类型的辨别做到一些看似困难的事。

       当我们拥有两个对象的时候,我们有时候会想知道这两个对象是否具有某些关系,例如这两个对象是否具有相同的类型。这个时候很多人都会异口同声的回答到可以通过RTTI判断类型是否相同,C++中似乎没有专门处理这类事情的关键字,所以我们只有依靠RTTI做判断(可以通过type_id::name()判断两个对象是否是同个数据类型)。不过事情通常也不是那么简单,我们在更多时候可能还需要知道这两个对象之间是否有继承关系且谁是谁的父类,这时候RTTI似乎就显得没那么可靠了,因为RTTI无法提供足够的信息以判断继承关系。

       如果这个时候你想到C++的一个特性——隐式类型转换,那你就离解决这个难点不远了。C++一个常被人诟病又常给程序员带来便利的性质就是其容许一些数据类型的隐式转换,而这个转换不会通知程序员,也不会进行任何额外的安全判断,这就是C++灵活特性之一,也是魅力之一。当这个性质和模板很好的结合时候,就可完成我们前面提到的判断两个对象的继承关系的问题。

       现在请你跟随我的脚步踏入这灵活美丽的技法:

//做一个结构体判断两个型别是否是相同的型别

template<typename Base,typename Child>

struct Inheritance

{

    //利用模板的偏特化

    template<typename T> struct IsBase

    {

        enum{value = 0};

    };

 

    //如果上面的不匹配,这个必然匹配,因为...是最低型别,必然和任何型别匹配

    template<>

    struct IsBase<Base>

    {

        enum{value = 1};

    };

   

    enum {value = IsBase<Child>::value};

};

       Inheritance通过内含的模板偏特化完成了判断,我们通过获取Inheritance::value就可以间接调用其内部的偏特化结构体IsBase。当Base和Child有继承关系时候,编译器会特化IsBase的第二个版本代码,这样就使得value值为1,如果其没有继承关系,编译器就会选用一个一般特化代码,也就是IsBase的第一个版本代码,这时候value的值就会赋值为0,最后我们就可以通过判断value的值而得到这两个类型是否具有继承关系。还需要强调的一点是上面的代码完全在编译期完成,也就是说对于我们的程序使用是一定程度上“免费”的。

       有了上面的代码其实还不够,因为很多时候我们知道的只是两个对象,而不是两个数据类型,所以我们还需要对上面的结构体进行简单的封装以满足我们更苛刻的需要。

//下面这个函数是判断两个型别是否相同,如果两个型别能满足相互为父类,就是代表相同

template <typename T1,typename T2>

bool IsSame(T1,T2)

{

    return  Inheritance<T1,T2>::value == Inheritance<T2,T1>::value;

}

       IsSame函数通过接收两个参数而提取其数据类型,其内部调用Inheritance,很有趣的是Inheritance是单向的判断两个类型是否有继承关系,当需要判断两个类型是否相等就只需要再判断一次T2是否是T1的父类,只有当两个类型互为父类,就代表两个类型相等。

       未来在我的程序中只需要判断两个类型是否相等,不需要判断继承关系,但是实作判断继承关系的函数已经不难,这里我就不再赘述,有兴趣的读者可以思考一下。

       IsSame带来的不仅是思维的创新,还是代码的效率,其效率会远远高于通过RTTI判断两个对象的类型是否相等。

 

9.3.3 实作泛化信号模式

       信号模式中最重要的就是存储信号,这里可以选择的方法很多,比较高阶的方法可以参考我前面介绍的策略模式,但是为了更好的提高效率,我下面的代码将采用更复杂的办法,就是直接存储信号的地址。之所以这个办法可行的原因是信号的返回值类型以及参数类型我们在构造信号容器的时候就已经知道了,存储指针能够节约很多高阶的封装所消耗的效率,在使用的时候我们又可以通过足够的类型信息将其还原。

       模板参数我们依然采用惯常的返回值加参数类型容器的方法。需要再次强调的是我们依然假设参数个数至多只有两个,这里不是因为技术问题,而是为了缩短文章篇幅。

 

template< typename ResultType = void,typename TVector = TypeVector<> >

class Signal

{

public:

//下面是对TVector的提取,

    typedef typename ResultType ResultType;

    typedef typename TVector ParmVector;

    typedef typename TypeAt<ParmVector,0>::Result Parm1;

    typedef typename TypeAt<ParmVector,1>::Result Parm2;

//当我们接收消息的时候必须用到connect函数,这个函数接收一个仿函数或者函数指针

    template<typename Funct>

    int connect(Funct fun);

   

//这里的一个num参数是可以指定信息插入插槽的相对位置

    template<typename Funct>

    int connect(int num,Funct fun);

 

//删除函数,可以随时删除插槽中的信号

    template<typename Funct>

    void erase(Funct fun);

 

//这个对信号的调用重载

void operator () ();

void operator () (Parm1 p1);

void operator () (Parm1 p1,Parm2 p2);

 

//似乎基本功能都完成了,但是我们必须至少提供一个清空插槽的函数以及检查插槽是否被清空的函数

    void clear();

 

//判断插槽是否为空

    int empty();

 

private:

    //构造一个泛化仿函数的指针

    Functor<ResultType,ParmVector>* pCmdList;

   

    //用一个容器存储这些指针

    typedef std::vector< Functor<ResultType,ParmVector>* > CmdList;

    CmdList CmdListVect;

};

 

       上面是信号模式的整体,接下来我们来完成各个函数。

       首先是connect函数,这个可以将信号放入插槽。不光是这样,我们还希望这个函数能够通过制定相对位置的方式添加信号。

       template<typename Funct>

    int Signal::connect(Funct fun)

    {

        //创建一个新的指向行为fun的泛化仿函数对象

        pCmdList = new Functor<ResultType,ParmVector>(fun);

        //如果创建成功,就将这个泛化仿函数对象地址插入容器中

if(pCmdList)

        {

            CmdListVect.push_back(pCmdList);

            return 1;

        }

        //创建失败就返回0

        return 0;

    }

      

 

    template<typename Funct>

    int Signal::connect(int num,Funct fun)

    {

        //创建一个新的指向行为fun的泛化仿函数对象      

        pCmdList = new Functor<ResultType,ParmVector>(fun);

       

        //如果创建成功,就将这个泛化仿函数对象地址插入容器中

        if(pCmdList)

        {

            //由于制定了插入位置,所以这里需要对容器进行判断

            CmdList::iterator ite = CmdListVect.begin();

            //如果指定位置大于实际信号个数,就将其直接插入队尾

            if(num>=CmdListVect.size())

            {          

                CmdListVect.push_back(pCmdList);

            }

            //否则就将其插入制定位置

            else

            {

                ite = ite + num;

                CmdListVect.insert(ite,pCmdList);

            }

 

            return 1;

        }

        //创建失败就返回0

        return 0;

    }

 

       接下来是删除函数,由于我们存入时候是通过泛化仿函数封装,所以这里就不能做简单的判断,因为Functor并没有足够的关于fun行为的信息,而我们只有通过FunctorHandler进行判断,因为fun行为是存储在FunctorHandler。下面也将会用到前面介绍的IsSame函数判断两个fun类型是否相同。

 

    template<typename Funct>

    void Signal::erase(Funct fun)

    {

        //定义一个迭代器指向vector容器首部

        CmdList::iterator ite = CmdListVect.begin();

        while( ite != CmdListVect.end() )

        {

//做一个循环判断需要删除的fun是否和FunctorHandler中存储的_fun相同,//如果相同就停止循环查找

if( IsSame( ( ( (FunctorHandler<Funct,Functor< ResultType,ParmVector> >*) ((*ite)->spFun ) )->_fun ) ,fun ) )

                break;

            ++ite;

        }

        //如果迭代器不是指向容器尾就代表找到,进行删除。

        if(ite != CmdListVect.end())

            CmdListVect.erase(ite);

    }

 

       下面是对信号的统一调用的()操作符的重载,这比较简单。

void Signal::operator () ()

    {

        //通过循环,对容器中的指针进行逐个调用

        CmdList::iterator ite = CmdListVect.begin();

        while(ite != CmdListVect.end())

        {

            (*(*ite))();

            ite++;

        }

    }

 

    void Signal::operator () (Parm1 p1)

    {

        //通过循环,对容器中的指针进行逐个调用

        CmdList::iterator ite = CmdListVect.begin();

        while(ite != CmdListVect.end())

        {

            (*(*ite))(p1);

            ite++;

        }

    }

 

    void Signal::operator () (Parm1 p1,Parm2 p2)

    {

        //通过循环,对容器中的指针进行逐个调用

        CmdList::iterator ite = CmdListVect.begin();

        while(ite != CmdListVect.end())

        {

            (*(*ite))(p1,p2);

            ite++;

        }

    }

 

    清空函数和判断是否为空的函数都比较简单,可以直接调用容器vector的成员函数。

    void Signal::clear()

    {

        CmdListVect.clear();

    }

 

    int Signal::empty()

    {

        return CmdListVect.empty();

    }

       到此就将泛化信号模式实作完成,现在用户可以通过各种灵活的方法使用这个设计模式或将其更加完善,这里我更希望提供的是一种思想,而不是一个库函数。

 

9.4 泛化信号模式摘要

l  泛化信号模式的返回值没有更好的处理,由于更多的处理会增加更多代码,而这些代码并不是我所想讲述的重点,所以未实作。

l  IsSame函数是通过判断继承关系的模板获得两个对象的类型是否相同,这里可以举一反三的完成其他更多功能的函数。

l  泛化信号模式存储可以有多种方式,这里我采用的是容器存储泛化仿函数指针的方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值