设计模式之观察者(Observer)模式与其C++通用实现分上、中、下三篇。上篇详细讲解何为观察者模式以及其特点,并给出一个应用实例与其实现。中篇研究如何运用C++各种技术实现一个通用/万能的观察者模式。下篇讨论中篇所给出的实现可能遇到的问题及解决方案。
我们在《设计模式之观察者(Observer)模式与其C++通用实现(中)》一文中给出了一个以C++语言实现的通用观察者模式方案骨架。然而,实际的工程项目需求往往要比理想状态复杂得多,此篇便是与读者一起探讨在现实世界中可能遇到的各种棘手问题及解决方案。
我把目前为止我所遇到的问题罗列如下:
接下来我们一一给予讨论。
(一)复合主题
考虑GUI的组件设计,我习惯用Widget类代表之,它需要处理许多用户交互以及系统事件,其中最常见的用户交互事件有鼠标及键盘事件。倘若架构师决定以事件监听方式设计整个UI框架,那么Widget便具有主题的角色,相应的,鼠标及键盘事件便是观察者角色。实际上,一个主题对应多种(不是多个)观察者的现象很普遍。
我们借助中篇所给的观察者模式骨架实现这类应用。
借助多继承机制,很容易办到:
- struct MouseListener {
- void mouseMoved(int x, int y) {}
- };
- struct KeyListener {
- void keyPressed(int keyCode) {}
- };
- class Widget : public BasicSubject, public BasicSubject{...};
添加事件监听器的伪代码大致如下:
- MouseListener mel;
- KeyListener kel;
- Widget w;
- w.addObserver(mel);
- w.addObserver(kel);
为了使Widget添加/移除事件监听器的方法更加友好,我们可以为Widget提供addXXXListener/removeXXXListener 方法,这些方法会把调用转给基类。有了这些相对较友好的接口后,基类的addObserver/removeObserver接口对用户已经没有用了,所以我们可改用protected继承。综合起来,代码看起来大致像这样:
- class Widget : protected BasicSubject,protected BasicSubject{
- typedef BasicSubject MouseSubject;
- typedef BasicSubject 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 {
- public:
- inline void addMouseListener(MouseListener &mel) {
- ms_.addObserver(mel);
- }
- inline void removeMouseListener(MouseListener &mel) {
- ms_.removeObserver(mel);
- }
- ...
- private:
- BasicSubject ms_;
- BasicSubject ks_;
- };
(二)多线程
倘若我们的应用程序运行在多线程环境中,那你可要谨慎了。试想线程A正在添加观察者的同时另一线程B也试图添加观察者吧。我们默认使用的容器std::list是线程非安全的,所以我们的BasicSubjcet也会是线程非安全的。要解决此问题,有两种途径。一是使用线程安全容器,另一种是我们在BasicSubject的适当地方放置锁。我只讨论后一种情况。
为了让代码具有一定的灵活性,我们使用泛型编程中常用的Policies技术。第一步将锁类定义出来:
- 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_;
- };
前者为空锁,用于单线程环境中。后者借助Windows平台中的临界区实现进程内的锁语义。你也可以再增加进程间的锁语义。
接着便是将我们的BasicSubject类修改成如下样子:
- template <
- class ObserverT,
- class LockerT = NullLocker,
- class ContainerT = std::list
- >
- class BasicSubject : protected LockerT {
- public:
- inline void addObserver(ObserverT &observer) {
- lock();
- observers_.push_back(&observer);
- unlock();
- }
- inline void removeObserver(ObserverT &observer) {
- lock();
- ...
- unlock();
- }
- protected:
- template <typename ReturnT>
- inline void notifyAll(ReturnT (ObserverT::*pfn)()) {
- lock();
- for (ContainerT::iterator it = observers_.begin(), itEnd = observers_.end(); it != itEnd; ++it)
- ((*it)->*pfn)();
- unlock();
- }
- ...
- };
默认的锁类是NullLocker,也就是运行在单线程环境中。需要工作在多线程中时可像这样使用:
- class Widget : protected BasicSubject {...};
(三)更新方法修改观察者链表 想像一下当观察者在接收到通知而立即修改主题中的观察者链表时会发生什么?因为主题是通过对已注册的观察者链表迭代而逐个通知观察者的相应更新方法的,换句话说,在迭代进行中观察者就去修改观察者链表。这个问题类似于这样的代码设计:
- std::list<int> is = ...
- for (std::list<int>::iterator it = is.begin(); it != is.end(); ++it) {
- is.erase(std::remove(is.begin(), is.end(), 2), is.end());
- }
危险!迭代器在链表被修改后有可能失效。
也许你会疑虑,在使用了(二)中所提的锁机制之后不就不会有此问题了吗?实际情况是,锁对于此类问题没有任何作用。
解决此类问题的最好办法是使用不会因容器本身被修改而促使迭代器失效的容器。然而,就目前来说,标准STL库中的所有容器都不属此类。因此,我们有必要花点心思处理此类问题。
当链表处于被迭代过程中时,对链表的修改动作先被记录下来,等到链表迭代完毕后再回过头执行先前记录下来的修改动作,如果对链表的修改动作不是发生在迭代过程中,就按普通方式处理。依据此思想,代码可像这样实现:
- template <
- ...
- >
- class BasicSubject : protected LockerT
- {
- public:
- BasicSubject() : withinLoop_(false) {}
- void addObserver(ObserverT &observer) {
- lock();
- if (withinLoop_)
- modifyActionBuf_.insert(std::make_pair(true, &observer));
- else
- observers_.push_back(&observer);
- unlock();
- }
- void removeObserver(ObserverT &observer) {
- lock();
- if (withinLoop_)
- modifyActionBuf_.insert(std::make_pair(false, &observer));
- else
- observers_.erase(
- remove(observers_.begin(), observers_.end(), &observer),
- observers_.end());
- unlock();
- }
- protected:
- template <typename ReturnT>
- void notifyAll(ReturnT (ObserverT::*pfn)()) {
- lock();
- beginLoop();
- for (ContainerT::iterator it = observers_.begin(), itEnd = observers_.end(); it != itEnd; ++it)
- ((*it)->*pfn)();
- endLoop();
- unlock();
- }
- ...
- private:
- inline void beginLoop() {
- withinLoop_ = true;
- }
- void endLoop() {
- if (!modifyActionBuf_.empty()) {
- for (std::multimap<bool, ObserverT*>::iterator it = modifyActionBuf_.begin(),
- itEnd = modifyActionBuf_.end(); it != itEnd; ++it) {
- if (it->first)
- observers_.push_back(it->second);
- else
- observers_.erase(
- remove(observers_.begin(), observers_.end(), it->second),
- observers_.end());
- }
- modifyActionBuf_.clear();
- }
- withinLoop_ = false;
- }
- protected:
- ContainerT observers_;
- private:
- bool withinLoop_;
- std::multimap<bool, ObserverT*> modifyActionBuf_;
- };
我使用了STL中的multimap模板类来储存修改动作。其中key被设为bool类型,true表明是添加动作,false表明是移除动作。此外,因代码量的增加,内联函数已无必要,故移除了所有的inline关键字。
后记:编写通用库时不能假定用户所处某一特定环境中,因而须谨慎应对各种可能遇到的问题,这便是为什么我们常说库的实现往往比为特定应用而编写的模块要复杂得多的缘故,加之C++语言本身的复杂性以及局限性,以致我们设计一个相对完美的观察者模式是何其困难。
鉴于以上情况,我相信问题远不止如此,真诚希望读者提出你所遇到的各种问题,以便我们一起讨论学习。
阅读(1276) | 评论(0) | 转发(0) |