理解觀察者、中介者模式
2017/10/6
Any fool can write code that a computer can understand.
Good programmers write code that humans can understand.
—— Martin Fowler
為什么要寫這篇文章
- 觀察者 (observer) 模式和 中介者 (mediator) 模式
- 調用流程非常相似
- 網上相關資料、代碼對兩者區別的解釋不夠清楚
- 兩個設計模式在 圖形界面 (GUI) 編程中,被廣泛使用
- 學習的過程是:不知道 -> 知道 -> 能向別人解釋清楚
基本概念
首先需要知道 回調函數的基本概念 。。
觀察者 (observer) 模式
- 通過 訂閱-發布 (subscribe-publish) 模型,消除組件之間雙向依賴
- 消息的 發布者 (subject) 不需要知道 觀察者 (observer) 的存在
- 兩者只需要約定消息的格式(如何訂閱、如何發布),就可以通信
- 筆記鏈接
中介者 (mediator) 模式
- 通過設置 消息中心 (message center),避免組件之間直接依賴
- 所有的 協同者 (colleague) 只能通過 中介者 (mediator) 進行通信,
而相互之間不知道彼此的存在 - 當各個組件的消息出現循環時,消息中心可以消除組件之間的依賴混亂
- 筆記鏈接
兩者的聯系
- 中介者模式 一般通過 觀察者模式 實現
- 協同者 作為 發布者,中介者 作為 觀察者
- 協同者 發布消息 -> 中介者 收到並處理消息 -> 中介者 直接發送消息給 協同者
- 協同者 不依賴於 中介者
- 當組件之間依賴關系簡單時,可以直接使用 觀察者模式
- 當組件之間依賴關系復雜是,需要借助 中介者模式 梳理關系
需求(HTML)
我們需要實現一個簡單的 輸入框-選擇框 同步的功能:
- 從 輸入框 輸入:將輸入的 文本 同步到 選擇框(如果存在對應的 選擇項)
- 從 選擇框 選擇:將選擇的 選擇項 同步到 輸入框
在線演示 - Live Demo
Powered by BOT-Mark, 2017 😘
博客園已阻止腳本,可以到原文查看效果
[no-number] 在線演示 HTML 代碼
<input id="textbox"
oninput="document.getElementById('listbox').value =
document.getElementById('textbox').value" />
<select id="listbox"
onchange="document.getElementById('textbox').value =
document.getElementById('listbox').value">
</select>
實現(C++)
- 代碼使用 C++ 編寫,可以通過 腳本 運行
- 代碼基於一個
假想的😂 組件庫 (Widget Library) 進行開發 - 代碼流程包含兩個部分:
- Client 初始化流程:初始化組件,並設置依賴
- Invoker 模擬用戶行為:模擬用戶對組件的操作,並查看效果(自動化測試)
- 如果所有的 Invoker 行為符合預期,通過測試:
- 設置 輸入框 的文本,檢查 選擇框 的選擇項是否同步
- 設置 選擇框 的選擇項,檢查 輸入框 的文本是否同步
假想的 組件庫
- 我們使用的組件庫包含兩個基本組件:輸入框 和 選擇框
- 代碼鏈接
利用
using MyItem = std::string;
定義默認的文本類型為一般的字符類型,並填入模板參數Item
。
輸入框 TextBox
- 設置輸入框文本
SetText
- 獲取輸入框文本
GetText
- 接收用戶行為
OnInput
- 當用戶在輸入框輸入文本時,組件庫調用這個虛函數
- 組件庫的使用者重載這個函數,定義組件行為
template<typename Item>
class TextBox {
Item _item;
public:
TextBox (const Item &item);
void SetText (const Item &item);
const Item &GetText () const;
// Interface for Invoker
virtual void OnInput () = 0;
};
選擇框 ListBox
- 設置選擇項
SetSelection
- 獲取選擇項
GetSelection
- 接收用戶行為
OnChange
- 當用戶在選擇框選擇項目時,組件庫調用這個虛函數
- 組件庫的使用者重載這個函數,定義組件行為
template<typename Item>
class ListBox {
std::vector<Item> _items;
unsigned _index;
public:
ListBox (const std::vector<Item> &items,
unsigned index = 0);
void SetSelection (const Item &item);
const Item &GetSelection () const;
// Interface for Invoker
virtual void OnChange () = 0;
};
最簡單的實現
- 通過自定義組件的方法,重載原始的用戶行為,從而實現界面邏輯
- 代碼鏈接
自定義輸入框
- 繼承於
TextBox<MyItem>
,額外保存一個 選擇框 的引用 - 當用戶輸入
OnInput
時,調用 選擇框 的設置函數SetSelection
,設置為 輸入框 的內容GetText
class MyTextBox : public TextBox<MyItem> {
std::weak_ptr<MyListBox> _listbox;
public:
MyTextBox (const MyItem &item);
void SetListBox (std::weak_ptr<MyListBox> &&p) { _listbox = p; }
void OnInput () override {
if (auto p = _listbox.lock ())
p->SetSelection (this->GetText ());
}
};
自定義選擇框
- 繼承於
ListBox<MyItem>
,額外保存一個 輸入框 的引用 - 當用戶選擇
OnChange
時,調用 輸入框 的設置函數SetText
,設置為 選擇框 的選項GetSelection
class MyListBox : public ListBox<MyItem> {
std::weak_ptr<MyTextBox> _textbox;
public:
MyListBox (const std::vector<MyItem> &items,
unsigned index = 0);
void SetTextBox (std::weak_ptr<MyTextBox> &&p) { _textbox = p; }
void OnChange () override {
if (auto p = _textbox.lock ())
p->SetText (this->GetSelection ());
}
};
初始化流程
- 分別構造一個 輸入框
textbox
和 選擇框listbox
- 相互設置為依賴對象
auto textbox = std::make_shared<MyTextBox> (items[0]);
auto listbox = std::make_shared<MyListBox> (items, 0);
textbox->SetListBox (listbox);
listbox->SetTextBox (textbox);
相關討論
- 類似於在線演示的代碼,
MyTextBox
和MyListBox
- 構成強耦合 —— 兩者相互依賴,協同調用(一個類的成員函數內,調用另一個類的成員函數)
- 不易於復用 —— 硬編碼界面邏輯,難以重復利用
- 當界面變得復雜時,不易於維護,例如
- 新增組件:需要新組件和原有的兩個組件分別耦合,界面邏輯變得復雜而且零散
- 修改行為:如果需要修改個組件的行為,可能涉及到多處代碼的改動(沒遇到過,目前至少假設。。。)
改進 —— 基於 觀察者模式 的實現
- 應用觀察者模式,將用戶行為委托到觀察者的回調函數上,消除組件之間雙向依賴
- 代碼鏈接
在原有組件庫 的基礎上,我們封裝了一個可觀察的組件庫 (Observable Widget Library),用於實現觀察者模式。
可觀察的組件庫
- 繼承於
TextBox<Item>
/ListBox<Item>
,額外保存一個Observer
的引用 - 將原始的用戶行為,重定向到 觀察者 上:
- 當用戶輸入
OnInput
時,調用 觀察者 的回調函數TextUpdated
,設置為 輸入框 的內容GetText
- 當用戶選擇
OnChange
時,調用 觀察者 的回調函數SelectionChanged
,設置為 選擇框 的選項GetSelection
- 當用戶輸入
- 代碼鏈接
template<typename Item>
class ObservableTextBox : public TextBox<Item> {
public:
ObservableTextBox (const Item &item);
class Observer {
public:
// Interface for Observer
virtual void TextUpdated (const Item &) = 0;
};
void SetObserver (std::weak_ptr<Observer> &&p) { _observer = p; }
// Interface for Invoker
void OnInput () override {
if (auto p = _observer.lock ())
p->TextUpdated (this->GetText ());
}
private:
std::weak_ptr<Observer> _observer;
};
template<typename Item>
class ObservableListBox : public ListBox<Item> {
public:
ObservableListBox (const std::vector<Item> &items,
unsigned index = 0);
class Observer {
public:
// Interface for Observer
virtual void SelectionChanged (const Item &) = 0;
};
void SetObserver (std::weak_ptr<Observer> &&p) { _observer = p; }
// Interface for Invoker
void OnChange () override {
if (auto p = _observer.lock ())
p->SelectionChanged (this->GetSelection ());
}
private:
std::weak_ptr<Observer> _observer;
};
相互觀察的輸入框/選擇框
- 定義觀察關系:
MyTextBox
繼承於ObservableTextBox<MyItem>
和ObservableListBox<MyItem>::Observer
,即 我們的輸入框 作為 選擇框的觀察者MyListBox
繼承於ObservableListBox<MyItem>
和ObservableTextBox<MyItem>::Observer
,即 我們的選擇框 作為 輸入框的觀察者
- 定義觀察行為:
- 輸入框 觀察到 選擇框變化
SelectionChanged
時,更新文本SetText
- 選擇框 觀察到 輸入框輸入
TextUpdated
時,更新選項SetSelection
- 輸入框 觀察到 選擇框變化
class MyTextBox :
public ObservableTextBox<MyItem>,
public ObservableListBox<MyItem>::Observer
{
public:
MyTextBox (const MyItem &item);
void SelectionChanged (const MyItem &item) override {
this->SetText (item);
}
};
class MyListBox :
public ObservableListBox<MyItem>,
public ObservableTextBox<MyItem>::Observer
{
public:
MyListBox (const std::vector<MyItem> &items,
unsigned index = 0);
void TextUpdated (const MyItem &item) override {
this->SetSelection (item);
}
};
初始化流程
- 分別構造一個 輸入框
textbox
和 選擇框listbox
- 相互設置為觀察的對象(發布者)
auto textbox = std::make_shared<MyTextBox> (items[0]);
auto listbox = std::make_shared<MyListBox> (items, 0);
textbox->SetObserver (listbox);
listbox->SetObserver (textbox);
相關討論
- 使用觀察者模式
- 接收到用戶行為的組件(發布者) 將 組件的用戶行為 作為消息,發布到 訂閱了這個消息的組件(觀察者)上
- 從而實現了 界面邏輯的處理 從 接收到用戶行為的組件 轉移到 對這個用戶行為感興趣的組件
- 而不是由 接收到用戶行為的組件 直接處理消息,從而解除了雙向的相互依賴(因為接收到消息的一方需要依賴於處理消息的一方)
- 在這個例子中,輸入框內容 發生變化時,它本身不知道如何處理(因為它不是 選擇框,不能更新選擇項),而是通知對這個變化改興趣的 選擇框 去處理當前的用戶行為(更新選擇項)
- 但是,當界面變得復雜時,組件對用戶行為的處理邏輯仍然非常零散
再改進 —— 基於 中介者模式 的實現
- 應用中介者模式,將用戶行為委托到中介者上,避免組件之間直接依賴
- 代碼鏈接
中介者模式基於觀察者模式實現,所以這里仍使用之前定義的可觀察的組件庫。
定義消息中心(中介者)
- 定義觀察關系:
- 輸入框
ObservableTextBox<MyItem>
和 選擇框ObservableListBox<MyItem>
不再相互觀察,而是作為獨立的組件存在 - 中介者
Mediator
繼承於MyTextBox::Observer
和MyListBox::Observer
,即 作為 輸入框、選擇框的觀察者
- 輸入框
- 定義觀察行為:
- 中介者 觀察到 輸入框輸入
TextUpdated
時,更新 選擇框選項SetSelection
- 中介者 觀察到 選擇框變化
SelectionChanged
時,更新 輸入框文本SetText
- 中介者 觀察到 輸入框輸入
using MyTextBox = ObservableTextBox<MyItem>;
using MyListBox = ObservableListBox<MyItem>;
class Mediator :
public MyTextBox::Observer,
public MyListBox::Observer
{
std::shared_ptr<MyTextBox> _textbox;
std::shared_ptr<MyListBox> _listbox;
public:
Mediator (std::shared_ptr<MyTextBox> &textbox,
std::shared_ptr<MyListBox> &listbox);
void TextUpdated (const MyItem &item) override {
_listbox->SetSelection (item);
}
void SelectionChanged (const MyItem &item) override {
_textbox->SetText (item);
}
};
初始化流程
- 分別構造 輸入框
textbox
、選擇框listbox
和 中介者mediator
- 將 中介者 作為另外兩個對象(發布者)的 觀察者
auto textbox = std::make_shared<MyTextBox> (items[0]);
auto listbox = std::make_shared<MyListBox> (items, 0);
auto mediator = std::make_shared<Mediator> (textbox, listbox);
textbox->SetObserver (mediator);
listbox->SetObserver (mediator);
相關討論
- 使用中介者模式
- 化簡了觀察關系:所有組件只能和 中介者 通信,組件之間沒有消息傳遞
- 化簡了觀察行為:原本零散的消息傳遞關系,集中於 中介者 內部實現
- 相對於零散的觀察者
- 把 組件之間消息的耦合 轉化為 中介者的內聚
- 從而實現了 高內聚、低耦合
另一種基於 中介者模式 的實現
- 中介者 作為消息中心,保存了對所有組件的引用(依賴於所有的組件),從而對所有的組件進行協調
- 所以,我們可以使用另一種 觀察者模式 的實現 —— 基於 拉取模型 (pull model)
- 代碼鏈接
基於拉取的可觀察組件庫
- 和 推送模型 (push model) 不同,
Observer
的接口沒有參數- 發布者 僅僅告知有消息到達,而不告知消息的內容
- 觀察者 不能直接從接收到的消息獲取內容(進一步通過拉取的方式獲取消息)
- 這就類似於
- 推送模型:手機(發布者)收到消息時,消息提示音響起,用戶(觀察者)能在鎖屏界面上看到消息的內容
- 拉取模型:朋友圈(發布者)更新時,只會顯示一個小紅點,用戶(觀察者)需要點進去才能看到更新
- 代碼鏈接
class ObservableTextBox::Observer {
public:
// Interface for Observer
virtual void TextUpdated () = 0; // omitting param 'const Item &'
};
void ObservableTextBox::OnInput () {
if (auto p = _observer.lock ())
p->TextUpdated (); // omitting argument 'this->GetText ()'
}
重新定義消息中心(中介者)
- 當 觀察者 收到消息時,中介者 通過被觀察對象(發布者)獲取需要的內容,而不是直接從推送的消息中獲取
- 由於 中介者 保存了對所有組件的引用(依賴於所有的組件),可以方便的直接獲取需要的內容
- 而對於 沒有中介者 的設計,保存交叉引用會導致代碼變得混亂,進而退化為最開始討論的形式
void Mediator::TextUpdated () {
_listbox->SetSelection (_textbox->GetText ());
}
void Mediator::SelectionChanged () {
_textbox->SetText (_listbox->GetSelection ());
}
[no-number] 寫在最后
本文僅是我對設計模式的一些理解。如果有什么問題,望不吝賜教。😄
感謝 @flythief 提出的修改意見~
Related: Design Patterns Notes
原文:https://bot-man-jl.github.io/articles/?post=2017/Observer-Mediator-Explained
公眾號:BOTManJL
Delivered under MIT License © 2017, BOT Man