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

时间:2009-06-19 14:18:53

标签: c++ design-patterns observer-pattern

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

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”最有希望的决定,但我不能在项目上引入提升,这样的决定必须由项目负责人做出(我们在Playstation下编写) );

[3]“使用shared__ptr”这会阻止观察者取消分配。有些子系统可能依赖于内存池清理,所以我认为我不能使用shared_ptr。

[4]“推迟观察员释放”队列观察员在通知时删除,然后使用第二个周期删除它们。不幸的是,我无法阻止重新分配,所以我使用了某种“适配器”包装观察者的技巧,实际上保留了“适配器”列表。在析构函数中,观察者从他们的适配器取消分配,然后我采取第二个循环来销毁空适配器。

P.S。没关系,我编辑我的问题来总结所有的帖子?我是StackOverflow上的菜鸟......

14 个答案:

答案 0 :(得分:14)

非常有趣的问题。

试试这个:

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

    for(所有已登记的观察员){if(观察员)观察员 - > notify(); }

  3. 在notifyAll的末尾添加另一个循环以从观察者列表中删除所有空条目

答案 1 :(得分:7)

就个人而言,我使用boost::signals来实现我的观察者;我必须检查,但我相信它会处理上述情况(已编辑:找到它,请参阅"When can disconnections occur")。它简化了您的实现,并且不依赖于创建自定义类:

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.
}

答案 2 :(得分:6)

一名男子去看医生并说:“当我这样举起手臂时,医生真疼了!”医生说,“不要这样做。”

最简单的解决方案是与您的团队合作并告诉他们不要这样做。如果观察者“确实需要”自杀或所有观察者,则安排通知结束时的行动。或者,更好的是,更改remObserver函数以了解是否发生了通知过程,并在完成所有操作后排队删除。

答案 3 :(得分:5)

这是T.E.D.的一个变种。已经提出。

只要remObserver可以使条目为空而不是立即删除它,那么您可以将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()调用触发了自身或位于列表中较早位置的观察者的移除,那么list元素的实际删除将推迟到下一个notifyAll()。但是只要在列表上运行的任何函数在适当的时候都要仔细检查空条目,那么这应该不是问题。

答案 4 :(得分:4)

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

答案 5 :(得分:3)

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

  1. 使用boost::signal它可以在对象被破坏时自动连接删除。但你应该非常小心线程安全
  2. 使用boost::weak_ptrtr1::weak_ptr来管理观察员,boost::shared_ptrtr1::shared_ptr用于观察者他们自我引用计数会 帮助你使对象无效,weak_ptr会让你知道对象是否存在。
  3. 如果您正在运行某个事件循环,请确保每个观察者都没有 摧毁自己,在同一个电话中添加自己或任何其他人。只是推迟工作,意思是

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

答案 6 :(得分:0)

如何在for循环中使用链接列表?

答案 7 :(得分:0)

如果你的程序是多线程的,你可能需要在这里使用一些锁定。

无论如何,根据你的描述,似乎问题不是并发(多重问题),而是由Observer :: notify()调用引起的突变。如果是这种情况,那么您可以通过使用向量并通过索引而不是迭代器遍历它来解决问题。

for(int i = 0; i < observers.size(); ++i)
  observers[i]->notify();

答案 8 :(得分: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(); 
    }
}

答案 9 :(得分:0)

在对删除具有弹性的通知容器上定义和使用重型迭代器(例如,如前所述,将其置零)并且可以处理添加(例如追加)

另一方面,如果要在通知期间强制保持容器const,请声明notifyAll并将容器作为const迭代。

答案 10 :(得分: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;
  }
}

答案 11 :(得分:0)

你永远不能避免在迭代时删除观察者。

您甚至可以删除观察者WHILE,并尝试调用其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
    }
  }

答案 12 :(得分:0)

几个月前,当我遇到这篇文章时,我正在寻找解决这个问题的方法。它让我思考解决方案,我认为我有一个不依赖于提升,智能指针等。

简而言之,这是解决方案的草图:

  1. Observer是一个单例,其中包含让主题注册兴趣的键。因为它是一个单例,所以它总是存在。
  2. 每个主题都来自一个共同的基类。基类有一个抽象的虚函数Notify(...),它必须在派生类中实现,还有一个析构函数,当它被删除时,它会从Observer中删除它(它总是可以到达)。
  3. 在Observer本身内部,如果在Notify(...)正在进行时调用Detach(...),任何分离的Subjects都会在列表中结束。
  4. 在Observer上调用Notify(...)时,它会创建Subject列表的临时副本。当它迭代它时,它将它与最近分离的它进行比较。如果目标不在其上,则在目标上调用Notify(...)。否则,它会被跳过。
  5. 观察者中的通知(...)还跟踪处理级联呼叫的深度(A通知B,C,D和D.Notify(...)触发Notify(...)呼叫到E等。)
  6. 这似乎运作良好。该解决方案与源代码一起发布在网络here上。这是一个相对较新的设计,因此非常感谢任何反馈。

答案 13 :(得分:0)

我刚写了一个完整的观察者类。我会在经过测试后加入它。

但我对你的问题的回答是:处理案件!

我的版本确实允许在notify循环中触发notify循环(它们立即运行,将其视为深度优先递归),但是有一个计数器,以便Observable类知道通知正在运行以及有多少深度

如果一个观察者被删除,它的析构函数会告诉所有可观察者它已经订阅了有关破坏的信息。如果它们不在观察者所在的通知循环中,则从std :: list&lt; pair&lt; Observer *,int&gt;&gt;中删除该observable。对于该事件,如果它处于循环中,则其在列表中的条目无效,并且命令被推入队列,该队列将在通知计数器降至零时运行。该命令将删除无效的条目。

所以基本上,如果你不能安全地删除(因为可能有一个迭代器持有将通知你的条目),那么你使条目无效而不是删除它。

就像所有并发的无等待系统一样,规则是 - 如果你没有被锁定,则处理这个案例,但是如果你那么你排队工作,持有锁的人将在他释放锁时完成工作。