程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> 設計模式之觀察者(Observer)模式與其C++通用實現(下)

設計模式之觀察者(Observer)模式與其C++通用實現(下)

編輯:關於C++

我們在《設計模式之觀察者(Observer)模式與其C++通用實現(中)》一文中給出了一個以C++語言實現的通用觀察者模式方案骨架。然而,實際的工程項目需求往往要比理想狀態復雜得多,此篇便是與讀者一起探討在現實世界中可能遇到的各種棘手問題及解決方案。

我把目前為止我所遇到的問題羅列如下:

復合主題

多線程

更新方法修改觀察者鏈表

接下來我們一一給予討論。

(一)復合主題

考慮GUI的組件設計,我習慣用Widget類代表之,它需要處理許多用戶交互以及系統事件,其中最常見的用戶交互事件有鼠標及鍵盤事件。倘若架構師決定以事件監聽方式設計整個UI框架,那麼Widget便具有主題的角色,相應的,鼠標及鍵盤事件便是觀察者角色。實際上,一個主題對應多種(不是多個)觀察者的現象很普遍。

我們借助中篇所給的觀察者模式骨架實現這類應用。

借助多繼承機制,很容易辦到:

01.struct MouseListener {
02. void mouseMoved(int x, int y) {}
03.};
04.
05.struct KeyListener {
06. void keyPressed(int keyCode) {}
07.};
08.
09.class Widget : public BasicSubject<MouseListener>, public BasicSubject<KeyListener>{...};

添加事件監聽器的偽代碼大致如下:

01.MouseListener mel;
02.KeyListener kel;
03.Widget w;
04.w.addObserver(mel);
05.w.addObserver(kel);

為了使Widget添加/移除事件監聽器的方法更加友好,我們可以為Widget提供addXXXListener/removeXXXListener 方法,這些方法會把調用轉給基類。有了這些相對較友好的接口後,基類的addObserver/removeObserver接口對用戶已經沒有用了,所以我們可改用protected繼承。綜合起來,代碼看起來大致像這樣:

01.class Widget : protected BasicSubject<MouseListener>,protected BasicSubject<KeyListener>{
02. typedef BasicSubject<MouseListener> MouseSubject;
03. typedef BasicSubject<KeyListener> KeySubject;
04.public:
05. inline void addMouseListener(MouseListener &mel) {
06. MouseSubject::addObserver(mel);
07. }
08.
09. inline void removeMouseListener(MouseListener &mel) {
10. MouseSubject::removeObserver(mel);
11. }
12.
13. inline void addKeyListener(KeyListener &kel) {
14. KeySubject::addObserver(kel);
15. }
16.
17. inline void removeKeyListener(KeyListener &kel) {
18. KeySubject::removeObserver(kel);
19. }
20.
21. void handleMsg(int msg) {
22. if (msg == 0) {
23. MouseSubject::notifyAll(&MouseListener::mouseMoved, 1, 1);
24. } else if (msg == 1) {
25. KeySubject::notifyAll(&KeyListener::keyPressed, 100);
26. }
27. }
28.};

當然,你也可以不使用繼承改而使用組合技術實現,這完全取決於你的愛好。組合版本的實現大致是像這樣的:

01.class Widget {
02.public:
03. inline void addMouseListener(MouseListener &mel) {
04. ms_.addObserver(mel);
05. }
06.
07. inline void removeMouseListener(MouseListener &mel) {
08. ms_.removeObserver(mel);
09. }
10. ...
11.private:
12. BasicSubject<MouseListener> ms_;
13. BasicSubject<KeyListener> ks_;
14.};

(二)多線程

倘若我們的應用程序運行在多線程環境中,那你可要謹慎了。試想線程A正在添加觀察者的同時另一線程B也試圖添加觀察者吧。我們默認使用的容器std::list是線程非安全的,所以我們的BasicSubjcet也會是線程非安全的。要解決此問題,有兩種途徑。一是使用線程安全容器,另一種是我們在BasicSubject的適當地方放置鎖。我只討論後一種情況。

為了讓代碼具有一定的靈活性,我們使用泛型編程中常用的Policies技術。第一步將鎖類定義出來:

01.struct NullLocker{
02. inline void lock() {};
03. inline void unlock() {};
04.};
05.
06.struct CriticalSectionLocker{
07. CriticalSectionLocker() {::InitializeCriticalSection(&cs_);}
08. ~CriticalSectionLocker() {::DeleteCriticalSection(&cs_);}
09. inline void lock() {::EnterCriticalSection(&cs_);}
10. inline void unlock() {::LeaveCriticalSection(&cs_);}
11.private:
12. CRITICAL_SECTION cs_;
13.};

前者為空鎖,用於單線程環境中。後者借助Windows平台中的臨界區實現進程內的鎖語義。你也可以再增加進程間的鎖語義。

接著便是將我們的BasicSubject類修改成如下樣子:

