观察数据变化的不同方式

时间:2010-07-01 20:23:46

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

在我的应用程序中,我有很多课程。这些类中的大多数都存储了一些数据,如果其中一个数据类的内容发生变化,我的应用程序中的其他模块也会“更新”,这一点非常重要。

这样做的典型方法是:

void MyDataClass::setMember(double d)
{
m_member = d;
notifyAllObservers();
}

如果不经常更改成员并且“观察类”需要尽可能快地更新,这是一个非常好的方法。

另一种观察变化的方法是:

void MyDataClass::setMember(double d)
{
setDirty();
m_member = d;
}

如果成员多次更改,这是一个很好的方法,并且“观察类”会在所有“脏”实例中定期查看。

不幸的是,我的课程中混合了两种数据成员。有些是经常改变的(我可以和普通的观察者一起生活),有些被改变很多次(这是在复杂的数学算法中),并且每当值改变时调用观察者将会破坏我的应用程序的性能。

是否还有其他观察数据变化的技巧,或者您可以轻松组合几种观察数据变化的不同方法的模式?

虽然这是一个与语言无关的问题(我可以尝试理解其他语言中的示例),但最终的解决方案应该适用于C ++。

4 个答案:

答案 0 :(得分:6)

你所描述的两种方法涵盖(概念上)两个方面,但我认为你没有充分解释它们的利弊。

您应该注意一个项目,这是人口因素。

  • 当有许多通知者和很少的观察者时,推送方法很棒
  • 当没有通知者和许多观察者时,拉方法很棒

如果您有许多通知者,并且您的观察者应该遍历每个通知符以发现dirty的2或3 ...它将无效。另一方面,如果你有很多观察者,并且在每次更新时都需要通知他们所有人,那么你可能注定要失败,因为简单地遍历所有这些就会破坏你的表现。

有一种可能性,你没有谈到:将这两种方法结合起来,与另一种间接方式相结合。

  • 将每次更改推送到GlobalObserver
  • 让每位观察员在需要时检查GlobalObserver

但这并不容易,因为每个观察者都需要记住它最后一次检查的时间,仅通知它尚未观察到的变化。通常的技巧是使用时代。

Epoch 0       Epoch 1      Epoch 2
event1        event2       ...
...           ...

每个观察者都记得它需要阅读的下一个纪元(当观察者订阅它时,它被给予当前的纪元),并从该纪元读取到当前纪元以了解所有事件。通常,通知程序无法访问当前时期,例如,您可以决定每次读取请求到达时切换时期(如果当前时期不为空)。

这里的困难是知道何时丢弃时期(当不再需要它们时)。这需要某种引用计数。请记住,GlobalObserver是将当前时期返回给对象的那个。因此,我们为每个时代引入一个计数器,它只计算有多少观察者尚未观察到这个时代(以及后来的时期)。

  • 在订阅时,我们返回纪元号并增加此纪元的计数器
  • 在轮询中,我们减少轮询的纪元的计数器并返回当前的纪元号并增加其计数器
  • 在取消订阅时,我们减少了纪元的计数器 - >确保析构函数取消订阅!

也可以将它与超时结合起来,记录我们最后一次修改时期(即创建下一个时期)并决定在一定时间后我们可以丢弃它(在这种情况下我们收回计数器和将它添加到下一个时代。)

请注意,该方案可扩展为多线程,因为一个纪元可以进行写入(在堆栈上进行推送操作),而其他时间段是只读的(原子计数器除外)。在不需要分配内存的情况下,可以使用无锁操作来推送堆栈。在堆栈完成时决定切换纪元是完全合理的。

答案 1 :(得分:4)

  

观察数据变化的其他技巧

不是真的。你有“推”和“拉”设计模式。没有任何其他选择。

notifyAllObservers推送,普通属性访问是

我建议一致性。显然,您遇到的情况是对象有很多更改,但所有更改都不会渗透到其他对象。

不要被此混淆。

观察者不需要进行昂贵的计算,仅仅因为它被通知了变化。

我认为你应该有这样的类来处理“频繁更改但请求缓慢”的类。

class PeriodicObserver {
    bool dirty;
    public void notification(...) {
        // save the changed value; do nothing more.  Speed matters.
        this.dirty= True;
    }
    public result getMyValue() {
        if( this.dirty ) { 
            // recompute now
        }
        return the value
}

答案 2 :(得分:2)

你有拉动和推送通知。我会考虑尽可能地隐藏细节,所以至少通知者不需要关心差异:

class notifier { 
public:
    virtual void operator()() = 0;
};

class pull_notifier : public notifier { 
    bool dirty;
public:
    lazy_notifier() : dirty(false) {}
    void operator()() { dirty = true; }
    operator bool() { return dirty; }
};

class push_notifier : public notifier { 
    void (*callback)();
public:
    push_notifier(void (*c)()) : callback(c) {}
    void operator()() { callback(); }
};

然后观察者可以根据需要传递push_notifierpull_notifier,并且mutator不需要关心差异:

class MyDataClass { 
    notifier &notify;
    double m_member;
public:
    MyDataClass(notifier &n) : n_(n) {}
    void SetMember(double d) { 
        m_member = d; 
        notify();
    }
};

目前我每个mutator只用一个观察者编写它,但是如果你需要更多的话,将它改为指向一个mutator的观察者对象的指针向量是相当简单的。有了它,给定的mutator将支持push_和pull_通知器的任意组合。如果您确定给定的mutator只会使用pull_notifier或push_notifiers,您可以考虑使用带有通知程序的模板作为模板参数(策略)来避免虚函数调用的开销(可能忽略不计)对于push_notifier,但pull_notifier的情况则较少。

答案 3 :(得分:0)

您描述了两个可用的高级选项(推送与拉/轮询)。我不知道其他任何选择。