设计C++回调模型(一):设计模式

作者:[email protected] 新浪微博@孙雨润 新浪博客 CSDN博客日期:2012年11月10日

1. 什么是回调

开发中经常遇到等待其他模块事件通知的情况,例如:

  • 用户点击UI上button的事件,通知给相关函数处理逻辑
  • Model中数据改变的事件,通知给相关View模块刷新界面
  • 异步IO完成的事件,通知给处理函数确认成功还是失败
  • 客户端向服务器发N种不同请求,服务器为每种请求准备好处理函数

这些等待通知的函数被执行的过程就是回调的过程,所以回调是一个很常见很简单的事情,接下来我们一起看一下C++中有哪些方法可以完成回调。

2. 函数指针

这是最简单最原始的方法,从C语言开始就被广泛使用尽人皆知。比如Windows的窗口过程回调函数,线程运行函数,linux内核与驱动等。例如下载文件完成的通知:

#include <iostream>
#include <string>
using namespace std;

typedef void (*DownloadHandler)(const string& url, unsigned ec);
void doDownloadJob(const string& url, DownloadHandler pHandler) {
    // be busy doing sth. with downloading
    if (pHandler != nullptr) {
        pHandler(url, 0);
    }
}
void onDownloadComplete(const string& url, unsigned ec) {
    cout << "file " << url << " finished downloading, ec = " << ec << endl;
}

int main() {
    doDownloadJob("http://yy.com/music/spring.mp3", onDownloadComplete);
}

【Note】C++0x标准中新加了关键词nullptr,并推荐用来进行空指针测试。具体原因会在本博客C++0x新feature进行介绍。

使用C++的OO风格,需要将普通函数指针换成成员函数指针。关于函数指针后续会有深入的讨论,这里只用最简单的形式:

class Client;
typedef void (Client::*ClientMemFun)(const string&, unsigned);
class Downloader {
public:
    void doDownloadJob(const string& url, Client* clientOwner, ClientMemFun pHandler) {
        // be busy doing sth. with downloading
        if ((clientOwner != nullptr) && (pHandler != nullptr)) {
            (clientOwner->*pHandler)(url, 0);
        }
    }
};
class Client {
public:
    void startDownload(const string& url) {
        cout << "start to download file " << url << endl;
        m_downloader.doDownloadJob(url, this, &Client::onDownloadComplete);
    }
    void onDownloadComplete(const string& url, unsigned ec) {
        cout << "file " << url << " finished downloading, ec = " << ec << endl;
    }
private:
    Downloader m_downloader;
};

int main() {
    Client c;
    c.startDownload("http://yy.com/music/spring.mp3");
}

在第C和C++的例子中都为为下载成功的通知准备了处理函数onDownloadCompleteClient::onDownloadComplete,实现了下载成功后接受通知被动调用。

问题1: 刚刚例子中是一个用户Client使用自己的下载工具Downloader,只有自己对此感兴趣,下载完成后通知自己很合理;如果是一位交警在指挥,N个司机等待交警的手势通知,上边的函数指针方法就很难实现了。而观察者模式能有效解决这种一对多的通知方式。

3. 观察者模式

观察者模式是最常用的几个设计模式之一,想必大家都不陌生。思路就是一群人作为观察者,盯着一个发布信息的人,并根据信息有所行动。我这里想到的交警司机的例子能够生动的描述这种模式。

#include <iostream>
#include <string>
#include <list>
#include <vector>
using namespace std;

enum Direction {NORTH, EAST, SOUTH, WEST};
class Driver {
public:
    void onPolicePointTo(Direction direction) {
        if (direction == NORTH) {
            cout << "I'm gonna buckle up and go!" << endl;
        }
    }
};
class TrafficPolice {
public:
    void registerDriver(Driver* pDriver) {
        m_drivers.push_back(pDriver);
    }
    void PointTo(Direction direction) {
        for (list<Driver*>::iterator it = m_drivers.begin(); it != m_drivers.end(); ++it) {
            if ((*it) != nullptr) {
                (*it)->onPolicePointTo(direction);
            }
        }
    }
private:
    list<Driver*> m_drivers;
};