01.template <
02. class ObserverT,
03. class LockerT = NullLocker,
04. class ContainerT = std::list<ObserverT *>
05.>
06.class BasicSubject : protected LockerT {
07.public:
08. inline void addObserver(ObserverT &observer) {
09. lock();
10. observers_.push_back(&observer);
11. unlock();
12. }
13.
14. inline void removeObserver(ObserverT &observer) {
15. lock();
16. ...
17. unlock();
18. }
19.
20.protected:
21. template <typename ReturnT>
22. inline void notifyAll(ReturnT (ObserverT::*pfn)()) {
23. lock();
24. for (ContainerT::iterator it = observers_.begin(), itEnd = observers_.end(); it != itEnd; ++it)
25. ((*it)->*pfn)();
26. unlock();
27. }
28. ...
29.};

默認的鎖類是NullLocker,也就是運行在單線程環境中。需要工作在多線程中時可像這樣使用:

class Widget : protected BasicSubject<MouseListener, CriticalSectionLocker> {...};

(三)更新方法修改觀察者鏈表

想像一下當觀察者在接收到通知而立即修改主題中的觀察者鏈表時會發生什麼?因為主題是通過對已注冊的觀察者鏈表迭代而逐個通知觀察者的相應更新方法的,換句話說,在迭代進行中觀察者就去修改觀察者鏈表。這個問題類似於這樣的代碼設計:

01.std::list<int> is = ...
02.for (std::list<int>::iterator it = is.begin(); it != is.end(); ++it) {
03. is.erase(std::remove(is.begin(), is.end(), 2), is.end());
04.}

危險!迭代器在鏈表被修改後有可能失效。

也許你會疑慮,在使用了(二)中所提的鎖機制之後不就不會有此問題了嗎?實際情況是,鎖對於此類問題沒有任何作用。

解決此類問題的最好辦法是使用不會因容器本身被修改而促使迭代器失效的容器。然而,就目前來說,標准STL庫中的所有容器都不屬此類。因此,我們有必要花點心思處理此類問題。

當鏈表處於被迭代過程中時,對鏈表的修改動作先被記錄下來,等到鏈表迭代完畢後再回過頭執行先前記錄下來的修改動作,如果對鏈表的修改動作不是發生在迭代過程中,就按普通方式處理。依據此思想,代碼可像這樣實現:

01.template <
02. ...
03.>
04.class BasicSubject : protected LockerT
05.{
06.public:
07. BasicSubject() : withinLoop_(false) {}
08.
09. void addObserver(ObserverT &observer) {
10. lock();
11. if (withinLoop_)
12. modifyActionBuf_.insert(std::make_pair(true, &observer));
13. else
14. observers_.push_back(&observer);
15. unlock();
16. }
17.
18. void removeObserver(ObserverT &observer) {
19. lock();
20. if (withinLoop_)
21. modifyActionBuf_.insert(std::make_pair(false, &observer));
22. else
23. observers_.erase(
24. remove(observers_.begin(), observers_.end(), &observer),
25. observers_.end());
26. unlock();
27. }
28.
29.protected:
30. template <typename ReturnT>
31. void notifyAll(ReturnT (ObserverT::*pfn)()) {
32. beginLoop();
33. for (ContainerT::iterator it = observers_.begin(), itEnd = observers_.end(); it != itEnd; ++it)
34. ((*it)->*pfn)();
35. endLoop();
36. }
37. ...
38.private:
39. inline void beginLoop() {
40. lock();
41. withinLoop_ = true;
42. unlock();
43.
44. }
45.
46. void endLoop() {
47. lock();
48. if (!modifyActionBuf_.empty()) {
49. for (std::multimap<bool, ObserverT*>::iterator it = modifyActionBuf_.begin(),
50. itEnd = modifyActionBuf_.end(); it != itEnd; ++it) {
51. if (it->first)
52. observers_.push_back(it->second);
53. else
54. observers_.erase(
55. remove(observers_.begin(), observers_.end(), it->second),
56. observers_.end());
57. }
58. modifyActionBuf_.clear();
59. }
60. withinLoop_ = false;
61. unlock();
62. }
63.
64.protected:
65. ContainerT observers_;
66.
67.private:
68. bool withinLoop_;
69. std::multimap<bool, ObserverT*> modifyActionBuf_;
70.};

我使用了STL中的multimap模板類來儲存修改動作。其中key被設為bool類型,true表明是添加動作,false表明是移除動作。此外,因代碼量的增加,內聯函數已無必要,故移除了所有的inline關鍵字。

後記:編寫通用庫時不能假定用戶所處某一特定環境中,因而須謹慎應對各種可能遇到的問題,這便是為什麼我們常說庫的實現往往比為特定應用而編寫的模塊要復雜得多的緣故,加之C++語言本身的復雜性以及局限性,以致我們設計一個相對完美的觀察者模式是何其困難。

鑒於以上情況,我相信問題遠不止如此,真誠希望讀者提出你所遇到的各種問題,以便我們一起討論學習。

<暫完>

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved