為消除重複樣板碼,將觀察者模式抽象為通用庫。本文從介面基底類別出發,逐步引入樣板技術、Traits 輔助類別與標籤分派,實現支援任意觀察者介面、任意參數個數與型別、完美保留參數傳遞語意(含引用語意)的 BasicSubject。
早期版本需定義 IObserver、Event、BasicSubject 三個基底類別,強制觀察者實作單一 update(Event&)。改用樣板後,BasicSubject 直接接受觀察者型別作為樣板參數,無需共同基底:
template <class ObserverT, class ContainerT = std::list<ObserverT*>>
class BasicSubject {
public:
inline void addObserver(ObserverT& o) { observers_.push_back(&o); }
inline void removeObserver(ObserverT& o) { /* erase */ }
protected:
ContainerT observers_;
};
notifyAll 需將參數轉發給觀察者的多種更新方法(0~5 參數)。直接使用樣板參數推導會遇到:
int vs int&)導致推導模稜兩可。10)無法繫結至非 const 引用參數。解法:定義 arg_type_traits 依目的函數參數型別決定轉發函數參數型別:
template <typename T> struct arg_type_traits { typedef const T& result; };
template <typename T> struct arg_type_traits<T&> { typedef T& result; };
template <typename T> struct arg_type_traits<const T&> { typedef const T& result; };
配合多重載 notifyAll(0~5 參數),實現完美轉發:
template <typename ReturnT, typename Arg1T>
inline void notifyAll(ReturnT (ObserverT::*pfn)(Arg1T),
typename arg_type_traits<Arg1T>::result arg1) {
for (auto it = observers_.begin(); it != observers_.end(); ++it)
((*it)->*pfn)(arg1);
}
// 同理擴充至 2~5 參數版本
struct MyObserver {
void copy(int src, int& dest) { dest = src; }
};
struct MySubject : public BasicSubject<MyObserver> {
void trigger() {
int i = 0;
notifyAll(&MyObserver::copy, 100, i); // i 成為 100
}
};
客戶端程式碼簡潔直觀,無需記憶輔助類別(如 ByRef),編譯器自動根據觀察者方法簽名選擇正確轉發語意。