实现“观察者”模式时遇到的问题

33

我在使用C++和STL实现观察者模式时遇到了一个有趣的问题。考虑这个经典的例子:

class Observer {
public:
   virtual void notify() = 0;
};

class Subject {
public:
   void addObserver( Observer* );
   void remObserver( Observer* );
private:
   void notifyAll();
};

void Subject::notifyAll() {
   for (all registered observers) { observer->notify(); }
}

这个例子在每本设计模式的书中都能找到。不幸的是,现实中的系统更加复杂,所以第一个问题出现了: 一些观察者决定在被通知时向主题添加其他观察者。这使得“for”循环和我使用的所有迭代器无效。解决方法相当简单 - 我会对注册的观察者列表进行快照并遍历该快照。添加新的观察者不会使快照无效,所以一切看起来都没问题。但又出现了另一个问题:观察者决定在被通知时自行销毁。更糟糕的是,一个单一的观察者可以决定销毁所有其他观察者(它们是通过脚本控制的),这会使队列和快照无效。于是我发现自己正在迭代已被释放指针。

我的问题是,当观察者相互删除时,我应该如何处理?有没有现成的模式可以使用?我一直认为“观察者”是世界上最容易的设计模式,但现在似乎实现它并不那么容易...

感谢大家的关注,请允许我总结一下决策:

[1] “不要这样做” 很抱歉,但这是必须的。观察者是通过脚本控制并进行垃圾回收的。我无法控制垃圾回收以防止它们被释放;

[2]“使用boost::signal” 最有希望的决定,但我不能在项目中引入boost,这样的决定必须由项目负责人作出(我们正在Playstation下编写);

[3]“使用shared_ptr” 这将防止观察者被释放。一些子系统可能依赖于内存池清理,所以我认为我不能使用shared_ptr。

[4]“推迟观察者的销毁” 在通知时排队观察者进行删除,然后使用第二个循环来删除它们。不幸的是,我无法防止它们被释放,因此我使用一个技巧将观察者包装成某种“适配器”,实际上保留“适配器”的列表。在析构函数中,观察者将从其适配器中取消分配,然后我会使用第二个循环来销毁空适配器。

p.s. 我能编辑我的问题总结所有的帖子吗?我在StackOverflow上是新手...


1
好问题!我之前没有考虑过使用观察者模式,其中观察者可以创建和销毁其他观察者。 - A. Levy
1
我喜欢在问题中总结回答,但不要删除原始问题,否则后来的读者可能会错过原始问题的细微差别(并不是说你这样做了;我认为你的总结和注明已经总结的方式非常好)。 - Jay
你有没有测试过这些工具,看看哪个是你最喜欢的或者感觉最好用的? - prolink007
14个回答

16

非常有趣的问题。

尝试这样做:

  1. 更改remObserver为null以清除该条目,而不仅仅是删除它(并使列表迭代器无效)。
  2. 将notifyAll循环更改为:

    对于所有已注册的观察者:{如果(观察者)observer - > notify(); }

  3. 在notifyAll结束时添加另一个循环,以删除观察者列表中的所有空条目


这对我来说听起来最合适,因为我不能使用 signals 或 shared_ptr。虽然使用两个循环而不是一个可能会导致性能损失,但我认为这是最简单的方法。谢谢! - SadSido
2
如果你真的很担心性能问题,你可以添加一个“脏”标志,以便最后一个循环只有在需要删除东西时才会被激活。然而,除非这个循环存在已验证和测量的性能问题,否则我不会费心去做这件事。过早优化等等。 - T.E.D.

7

就我个人而言,我使用boost::signals来实现我的观察者模式。我需要检查一下,但我相信它可以处理上述场景(编辑:找到了,请参见"何时会发生断开连接")。它简化了你的实现过程,并且不需要创建自定义类:

class Subject {
public:
   boost::signals::connection addObserver( const boost::function<void ()>& func )
   { return sig.connect(func); }

private:
   boost::signal<void ()> sig;

   void notifyAll() { sig(); }
};

void some_func() { /* impl */ }

int main() {
   Subject foo;
   boost::signals::connection c = foo.addObserver(boost::bind(&some_func));

   c.disconnect(); // remove yourself.
}

+1 对于 boost::signal 这是我实现观察者模式的方式,它使生活变得更简单。 - Gustavo Muenz

5
这是T.E.D.提出的一种变体思路。
只要remObserver能够将条目设为null而非立即删除,那么你就可以实现notifyAll,如下所示:
void Subject::notifyAll()
{
    list<Observer*>::iterator i = m_Observers.begin();
    while(i != m_Observers.end())
    {
        Observer* observer = *i;
        if(observer)
        {
            observer->notify();
            ++i;
        }
        else
        {
            i = m_Observers.erase(i);
        }
    }
}

