理解觀察者、中介者模式


理解觀察者、中介者模式

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 行為符合預期,通過測試:
    1. 設置 輸入框 的文本,檢查 選擇框 的選擇項是否同步
    2. 設置 選擇框 的選擇項,檢查 輸入框 的文本是否同步

假想的 組件庫

  • 我們使用的組件庫包含兩個基本組件:輸入框 和 選擇框
  • 代碼鏈接

利用 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);

相關討論

  • 類似於在線演示的代碼,MyTextBoxMyListBox
    • 構成強耦合 —— 兩者相互依賴,協同調用(一個類的成員函數內,調用另一個類的成員函數)
    • 不易於復用 —— 硬編碼界面邏輯,難以重復利用
  • 當界面變得復雜時,不易於維護,例如
    • 新增組件:需要新組件和原有的兩個組件分別耦合,界面邏輯變得復雜而且零散
    • 修改行為:如果需要修改個組件的行為,可能涉及到多處代碼的改動(沒遇到過,目前至少假設。。。)

改進 —— 基於 觀察者模式 的實現

  • 應用觀察者模式,將用戶行為委托到觀察者的回調函數上,消除組件之間雙向依賴
  • 代碼鏈接

在原有組件庫 的基礎上,我們封裝了一個可觀察的組件庫 (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::ObserverMyListBox::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

BOTManJL

Delivered under MIT License © 2017, BOT Man


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM