【问题标题】:Different ways of observing data changes观察数据变化的不同方式
【发布时间】:2010-07-01 20:23:45
【问题描述】:

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

典型的做法是这样的:

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

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

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

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

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

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

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

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

【问题讨论】:

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


    【解决方案1】:

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

    您应该注意一项,那就是人口因素。

    • 当通知者多而观察者少时,推送方法非常有用
    • 当通知者少而观察者多时,Pull 方法非常有用

    如果您有许多通知程序,并且您的观察者应该遍历它们中的每一个以发现 dirty 的 2 或 3 个...它不会工作。另一方面,如果您有很多观察者,并且在每次更新时都需要通知所有观察者,那么您可能注定要失败,因为简单地遍历所有观察者会影响您的性能。

    但是,您还没有谈到一种可能性:将这两种方法与另一个间接级别结合起来。

    • 将每个更改推送到GlobalObserver
    • 让每个观察者在需要时检查GlobalObserver

    但这并不容易,因为每个观察者都需要记住它上次检查的时间是什么时候,以便仅在它尚未观察到的更改时收到通知。通常的技巧是使用 epoch。

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

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

    这里的困难是知道何时丢弃 epoch(何时不再需要)。这需要某种引用计数。请记住,GlobalObserver 是将当前时期返回给对象的那个。所以我们为每个 epoch 引入了一个计数器,它简单地计算有多少观察者还没有观察到这个 epoch(以及后续的)。

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

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

    请注意,该方案可扩展到多线程,因为一个时期可用于写入(堆栈上的推送操作),而其他时期是只读的(原子计数器除外)。在不需要分配内存的情况下,可以使用无锁操作来压栈。在堆栈完成后决定切换 epoch 是非常明智的。

    【讨论】:

    【解决方案2】:

    观察数据变化的其他技巧

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

    notifyAllObserverspush,普通属性访问是pull

    我建议保持一致性。显然,您遇到的情况是,一个对象有很多变化,但所有变化并没有渗透到其他对象。

    不要被这个弄糊涂了。

    观察者不需要仅仅因为收到更改通知而进行昂贵的计算。

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

    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
    }
    

    【讨论】:

      【解决方案3】:

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

      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_notifiers 或 push_notifiers,您可以考虑使用带有通知器的模板作为模板参数(策略)以避免虚函数调用的开销(对于push_notifier,但对于 pull_notifier 则更少)。

      【讨论】:

        【解决方案4】:

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

        【讨论】:

          猜你喜欢
          • 2019-03-01
          • 2012-07-14
          • 2012-02-22
          • 1970-01-01
          • 2017-01-03
          • 2018-12-15
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多