这避免了需要进行第二次清理循环的情况。但是,这意味着如果某个特定的notify()调用触发了自身或位于列表中较早位置的观察者的删除,则列表元素的实际删除将被推迟到下一个notifyAll()。但只要任何操作列表的函数在适当时检查空条目,那么这就不应该成为问题。


5
一个人去看医生说,“医生,我抬手的时候很痛!” 医生说,“别那样做。”
最简单的解决办法是与团队合作,并告诉他们不要那样做。如果观察者“非常需要”杀死自己或所有观察者,则将操作安排在通知结束后进行。或者,更好的办法是将remObserver函数更改为知道是否有通知过程正在进行,并仅在完成所有操作后排队删除。

这是我使用修改后的观察者模式的方法。但是,如果必须立即执行删除操作,那么您将被困扰于处理边角情况的逻辑复杂性中。 - Ron Warholic
1
诚然,那是我的第一反应。然而,我认为这种态度恰恰显示了“模式”存在的问题所在。软件开发不是关于将完美的砖块拼凑在一起,而是关于解决问题。 - T.E.D.
2
@T.E.D. 我完全同意模式不是一种圣经般的东西,不应该绝对不允许修改。但我经常发现开发人员倾向于过度复杂化事情,只是为了快速解决问题,而没有退后一步问问真正的问题所在。可能他们正在将观察者模式用于其未设计的情况下。 - Lee
1
@李:太对了。区分这两种情况是困难的部分。 - T.E.D.

4
问题在于所有权。可以使用智能指针,例如 boost::shared_ptrboost::weak_ptr 类,以延长观察者的生命周期,避免“取消分配”的问题。

3

解决这个问题有几种方法:

  1. Use boost::signal it allows automatic connection removal when the object destroyed. But you should be very careful with thread safety
  2. Use boost::weak_ptr or tr1::weak_ptr for managment of observers, and boost::shared_ptr or tr1::shared_ptr for observers them self -- reference counting would help you for invalidating objects, weak_ptr would let you know if object exists.
  3. If you are running over some event loop, make sure, that each observer does not destroy itself, add itself or any other in the same call. Just postpone the job, meaning

    SomeObserver::notify()
    {
       main_loop.post(boost::bind(&SomeObserver::someMember,this));
    }
    

0

在迭代时,你永远无法避免观察者被移除。

即使在尝试调用其notify()函数时,观察者甚至也可以被移除。

因此,我认为你需要一个try/catch机制。

锁定是为了确保在复制观察者集合时不会更改观察者集合。

  lock(observers)
  set<Observer> os = observers.copy();
  unlock(observers)
  for (Observer o: os) {
    try { o.notify() }
    catch (Exception e) {
      print "notification of "+o+"failed:"+e
    }
  }

0

这样做会慢一些,因为你要复制集合,但我认为这样更简单。

class Subject {
public:
   void addObserver(Observer*);
   void remObserver(Observer*);
private:
   void notifyAll();
   std::set<Observer*> observers;
};

void Subject::addObserver(Observer* o) {
  observers.insert(o);
}

void Subject::remObserver(Observer* o) {
  observers.erase(o);
}

void Subject::notifyAll() {
  std::set<Observer*> copy(observers);
  std::set<Observer*>::iterator it = copy.begin();
  while (it != copy.end()) {
    if (observers.find(*it) != observers.end())
      (*it)->notify();
    ++it;
  }
}

我看到的问题是对每个元素进行“find(*it)”检查,而不是复制一个集合。这增加了复杂性,并且当你有很多观察者时可能会很痛苦。不管怎样,这个想法很酷,谢谢! - SadSido

0
如果您的程序是多线程的,您可能需要在这里使用一些锁定。
无论如何,从您的描述来看,问题不是并发(多线程),而是由Observer::notify()调用引起的变异。如果是这种情况,那么您可以通过使用向量并通过索引而不是迭代器遍历它来解决问题。
for(int i = 0; i < observers.size(); ++i)
  observers[i]->notify();

使用向量并不是一个解决方案,因为它会使一些观察者未被处理... 假设你有 i = 3,观察者 #3 自杀了,向量被移动,你增加了 i 然后... 其中一个观察者被遗漏了 :) - SadSido
如果你坚持多线程的警告,我会投赞成票的。从描述中,我非常怀疑有一些线程正在运行,这意味着可能会出现竞态条件。 - T.E.D.

0
如何创建一个名为current的成员迭代器(初始化为end迭代器)。
void remObserver(Observer* obs)
{
    list<Observer*>::iterator i = observers.find(obs);
    if (i == current) { ++current; }
    observers.erase(i);
}

void notifyAll()
{
    current = observers.begin();
    while (current != observers.end())
    {
        // it's important that current is incremented before notify is called
        Observer* obs = *current++;
        obs->notify(); 
    }
}

当观察者决定在主题上重新触发notifyAll时,这种策略很容易被击败。那么主题应该有多少个“当前”成员呢?嗯,我想这是一个理论问题 - 我们应该真正限制我们的观察者。感谢您的回答! - SadSido

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接