「訊號與槽」是 Qt 框架的核心機制,但其本質是觀察者模式的一種高度泛化實作。本文從系統設計角度,追溯從硬編碼耦合、虛擬函數、C 風格回調、C++ 回調物件,到訊號槽機制的演進歷程,揭示其設計哲學與優勢。
此機制與程式語言無關,Qt、Boost.Signals2、libsigc++ 等皆有實作。
class Button { void clicked() { currentPage()->reload(); } };
缺點:Button 直接依賴 Page,無法重用。
class Button { virtual void clicked() = 0; };
class PageReloadButton : public Button {
void clicked() override { currentPage()->reload(); }
};
缺點:每種行為需衍生新類別,類別爆炸,且行為綁定在編譯期。
struct Button { void (*actionFunc)(); void* actionFuncData; };
void buttonClicked(Button* b) { if (b->actionFunc) b->actionFunc(b->actionFuncData); }
缺點:非型別安全,需強制轉型,編譯器無法檢查參數匹配。
class AbstractAction { public: virtual void execute() = 0; };
class Button { AbstractAction* action_; void clicked() { if (action_) action_->execute(); } };
class PageReloadAction : public AbstractAction { void execute() override { currentPage()->reload(); } };
優點:型別安全、可重用、行與物件分離。但每個動作仍需定義新類別。
引入 MultiAction 組合多個動作,並透過 operator() 讓物件像函數一樣被呼叫,支援 STL 演算法(for_each、bind)。
傳統回調中,發送者決定呼叫誰(Button 持有 action_)。訊號槽將連接決策移至消費者/外部組裝者:
Button 只發射 clicked 訊號,完全不知誰在監聽。Page 只提供 reload 槽,不知誰會觸發。main()、UI 設計器)負責 connect(button, clicked, page, reload)。這實現了真正的鬆耦合:雙方零依賴,可獨立開發、測試、重用。
若連接關係以字串鍵值對儲存(如 XML、JSON),並透過註冊表/工廠動態建立物件與連接,則可實現:
代價是放棄編譯期型別檢查,屬於「重量級」實作。輕量級實作(如 Boost.Signals2)保留編譯期型別安全,僅支援程式碼層面連接。
訊號槽機制是觀察者模式在「型別安全、多對多、鬆耦合、參數傳遞」等工程需求下的集大成者。理解其演進脈絡,有助於在架構設計中做出正確的技術選型:單純解耦用輕量級訊號槽;需動態組裝/腳本化則投資重量級實作。