【问题标题】:Generic observer pattern in C++C++ 中的通用观察者模式
【发布时间】:2014-05-26 20:08:41
【问题描述】:

在我的应用程序的许多情况下,我需要 A 类将自己注册为 B 类的侦听器,以便在发生某些事情时接收通知。在每种情况下,我都定义了一个单独的接口 B 实现并且 A 可以调用 do。因此,例如,A 将具有以下方法:

void registerSomeEventListener(SomeEventListener l);

另外,在很多情况下,B 需要支持多个监听器,所以我重新实现了注册和 notifyAll 逻辑。

我知道的一种通用方法是拥有一些 EventListener(由 A 实现)和 EventNotifier(由 B 实现)类。在这种情况下,每个事件都由一个字符串标识,并且 A 实现了该方法:

void eventNotified(string eventType);

我认为这不是一个好的解决方案。如果 A 侦听多个事件,则会导致许多 if-else 语句,并且当事件名称仅在侦听器或通知器中更改时可能会导致错误。

我想知道在 C++ 中实现观察者模式的正确方法是什么?

【问题讨论】:

  • wrote something about this 不久前。我的“听众”只是可调用的实体。我不确定它是否完全适合您的用例,但它可能值得一看。
  • 没有对错之分。由于各种原因,有几十种方法被实施。最常见的是使用虚函数,并让侦听器从这个基本接口派生。但它有局限性。您还可以存储单独的类实例和方法名称,以便只有那些注册的类接收回调。如果您使用 boost 那么他们有一些很好的实用程序来处理这个问题。如果您更愿意自己编写,那么您需要研究模板,以及谁来定义指向类方法的指针..
  • @Dan 所以你的意思是最常见的方式是非泛型方式?只需为每个场景创建一个特定的侦听器类型,然后从它派生类 A 并实现其功能?
  • 我不会这么说,但是你得到的越通用,它的性能就越差。您不能拥有指向类实例的通用指针。除非它们都从同一个基类派生,否则该类的方法指针必须在基类中定义。但是,如果您不关心性能,那么您可以从具有虚拟 Invoke 的基类创建一个 thunking 层,该虚拟 Invoke 代理特定类和方法函数绑定。但是虚拟调用通常会导致缓存未命中。你通常可以忍受。
  • 如果你需要更高性能的东西,IE:每帧被调用数百次,那么你应该使用更具体的回调机制..

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


【解决方案1】:

看看boost::signals2。它提供了一种通用机制来定义其他对象可以注册的“信号”。然后,信号所有者可以通过“触发”信号来通知观察者。主题将信号定义为成员,而不是注册方法,然后跟踪连接的观察者并在启动时通知他们。信号是静态类型的,并接受具有匹配签名的每个函数。这样做的好处是不需要继承,因此比传统的观察者继承层次结构耦合更弱。

class Subject {
public:
    void setData(int x) {
         data_ = x;
         dataChanged(x);
     }

     boost::signals2<void (int)> dataChanged;

private:
    int data_;
};

class Observer {
public:
    Observer(Subject& s) {
        c_ = s.dataChanged.connect([&](int x) {this->processData(x);});
    }

    ~Observer() {
        c_.disconnect();
    }

private:
    void processData(int x) {
        std::cout << "Updated: " << x << std::endl;
    }

    boost::signals2::connection c_;
};

int main() {
    Subject s;

    Observer o1(s);
    Observer o2(s);

    s.setData(42);

    return 0;
}

在本例中,主体持有一些 int 数据,并在数据更改时通知所有注册的观察者。

【讨论】:

    【解决方案2】:

    假设您有一个通用的事件触发对象:

    class base_invoke {
    public:
        virtual ~base_invoke () {};
    
        virtual void Invoke() = 0;
    }
    

    但是你想在不同类型的对象上触发事件,所以你从 base 派生:

    template<class C>
    class methodWrapper : public base_invoke {
    
    public:
    typedef void (C::*pfMethodWrapperArgs0)();
    
        C *             mInstance;
        pfMethodWrapperArgs0 mMethod;
    
    public:
        methodWrapper(C * instance, pfMethodWrapperArgs0 meth) 
        : mInstance(instance) 
    {
            mMethod = meth;
        }
    
        virtual void Invoke () {
            (mInstance->*mMethod)();
        }
    }
    

    现在,如果您为指向 base_invoke 的指针集合创建一个包装器,您可以调用每个触发对象并在任何您想要的类上发出信号。

    你也可以把这个集合类变成一个点火对象的工厂。使工作简单化。

    class Event {
    
    protected:
        Collection<base_invoke *> mObservers;
    
    public:
    
        // class method observers
        template<class C>
        void Add (C * classInstance, typename methodWrapper<C>::pfMethodWrapperArgs0 meth) {
            methodWrapper<C> * mw = NEW(methodWrapper<C>)(classInstance, meth);
            mObservers.Add(ObserverEntry(key, mw));
        }
    
        void Invoke () {
            int count = mObservers.Count();
            for (int i = 0; i < count; ++i) {
                mObservers[i]->Invoke();
            }
        }
    };
    

    您已经完成了艰苦的工作。在您希望听众订阅的任何地方添加一个 Event 对象。您可能希望扩展它以允许移除侦听器,并且可能需要一些函数参数,但核心几乎相同。

    【讨论】:

    • 这样做的好处是允许您为每个被触发的事件调用不同的方法。当您注册正确的事件时,您担心的 if else 会被执行..
    • 对不起,我试图列出完整的来源,但网站有 30000 个字符的限制。希望这会有所帮助。
    猜你喜欢
    • 2016-05-11
    • 2021-11-22
    • 2013-12-30
    • 2021-05-08
    • 2016-02-20
    • 2023-04-10
    • 1970-01-01
    • 2013-10-28
    • 1970-01-01
    相关资源
    最近更新 更多