C++ 观察数据变化的不同方式

C++ 观察数据变化的不同方式,c++,language-agnostic,design-patterns,observer-pattern,C++,Language Agnostic,Design Patterns,Observer Pattern,在我的应用程序中,我有很多类。这些类中的大多数都存储了相当多的数据,如果其中一个数据类的内容发生了变化,那么我的应用程序中的其他模块也会被“更新”,这一点很重要 这样做的典型方式如下: void MyDataClass::setMember(double d) { m_member = d; notifyAllObservers(); } 如果成员不经常更换,并且“观察类”需要尽快更新,那么这是一个非常好的方法 观察变化的另一种方式是: void MyDataClass::setMember(

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

这样做的典型方式如下:

void MyDataClass::setMember(double d)
{
m_member = d;
notifyAllObservers();
}
如果成员不经常更换,并且“观察类”需要尽快更新,那么这是一个非常好的方法

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

void MyDataClass::setMember(double d)
{
setDirty();
m_member = d;
}
如果成员被多次更改,并且“观察类”在所有“脏”实例中定期查看,那么这是一个很好的方法

不幸的是,我的类中混合了这两种数据成员。有些变化不太频繁(我可以和普通的观察者一起生活),有些变化很多次(这是在复杂的数学算法中),每次值变化都调用观察者会破坏我的应用程序的性能

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


虽然这是一个相当独立的语言问题(我可以尝试理解其他语言的例子),但最终的解决方案应该在C++中工作。

< p>你描述了两个可用的高级选项(推VS拉/轮询)。我知道没有其他选择

观察数据变化的其他技巧

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

notifyAllobserver
是一个推式操作,普通属性访问是一个拉式操作

我建议保持一致。显然,您会遇到这样的情况:一个对象有许多更改,但所有更改不会渗透到其他对象

别被这搞糊涂了

观测者不需要仅仅因为接到变更通知就进行昂贵的计算

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


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

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(); }
};
class MyDataClass { 
    notifier &notify;
    double m_member;
public:
    MyDataClass(notifier &n) : n_(n) {}
    void SetMember(double d) { 
        m_member = d; 
        notify();
    }
};
然后,观察者可以通过一个
push\u通知器
或一个
pull\u通知器
,只要它认为合适,而变异者不需要关心差异:

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(); }
};
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_uuu和pull_uuuu通知程序的任意组合。如果你确信一个给定的调制器只会使用<代码> Pull NOTIVER < /Cult> s或Purth-NosiTube,你可以考虑使用一个带有通知器的模板作为一个模板参数(一个策略)来避免虚函数调用的开销(对于一个代码> PurthoNoistor 可能是可以忽略的,但是对于<代码> Pull通知器更少。.

您描述的两种方法(从概念上)涵盖了这两个方面,但是我认为您没有充分解释它们的优缺点

有一点你应该注意,那就是人口因素

  • 当有很多通知程序和很少的观察者时,Push方法非常有用
  • 当通知程序和观察程序较少时,Pull方法非常有用
如果您有许多通知程序,并且您的观察者应该迭代每个通知程序,以发现
脏的2或3个通知程序
。。。这行不通。另一方面,如果您有许多观察者,并且在每次更新时您都需要通知所有观察者,那么您很可能注定要失败,因为简单地遍历所有观察者会降低您的性能

但是,有一种可能性您没有提到:将这两种方法结合起来,再进行另一种间接处理

  • 将每次更改推送到
    GlobalObserver
  • 必要时,让每个观察者检查
    GlobalObserver
但这并不是那么容易,因为每个观察者都需要记住最后一次检查是在什么时候,只得到关于尚未观察到的更改的通知。通常的技巧是使用纪元

Epoch 0       Epoch 1      Epoch 2
event1        event2       ...
...           ...
每个观察者都会记住它需要读取的下一个历元(当观察者订阅时,它会得到当前历元作为回报),并从这个历元一直读取到当前历元,以了解所有事件。通常,通知程序无法访问当前历元,例如,您可以决定在每次读取请求到达时切换历元(如果当前历元不是空的)

这里的困难是要知道什么时候该抛弃时代(当不再需要它们的时候)。这需要某种类型的引用计数。请记住,
GlobalObserver
是向对象返回当前纪元的服务器。因此,我们为每个历元引入一个计数器,它只计算有多少观测者尚未观察到这个历元(以及随后的历元)

  • 订阅时,我们返回历元编号并递增此历元的计数器
  • 在轮询时,我们减少被轮询历元的计数器,返回当前历元编号并增加其计数器
  • 取消订阅时,我们减少历元-->的计数器,确保析构函数取消订阅
也可以将其与超时结合起来,记录上次修改历元的时间(即创建下一个历元),并决定在一定时间后可以丢弃它(在这种情况下,我们回收计数器并将其添加到下一个历元)

请注意,该方案扩展到多线程,因为一个历元可用于写入(堆栈上的推操作),而其他历元是只读的(原子计数器除外)。在没有内存的情况下,可以使用无锁操作推送堆栈