如何在C++中实现观察者模式

9
我有一个Animation类。我需要为Animation中的Play、Pause和Stop事件添加一些观察者。我找到了两种解决方案,但我不知道该选择哪个。
1. 使用boost::signals或类似的东西,并为每个事件注册回调函数。 2. 创建一个简单的接口,具有3个纯虚函数(OnPlay()、OnPause()、OnStop()),并将实现此接口的对象传递给Animation类。
每种方法都有优缺点。我尝试列举我目前发现的一些:
1. 的优点: - 可以使用任何成员函数/自由函数作为回调 - 如果不关心所有函数,则不必实现所有3个函数 - 同一对象可以作为多个动画的观察者而无需从Animation类传递额外参数
1. 的缺点: - 必须为每个回调创建可调用对象 - 如果以后要添加新事件,则很难找到使用它的地方(编译器不能强制我实现或忽略新事件)。 - 语法有些奇怪(必须在各处使用std::bind/boost::bind)。
2. 的优点: - 构造易于理解 - 如果我在Animation/Observer接口类中添加新事件,编译器将强制我实现(可能为空)新函数。
2. 的缺点: - 即使只使用其中一个,也必须实现(可能为空)3个函数 - 同一对象不能作为不同动画的观察者,而无需从动画传递某些额外参数(ID或其他内容)。 - 无法使用自由函数。
请问您应该选择哪一个?根据您的经验,对于这个问题,是第一种方法的自由还是第二种方法的清晰易懂的代码更好?您能否为两种方法提供其他优缺点或其他解决方案?

6
在C++11中(我假设您可以使用它,因为您用标签为其打了标记),Lambda表达式消除了大多数“不利条件1”的缺点。 - Mike Seymour
如果使用std::bind对你来说是奇怪的语法(特别是与具有虚拟OnWhatever函数的接口相比),那么你应该重新考虑你选择的编程语言。 - Christian Rau
@ChristianRau 不是为我而言,但不止我一个人在这个代码库上工作。 - Mircea Ispas
3个回答

3

首先,了解“绑定”是否在编译时已知是有用的。如果是这样,我建议您查看策略类。

除此之外,我会采用两种解决方案的混合方法,即使用接口方法并实现一个接口作为信号/自由函数的中继器。这样,您可以拥有默认行为,可以添加实现整个接口的自定义对象,并且基本上具有两种方法的优点以及更大的灵活性。

这里是所提议的方法的基本示例,希望对您有所帮助。

#include <functional>

using namespace std;

template <class ObserverPolicy>
class Animation : public ObserverPolicy{

};

class MonolithicObserver{
    public:
    void play(){
        state = playing;
    }
    void pause(){
        if(playing == state)
            state = stopped;
    }
    void stop(){
        state = stopped;
    }
    private:
    enum {playing, paused, stopped} state;
};

struct doNothing{
    static void play(){}
    static void pause(){}
    static void stop(){}
};

struct throwException{
    class noPlay{};
    class noPause{};
    class noStop{};
    static void play(){
        throw noPlay();
    }
    static void pause(){
        throw noPause();
    }
    static void stop(){
        throw noStop();
    }
};

template <class DefaultPolicy = doNothing>
class FreeFunctionObserver{
    public:
    void play(){
        if(playHandle)
            playHandle();
        else
            DefaultPolicy::play();
    }
    void pause(){
        if(pauseHandle)
            pauseHandle();
        else
            DefaultPolicy::pause();
    }
    void stop(){
        if(stopHandle)
            stopHandle();
        else
            DefaultPolicy::stop();
    }
    void setPlayHandle(std::function<void(void)> p){
        playHandle = p;
    }
    void setPauseHandle(std::function<void(void)> p){
        pauseHandle = p;
    }
    void setStopHandle(std::function<void(void)> p){
        stopHandle = p;
    }
    private:
    std::function<void(void)> playHandle;
    std::function<void(void)> pauseHandle;
    std::function<void(void)> stopHandle;
};

void play(){}
void pause(){}
void stop(){}

int main(){
    Animation<FreeFunctionObserver<> > affo;
    affo.setPlayHandle(play);
    affo.setPauseHandle(pause);
    affo.setStopHandle(stop);
    affo.play();
    affo.pause();
    affo.stop();

    Animation<FreeFunctionObserver<throwException> > affot;
    try{
        affot.play();
    }
    catch(throwException::noPlay&){}

    Animation<MonolithicObserver> amo;
    amo.play();
    amo.pause();
    amo.stop();
}

你可以在这里尝试:http://coliru.stacked-crooked.com/view?id=394c1769098521db97ccbdd8f43ec38a-37e310707bc41fe992acad9d9b03bc56。这个例子特别使用了一个策略类(因此没有“正式”定义接口),你可以像setPlayHandle一样“丰富”接口,但你也可以使用运行时绑定来实现类似的效果。

1

对于除了最简单的玩具示例之外的所有内容,Boost.Signals2 在我看来都是更优秀的解决方案。它经过良好设计、测试和文档化。重复造轮子只适用于课堂练习,而不是生产代码。例如,使自己的观察者线程安全并不容易做到正确和高效。

专门讨论你列出的缺点:

  • 你可以编写 C++11 lambda 表达式,而不是使用命名函数对象或 boost::bind 语法(对于大多数用途来说,它并不是真正复杂的)
  • 我不太理解你关于未使用事件的观点。你可以进行相当先进的 连接管理 来查询和断开信号与槽的连接。

TL;DR: 熟悉 Boost.Signals2。


我不敢说Boost.Signals或Boost.Signals2在性能方面是“高效”的。然而,我同意它们都有完整的文档和维护。@OP:你应该尝试双方面,这样你就可以了解你原来对每个方面的优缺点是否符合_你_的期望。至于信号库,选择一个定期维护、有良好文档,并且对可能的线程安全要求具备线程安全库。 - ApEk
@ApEk 我会说,(几乎)所有的Boost库都是高效的基于它们所提供的功能,而不使用它们的唯一原因是它们有时可能提供的东西比你需要的更多,因此你将为你不会使用的东西付费。 - TemplateRex

0

我认为,你可以两种方式都使用 :) 但这取决于需求。我有一些代码,在其中我使用了这两种模式。有很多函数被调用 onSomething()(onMouseButton()、onKey()、onDragStart() 等),但也有回调函数。当我需要实现某些行为,但是针对整个对象类时,我使用 onSomething() 方法。但如果我有一堆相同类的对象,但只有部分对象需要扩展功能 - 回调是一个完美的选择。

在实现中,它是这样完成的: 有一些分派代码尝试使用 onSomething() 方法(返回 bool),如果其结果为 false,则检查是否定义了回调,如果是,则执行它。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接