1 需求描述
- 基於Qt實現發佈訂閱模式;
- 發佈的消息類型可自定義;
- 能夠支持多線程使用。
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,且將讀鎖改爲寫鎖,這樣效率會有所下降,但是更加穩定。終極辦法還是讓槽函數執行快點吧,最好不要有互斥鎖在裏面,這樣效率和穩定性兼顧。