int main() {
    // Step1: 一共有20个司机开车在路上
    unsigned driverCount = 20;
    vector<Driver*> driverGroup;
    driverGroup.reserve(driverCount);
    for (unsigned i = 0; i < driverCount; ++i) {
        driverGroup.push_back(new Driver);
    }
    // Step2: 十字路口有一位交警
    TrafficPolice trafficPolice;
    // Step3: 交警把这些司机都注册到自己这边
    for (vector<Driver*>::iterator it = driverGroup.begin(); it != driverGroup.end(); ++it) {
        trafficPolice.registerDriver(*it);
    }
    // Step4: 交警指向北方,司机立刻行动
    trafficPolice.PointTo(NORTH);
}

这段代码能够实现一对多的通知,不过还有些不妥。

问题2:当交警指向北方时,有的司机需要右转 有的需要刹车,有的要启动。如何在一次通知中让不同司机能做不同的事情呢?需要用到接口的概念。

#include <iostream>
#include <string>
#include <list>
#include <vector>
using namespace std;

enum Direction {NORTH, EAST, SOUTH, WEST};
class IDriver {
public:
    virtual void onPolicePointTo(Direction direction) = 0;
};
class DriverA : public IDriver {
public:
    void onPolicePointTo(Direction direction) {
        if (direction == NORTH) {
            cout << "I'm gonna buckle up and go!" << endl;
        }
    }
};
class DriverB : public IDriver {
public:
    void onPolicePointTo(Direction direction) {
        if (direction == NORTH) {
            cout << "Oh shit, I have to stop!" << endl;
        }
    }
};
class TrafficPolice {
public:
    void registerDriver(IDriver* pDriver) {
        m_drivers.push_back(pDriver);
    }
    void PointTo(Direction direction) {
        for (list<IDriver*>::iterator it = m_drivers.begin(); it != m_drivers.end(); ++it) {
            (*it)->onPolicePointTo(direction);
        }
    }
private:
    list<IDriver*> m_drivers;
};
IDriver* createDriver(const string& type) {
    if (type == "A") {
        return new DriverA;
    } else if (type == "B") {
        return new DriverB;
    } else {
        return nullptr;
    }
}
void initDriver(const string& type, unsigned count, vector<IDriver*>& out) {
    out.reserve(count);
    for (unsigned i = 0; i < count; ++i) {
        out.push_back(createDriver(type));
    }
}

int main() {
    vector<IDriver*> driverGroupA;
    vector<IDriver*> driverGroupB;
    initDriver("A", 5, driverGroupA);
    initDriver("B", 10, driverGroupB);

    TrafficPolice trafficPolice;
    for (vector<IDriver*>::iterator it = driverGroupA.begin(); it != driverGroupA.end(); ++it) {
        trafficPolice.registerDriver(*it);
    }
    for (vector<IDriver*>::iterator jt = driverGroupB.begin(); jt != driverGroupB.end(); ++jt) {
        trafficPolice.registerDriver(*jt);
    }
    trafficPolice.PointTo(NORTH);
}

利用纯虚函数(接口)的概念,现在做到了同一通知,不同观察者做不同的事情。可是还是有一个地方怪怪的:

问题3:交警为什么要承担注册司机的职责呢?事实上现实生活中信号源往往并不关心信号接收者:演唱会歌手不需要对每位观众都做一次类似register这种操作,GPS信号源也不会关心地面上有哪些车载导航;相反是观众对歌手感兴趣、车载导航对GPS信号源感兴趣,所以才接受对方信号。这种松耦合的状态才更为真实地描述了对象之间的关系。

稍微修改一下Driver类可以使交警看起来摆脱注册司机的职责:

class IDriver {
public:
    virtual void ObservePolice(TrafficPolice* pPolice) {
        pPolice->registerDriver(this);
    }
    virtual void onPolicePointTo(Direction direction) = 0;
};

问题4:看起来还是怪怪的,首先交警虽然不主动调用registerDriver,但还是要提供这样一个功能,供Driver注册上来,实际上应该是交警发出的信号(手势、哨子)去提供通知的功能;然后交警还需要知道如何通知给Driver,也就是需要知道回调函数的形式;更主要的是,ObservePolice只能用来接收交警类型的对象指针,Driver还要提供越来越多的接口来观察信号灯、前车车距、后车车距等等。

4. signal/slot (信号/槽)

signal/slot是这样一个朴素的想法:

  • 信号源发出信号,不关心接受者
  • 接受者了解信号并主动接收信号,并决定如何处理
  • 多个接受者可以关注同一个信号源
  • 一个接受者可以关注多个信号源
  • 一个信号源可以关注另一个信号源

