承接前兩篇,本文探討實務專案中遇到的三大棘手問題:複合主題(一個主題對應多種觀察者介面)、多執行緒環境下的執行緒安全、以及觀察者在更新通知期間修改觀察者清單導致迭代器失效的問題。
GUI 元件常需同時支援滑鼠、鍵盤等多種事件監聽器。利用多重繼承或組合技術,讓 Widget 同時扮演多個主題角色:
// 多重繼承版
class Widget : protected BasicSubject<MouseListener>, protected BasicSubject<KeyListener> {
typedef BasicSubject<MouseListener> MouseSubject;
typedef BasicSubject<KeyListener> KeySubject;
public:
inline void addMouseListener(MouseListener& mel) { MouseSubject::addObserver(mel); }
inline void removeMouseListener(MouseListener& mel) { MouseSubject::removeObserver(mel); }
inline void addKeyListener(KeyListener& kel) { KeySubject::addObserver(kel); }
inline void removeKeyListener(KeyListener& kel) { KeySubject::removeObserver(kel); }
void handleMsg(int msg) {
if (msg == 0) MouseSubject::notifyAll(&MouseListener::mouseMoved, 1, 1);
else if (msg == 1) KeySubject::notifyAll(&KeyListener::keyPressed, 100);
}
};
// 組合版
class Widget {
BasicSubject<MouseListener> ms_;
BasicSubject<KeyListener> ks_;
public:
inline void addMouseListener(MouseListener& mel) { ms_.addObserver(mel); }
// ...
};
使用 protected 繼承隱藏基底介面,對外提供友善的 addXxxListener 方法。
引入 Policy 設計模式,預設無鎖(單執行緒),可切換至 Critical Section(Windows 程序內鎖)或其他鎖實作:
struct NullLocker { inline void lock() {} inline void unlock() {} };
struct CriticalSectionLocker {
CriticalSectionLocker() { InitializeCriticalSection(&cs_); }
~CriticalSectionLocker() { DeleteCriticalSection(&cs_); }
inline void lock() { EnterCriticalSection(&cs_); }
inline void unlock() { LeaveCriticalSection(&cs_); }
private: CRITICAL_SECTION cs_;
};
template <class ObserverT, class LockerT = NullLocker, class ContainerT = std::list<ObserverT*>>
class BasicSubject : protected LockerT {
public:
void addObserver(ObserverT& o) { lock(); observers_.push_back(&o); unlock(); }
void removeObserver(ObserverT& o) { lock(); /* erase */ unlock(); }
template <typename ReturnT>
void notifyAll(ReturnT (ObserverT::*pfn)()) {
lock();
for (auto it = observers_.begin(); it != observers_.end(); ++it) ((*it)->*pfn)();
unlock();
}
protected: ContainerT observers_;
};
// 使用:class Widget : protected BasicSubject<MouseListener, CriticalSectionLocker> { ... };
若觀察者在 update 中增刪觀察者,會導致正在迭代的容器結構改變,迭代器失效。鎖無法解決此問題(同一執行緒內)。解法:迭代期間將修改動作記錄至緩衝區,迭代結束後重放:
class BasicSubject {
bool withinLoop_ = false;
std::multimap<bool, ObserverT*> modifyActionBuf_; // true=add, false=remove
public:
void addObserver(ObserverT& o) {
lock();
if (withinLoop_) modifyActionBuf_.insert({true, &o});
else observers_.push_back(&o);
unlock();
}
void removeObserver(ObserverT& o) {
lock();
if (withinLoop_) modifyActionBuf_.insert({false, &o});
else observers_.erase(remove(observers_.begin(), observers_.end(), &o), observers_.end());
unlock();
}
protected:
template <typename ReturnT>
void notifyAll(ReturnT (ObserverT::*pfn)()) {
beginLoop();
for (auto it = observers_.begin(); it != observers_.end(); ++it) ((*it)->*pfn)();
endLoop();
}
private:
void beginLoop() { lock(); withinLoop_ = true; unlock(); }
void endLoop() {
lock();
if (!modifyActionBuf_.empty()) {
for (auto& act : modifyActionBuf_) {
if (act.first) observers_.push_back(act.second);
else observers_.erase(remove(observers_.begin(), observers_.end(), act.second), observers_.end());
}
modifyActionBuf_.clear();
}
withinLoop_ = false;
unlock();
}
};
使用 multimap 記錄動作類型與觀察者指標,迭代結束後批次套用,確保迭代器有效性。