理解观察者、中介者模式
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