很多脚本语言尤其是UI相关的语言,都在语法层面上支持signal/slot,例如as3的addEventListener、C#的delegate等,这项功能也被称为事件驱动。C++要实现signal/slot确实要做一些工作,但是已经有很多个实现版本了:

  • Qt使用moc预处理器,对signal/slot生成了类似观察者模式的代码 了解更多
  • boost::signal类综合使用了仿函数、boost::bind等工具 了解更多
  • C++ 仿照C# Delegate方式的小型库 了解更多 猛击此处下载源码
  • 另一个C++仿照C# Delegate的小型库 性能很好 了解更多 猛击此处下载源码
  • 用2000行的.h文件实现的signal/slot 了解更多 猛击此处下载源码

其中最后一个以其简洁方便安全最受欢迎,被用在google的多个开源项目中,也被本人用在公司的几个项目中取代观察者模式,以松耦合的方式设计架构,思路清晰开发高效。这里着重讲一下这个工具的使用方法与实现方式。

4.1 使用方法

以工作中一个场景为例:在一个聊天室内有多个用户,房间公告、主持人列表的变化都要广播给房间所有用户。

////////////// @file: hostess.h//////////////
#include "sigslot.h"
struct HostessInfo {
    // ...;
};
class Hostess {
public:
    sigslot::signal1<const HostessInfo&> hostessInfoChanged;
};

///////////// @file: bulletin.h//////////////
#include "sigslot.h"
struct BulletinInfo {
    // ...;
};
class Bulletin {
public:
    sigslot::signal1<const BulletinInfo&> BulletinInfoChanged;
};

///////////// @file: room.h///////////////
#include "hostess.h"
#include "bulletin.h"
#include "client.h"
#include <map.h>
class Room {
public:
    void addClient(uint32_t id) {
        m_client.insert(make_pair(id, new Client(this)));
    }
    void removeClient(uint32_t id) {
        m_client.erase(id);
    }
    Hostess m_hostess;
    Bulletin m_bulletin;
    map<uint32_t, Client*> m_client;
}

/////////////// @file: client.h///////////////
#include "hostess.h"
#include "bulletin.h"
class Room;
class Client : public sigslot::has_slots<> {
public:
    explicit Client(Room* pFacade);
    void onHostessInfoChanged(const HostessInfo& info);
    void onBulletinInfoChanged(const BulletinInfo& info);
}

///////////////// @file client.cpp///////////
#include "client.h"
#include "room.h"
Client::Client(Channel* pFacade) {
    pFacade->m_hostess.connect(this, &Client::onHostessChanged);
    pFacade->m_bulletin.connect(this, &Client::onBulletinInfoChanged);
}

当Hostess和Bulletin发生改变时只需要调用两个信号的emit,就能够实现广播回调。

4.2 实现方法

这个库以模板的形式实现了0~8个参数的signal/slot,如果需要更多参数则需要自行扩展。好消息是C++0x模板不定参数的诞生使得以后再不必为了参数个数而将相同逻辑的代码复制8次。我之后准备利用C++0x模板不定参数简化这个库到几百行代码范围内。

还是先来看实现方法:以不带参数的signal/slot为例

4.2.1 _connection_base0

template<class mt_policy>
class _connection_base0
{
public:
    virtual ~_connection_base0() {}
    virtual has_slots<mt_policy>* getdest() const = 0;
    virtual void emit() = 0;
    virtual _connection_base0* clone() = 0;
    virtual _connection_base0* duplicate(has_slots<mt_policy>* pnewdest) = 0;
};

4.2.2 _signal_base0

