Qt實戰7.輕量級發佈訂閱模式

1 需求描述

  1. 基於Qt實現發佈訂閱模式;
  2. 發佈的消息類型可自定義;
  3. 能夠支持多線程使用。

2 設計思路

Qt信號槽可看作的是觀察者模式的一種實現,信號槽的連接需要知道信號發送者和接收者。
但是有些情況下我們完全不需要知道發送者和接收者,發送者只管發送主題消息,接收者只管接收自己關心的主題消息,這樣使發送者和接收者完全脫耦,它們之間通過一個“中間使者”進行通信,這樣便實現了發佈訂閱模式,發送者稱爲發佈者,接收者稱爲訂閱者。

一個例子,你在微博上關注了A,同時其他很多人也關注了A,那麼當A發佈動態的時候,微博就會爲你們推送這條動態。A就是發佈者,你是訂閱者,微博就是調度中心,你和A是沒有直接的消息往來的,全是通過微博來協調的(你的關注,A的發佈動態)。

這裏設計兩個類Subscriber、Publisher,即訂閱者和發佈者,發佈者爲單例類,內部維護一個訂閱者列表,記錄訂閱者對應的主題列表,發佈主題消息的時候遍歷查詢進行發送即可。

3 代碼實現

3.1 發佈者

發佈者爲單例類,需要私有化構造、析構等函數,其對外只提供publish主題發佈接口,主題消息封裝爲QVariant,代碼如下:

#ifndef PUBLISHER_H
#define PUBLISHER_H

#include <QObject>
#include <QReadWriteLock>
#include <QHash>
#include <QSet>
#include <QStringList>
#include <QScopedPointer>

class Subscriber;

class Publisher : public QObject
{
    Q_OBJECT
public:
    /*!
     * \brief getInstance 獲取Publisher單例指針
     * \return
     */
    static Publisher *getInstance();

    /*!
     * \brief publish 發佈主題消息
     * \param topic 主題名稱
     * \param msg 消息內容
     */
    void publish(const QString &topic, const QVariant &msg);

private:
    explicit Publisher(QObject *parent = nullptr);
    ~Publisher();
    Publisher(const Publisher &other);
    Publisher& operator=(const Publisher &other);

    /*!
     * \brief add 訂閱者訂閱一個主題
     * \param object 訂閱者對象指針
     * \param topic 主題名稱
     */
    void add(Subscriber *object, const QString &topic);

    /*!
     * \brief remove 移除訂閱者訂閱的對應主題
     * \param object 訂閱者對象指針
     * \param topic 主題名稱
     */
    void remove(Subscriber *object, const QString &topic);

    /*!
     * \brief remove 移除訂閱者訂閱的所有主題
     * \param object 訂閱者對象指針
     */
    void remove(Subscriber *object);

    /*!
     * \brief getTopics 獲取訂閱者所訂閱的主題列表
     * \param object 訂閱者對象指針
     * \return
     */
    QStringList getTopics(Subscriber *object);

    friend class Subscriber;

private:
    static QReadWriteLock sm_readWriteLock;

    static QScopedPointer<Publisher> sm_instance;
    friend struct QScopedPointerDeleter<Publisher>;

    QHash<Subscriber *, QSet<QString> > m_objectTopicHash;
    QHash<QString, QVariant> m_topicLastMsgHash;
};

#endif // PUBLISHER_H
#include "Publisher.h"
#include "Subscriber.h"

#include <QDebug>
#include <QReadLocker>
#include <QWriteLocker>

QScopedPointer<Publisher> Publisher::sm_instance;
QReadWriteLock Publisher::sm_readWriteLock;
Publisher::Publisher(QObject *parent) : QObject(parent)
{
}

Publisher::~Publisher()
{
}

void Publisher::add(Subscriber *object, const QString &topic)
{
    QWriteLocker locker(&sm_readWriteLock);

    if (m_objectTopicHash.keys().contains(object)) {
        auto it = m_objectTopicHash.find(object);
        it.value().insert(topic);
    } else {
        QSet<QString> set = {topic};
        m_objectTopicHash.insert(object, set);

        connect(object, &QObject::destroyed, [=]() {
            remove(object);
        });
    }
      
    //訂閱後將自動發送最後一次主題消息
    if (m_topicLastMsgHash.keys().contains(topic)) {
        QMetaObject::invokeMethod(object, "topicUpdated", Qt::QueuedConnection,
                                  Q_ARG(QString, topic), Q_ARG(QVariant, m_topicLastMsgHash.value(topic)));
    }
}

void Publisher::remove(Subscriber *object, const QString &topic)
{
    QWriteLocker locker(&sm_readWriteLock);

    if (m_objectTopicHash.keys().contains(object)) {
        auto it = m_objectTopicHash.find(object);
        it.value().remove(topic);
    }
}