template<class mt_policy>
class _signal_base : public mt_policy
{
public:
    virtual void slot_disconnect(has_slots<mt_policy>* pslot) = 0;
    virtual void slot_duplicate(const has_slots<mt_policy>* poldslot, has_slots<mt_policy>* pnewslot) = 0;
};
template<class mt_policy>
class _signal_base0 : public _signal_base<mt_policy>
{
public:
    typedef std::list<_connection_base0<mt_policy> *>  connections_list;
    _signal_base0(){;}
    _signal_base0(const _signal_base0& s) : _signal_base<mt_policy>(s) {
        lock_block<mt_policy> lock(this);
        typename connections_list::const_iterator it = s.m_connected_slots.begin();
        typename connections_list::const_iterator itEnd = s.m_connected_slots.end();
        while(it != itEnd) {
            (*it)->getdest()->signal_connect(this);
            m_connected_slots.push_back((*it)->clone());
            ++it;
        }
    }
    ~_signal_base0() {
        disconnect_all();
    }
    bool is_empty()
    {
        lock_block<mt_policy> lock(this);
        typename connections_list::const_iterator it = m_connected_slots.begin();
        typename connections_list::const_iterator itEnd = m_connected_slots.end();
        return it == itEnd;
    }
    void disconnect_all()
    {
        lock_block<mt_policy> lock(this);
        typename connections_list::const_iterator it = m_connected_slots.begin();
        typename connections_list::const_iterator itEnd = m_connected_slots.end();
        while(it != itEnd) {
            (*it)->getdest()->signal_disconnect(this);
            delete *it;
            ++it;
        }
        m_connected_slots.erase(m_connected_slots.begin(), m_connected_slots.end());
    }
    void disconnect(has_slots<mt_policy>* pclass)
    {
        lock_block<mt_policy> lock(this);
        typename connections_list::iterator it = m_connected_slots.begin();
        typename connections_list::iterator itEnd = m_connected_slots.end();
        while(it != itEnd) {
            if((*it)->getdest() == pclass) {
                delete *it;
                m_connected_slots.erase(it);
                pclass->signal_disconnect(this);
                return;
            }
            ++it;
        }
    }
    void slot_disconnect(has_slots<mt_policy>* pslot)
    {
        lock_block<mt_policy> lock(this);
        typename connections_list::iterator it = m_connected_slots.begin();
        typename connections_list::iterator itEnd = m_connected_slots.end();
        while(it != itEnd) {
            typename connections_list::iterator itNext = it;
            ++itNext;
            if((*it)->getdest() == pslot) {
                delete *it;
                m_connected_slots.erase(it);
            }
            it = itNext;
        }
    }
    void slot_duplicate(const has_slots<mt_policy>* oldtarget, has_slots<mt_policy>* newtarget)
    {
        lock_block<mt_policy> lock(this);
        typename connections_list::iterator it = m_connected_slots.begin();
        typename connections_list::iterator itEnd = m_connected_slots.end();

        while(it != itEnd)
        {
            if((*it)->getdest() == oldtarget)
            {
                m_connected_slots.push_back((*it)->duplicate(newtarget));
            }

            ++it;
        }
    }

protected:
    connections_list m_connected_slots;
};

4.2.3 has_slots

template<class mt_policy = SIGSLOT_DEFAULT_MT_POLICY>
class has_slots : public mt_policy
{
private:
    typedef typename std::set<_signal_base<mt_policy> *> sender_set;
    typedef typename sender_set::const_iterator const_iterator;
public:
    has_slots(){;}
    has_slots(const has_slots& hs) : mt_policy(hs) {
        lock_block<mt_policy> lock(this);
        const_iterator it = hs.m_senders.begin();
        const_iterator itEnd = hs.m_senders.end();
        while(it != itEnd) {
            (*it)->slot_duplicate(&hs, this);
            m_senders.insert(*it);
            ++it;
        }
    }
    void signal_connect(_signal_base<mt_policy>* sender) {
        lock_block<mt_policy> lock(this);
        m_senders.insert(sender);
    }
    void signal_disconnect(_signal_base<mt_policy>* sender) {
        lock_block<mt_policy> lock(this);
        m_senders.erase(sender);
    }
    virtual ~has_slots() {
        disconnect_all();
    }
    void disconnect_all() {
        lock_block<mt_policy> lock(this);
        const_iterator it = m_senders.begin();
        const_iterator itEnd = m_senders.end();
        while(it != itEnd) {
            (*it)->slot_disconnect(this);
            ++it;
        }
        m_senders.erase(m_senders.begin(), m_senders.end());
    }
private:
    sender_set m_senders;
};

容易看出,signal/connect/has_slots<>这三个部分还是组成了一个观察者模式。与我们之前自己实现的观察者不同的是:

  • 模板使任意类型参数成为了可能
  • has_slot使所有观察者具有统一接口

google的libjingle开源项目中曾经将这个库添加了一个将近100行的文件,sigslotrepeater.h,用于处理signal A 绑定 signal B 而不用中转到一个slot函数中处理。实际上这个模型并不是线程安全的(尽管开发者曾模棱两可的声明过线程安全)。后续章节将从多线程的角度深入分析C++的回调模型。

  • 如果这篇文章对您有帮助,请到CSDN博客留言;
  • 转载请注明:来自雨润的技术博客 http://blog.csdn.net/sunyurun

你可能感兴趣的:(设计模式,C++,C++,回调,Signal,信号槽)