void Publisher::remove(Subscriber *object)
{
    QWriteLocker locker(&sm_readWriteLock);

    if (m_objectTopicHash.keys().contains(object)) {
        m_objectTopicHash.remove(object);
    }
}

QStringList Publisher::getTopics(Subscriber *object)
{
    QReadLocker locker(&sm_readWriteLock);

    if (m_objectTopicHash.keys().contains(object)) {
        return QStringList::fromSet(m_objectTopicHash.value(object));
    }

    return QStringList();
}

Publisher *Publisher::getInstance()
{
    if (sm_instance.isNull()) {

        sm_readWriteLock.lockForWrite();
        if (sm_instance.isNull()) {
            sm_instance.reset(new Publisher);
        }
        sm_readWriteLock.unlock();
    }

    return sm_instance.data();
}

void Publisher::publish(const QString &topic, const QVariant &msg)
{
    QReadLocker locker(&sm_readWriteLock);

    auto it = m_objectTopicHash.constBegin();
    while (it != m_objectTopicHash.constEnd()) {
        if (it.value().contains(topic)) {
            QMetaObject::invokeMethod(it.key(), "topicUpdated", Qt::QueuedConnection,
                                      Q_ARG(QString, topic), Q_ARG(QVariant, msg));
            m_topicLastMsgHash.insert(topic, msg);
            ++it;
        }
    }
}

發佈者做了一些處理,m_topicLastMsgHash用於緩存最後一次發送的主題消息,使剛訂閱的訂閱者能夠獲取到最新的主題消息。

3.2 訂閱者

訂閱者接口很簡單,主要有訂閱主題、取消主題訂閱、取消所有訂閱,代碼如下:

#ifndef SUBSCRIBER_H
#define SUBSCRIBER_H

#include <QObject>
#include <QStringList>

class Subscriber : public QObject
{
    Q_OBJECT
public:
    explicit Subscriber(QObject *parent = nullptr);

    /*!
     * \brief subscribe 訂閱主題
     * \param topic 主題名稱
     */
    void subscribe(const QString &topic);

    /*!
     * \brief unSubscribe 取消訂閱
     * \param topic 主題名稱
     */
    void unSubscribe(const QString &topic);

    /*!
     * \brief clearSubscribedTopics 取消所有已訂閱主題
     */
    void clearSubscribedTopics();

    /*!
     * \brief topics 獲取已訂閱的主題列表
     * \return 主題列表
     */
    QStringList topics();

signals:
    /*!
     * \brief topicUpdated 主題消息更新信號
     * \param topic 主題名稱
     * \param var 消息內容
     */
    void topicUpdated(const QString &topic, const QVariant &msg);
};

#endif // SUBSCRIBER_H
#include "Subscriber.h"
#include "Publisher.h"

#include <QDebug>

Subscriber::Subscriber(QObject *parent) : QObject(parent)
{
}

void Subscriber::subscribe(const QString &topic)
{
    Publisher *publiser = Publisher::getInstance();
    publiser->add(this, topic);
}

void Subscriber::unSubscribe(const QString &topic)
{
    Publisher *publiser = Publisher::getInstance();
    publiser->remove(this, topic);
}

void Subscriber::clearSubscribedTopics()
{
    Publisher *publiser = Publisher::getInstance();
    publiser->remove(this);
}

QStringList Subscriber::topics()
{
    Publisher *publiser = Publisher::getInstance();
    return publiser->getTopics(this);
}

到此,發佈者通過publish接口發送主題消息,訂閱者通過topicUpdated信號接收主題消息,訂閱者Subscriber可聲明爲自定義類的成員或直接繼承,使用起來非常簡單。

4 總結

對於設計模式,一般來說都是經驗總結,是解決一類問題的“套路”,既然是經驗總結,那麼一定具有實用性。設計模式其實也是源於生活,只是程序員用代碼的方式給實現了,這個叫面向對象(生活)編程,不要覺得這概念很懸乎,其實就是把生活中的邏輯移植到代碼的世界,設計模式讓代碼結構更加清晰,也更便於後期的維護。當然,設計模式也不要濫用,不然就是瞎搞,殺雞焉用牛刀,想想還真是這個道理。

劃重點了,在多線程複雜場景下,通過QVariant封裝自定義結構體類型時使用隊列方式處理信號槽可能會轉換異常,原因可能是信號過快,而槽函數又處理的慢,臨時解決辦法是publish中調用方式QueuedConnection改爲DirectConnection,且將讀鎖改爲寫鎖,這樣效率會有所下降,但是更加穩定。終極辦法還是讓槽函數執行快點吧,最好不要有互斥鎖在裏面,這樣效率和穩定性兼顧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章