在C++中,我们会经常用到观察者模式(回调模式,Delegate模式等,意思都一样),比如当Source中的某个参数发生了变化时,我们通过观察者模式进行回调通知,下面是一个例子:
class SourceParamsListener { public: virtual ~SourceParamsListener(){}; virtual void onSetRotateX(boost::shared_ptr<Source> src, int rotateX) = 0; virtual void onSetRotateY(boost::shared_ptr<Source> src, int rotateY) = 0; };
所有的事件响应函数都是纯虚函数(pure virtual functions),意味着如果你的子类要实现这个接口来监听Source的参数变化,子类必须实现所有的纯虚函数,否则子类就不能实例化。表面看起来没啥问题,但是在实际的工程项目中,有时候会有点蛋疼,特别是有大量的事件响应函数onXXX时(比如10个),即使子类只对其中的一个事件感兴趣,只需要处理一个事件,它也需要定义所有的响应函数,而这些函数体中仅仅是一个空函数,因为它对这些事件不感兴趣。
这会造成一个问题:大量的空函数代码会污染我们的代码,影响我们阅读代码,对于一个加入项目的新手来说,很难一下看出子类究竟在处理哪些事件。
让我们稍微修改一点点,把SourceParamsListener从一个纯虚基类变成一个抽象类,即事件响应函数都有一个默认的实现,实际上就是一个空的函数体,啥都不做(可能有些必须要实现的接口,还是保留纯虚函数),代码如下:
class SourceParamsListener { public: virtual ~SourceParamsListener(){}; virtual void onSetRotateX(boost::shared_ptr<Source> src, int rotateX){}; virtual void onSetRotateY(boost::shared_ptr<Source> src, int rotateY){}; };
这样,当子类要响应事件时,它只需要重载自己感兴趣的事件即可,比如它只对onSetRotateX感兴趣,那就只重载onSetRotateX函数吧,其它所有的事件响应函数,因为基类已经提供了默认的实现,所以在子类中就不用再提供默认的实现了,这样子类的代码就会简介很多了。
放眼对比其它编程语言,比如OC,我们会发现类似的理念。以Objective-C中的protocol为例,它的protocol就相当于是interface了,典型的代码如下:
@protocol SourceParamsListener <NSObject> //必须实现 @required - (int)onNeedSourceId; //可选实现 @optional - (void)onSetRotateX; - (void)onSetRotateY; @end
带有required修饰符的,就相当于C++中的纯虚函数,子类中必须要响应的事件。而带有optional修饰符的,则相当于提供了一个默认的空函数体,子类只需要重载自己感兴趣的就行了。
但是,当我们看到Windows中的COM接口时,又发现一个例外,COM中的接口全部都是纯虚函数,要求子类必须实现所有的事件响应函数,比如Core Audio APIs中的IMMNotificationClient接口:
IMMNotificationClient : public IUnknown { public: virtual /* [helpstring][id] */ HRESULT STDMETHODCALLTYPE OnDeviceStateChanged( /* [in] */ __in LPCWSTR pwstrDeviceId, /* [in] */ __in DWORD dwNewState) = 0; virtual /* [helpstring][id] */ HRESULT STDMETHODCALLTYPE OnDeviceAdded( /* [in] */ __in LPCWSTR pwstrDeviceId) = 0; virtual /* [helpstring][id] */ HRESULT STDMETHODCALLTYPE OnDeviceRemoved( /* [in] */ __in LPCWSTR pwstrDeviceId) = 0; virtual /* [helpstring][id] */ HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged( /* [in] */ __in EDataFlow flow, /* [in] */ __in ERole role, /* [in] */ __in LPCWSTR pwstrDefaultDeviceId) = 0; virtual /* [helpstring][id] */ HRESULT STDMETHODCALLTYPE OnPropertyValueChanged( /* [in] */ __in LPCWSTR pwstrDeviceId, /* [in] */ __in const PROPERTYKEY key) = 0; };
看到没有,全部是纯虚函数。这还不算,还有它的基类IUnknown中的纯虚函数:
IUnknown { public: BEGIN_INTERFACE virtual HRESULT STDMETHODCALLTYPE QueryInterface( /* [in] */ REFIID riid, /* [iid_is][out] */ __RPC__deref_out void __RPC_FAR *__RPC_FAR *ppvObject) = 0; virtual ULONG STDMETHODCALLTYPE AddRef( void) = 0; virtual ULONG STDMETHODCALLTYPE Release( void) = 0;
我在程序中只想监听OnDefaultDeviceChanged事件,即默认的音频播放设备发生变化的事件,来看看我的子类是怎么实现的:
ULONG STDMETHODCALLTYPE SDLAudioPlayer::AddRef() { return InterlockedIncrement(&m_cRef); } ULONG STDMETHODCALLTYPE SDLAudioPlayer::Release() { ULONG ulRef = InterlockedDecrement(&m_cRef); if (0 == ulRef) { delete this; } return ulRef; } HRESULT STDMETHODCALLTYPE SDLAudioPlayer::QueryInterface(REFIID riid, VOID **ppvInterface) { if (IID_IUnknown == riid) { AddRef(); *ppvInterface = (IUnknown*)this; } else if (__uuidof(IMMNotificationClient) == riid) { AddRef(); *ppvInterface = (IMMNotificationClient*)this; } else { *ppvInterface = NULL; return E_NOINTERFACE; } return S_OK; } HRESULT STDMETHODCALLTYPE SDLAudioPlayer::OnDeviceStateChanged(__in LPCWSTR pwstrDeviceId, __in DWORD dwNewState) { return 0; } HRESULT STDMETHODCALLTYPE SDLAudioPlayer::OnDeviceAdded(__in LPCWSTR pwstrDeviceId) { return 0; } HRESULT STDMETHODCALLTYPE SDLAudioPlayer::OnDeviceRemoved(__in LPCWSTR pwstrDeviceId) { return 0; } HRESULT STDMETHODCALLTYPE SDLAudioPlayer::OnDefaultDeviceChanged(__in EDataFlow flow, __in ERole role, __in LPCWSTR pwstrDefaultDeviceId) { return 0; } HRESULT STDMETHODCALLTYPE SDLAudioPlayer::OnPropertyValueChanged(__in LPCWSTR pwstrDeviceId, __in const PROPERTYKEY key) { return 0; }
有点晕吧,我的子类不得不重载了所有的纯虚函数,并提供一个空的函数体,而实际上我只需要处理 OnDefaultDeviceChanged函数,是不是有点蛋疼啊?!
是微软傻逼吗?应该不是,这后面可能有其它的设计考量:COM技术就是要保持接口的二进制兼容,COM所有的API都是通过纯虚接口来提供的,这样可以保证不同编程语言编写的代码客户互相调用。我没有仔细深究,猜测只有纯虚函数才能满足这样的需求,即使是提供一个空的函数体也会破坏二进制兼容性的。
对此,我想我们应该有个解决方案,不然如果我有2个子类要继承IMMNotificationClient接口,那我的2个子类中都要充满这样的空函数体,我觉得这会破坏代码的可读性,自己就变成傻逼了。因此我的解决方法是,提供一个MMNotificationClientBase的基础类,在这个基础类里面对所有的纯虚函数提供一个空函数体,代码如下:
class MMNotificationClientBase : public IMMNotificationClient { private: virtual HRESULT STDMETHODCALLTYPE OnDeviceStateChanged(__in LPCWSTR pwstrDeviceId, __in DWORD dwNewState){return 0;} virtual HRESULT STDMETHODCALLTYPE OnDeviceAdded(__in LPCWSTR pwstrDeviceId){return 0;} virtual HRESULT STDMETHODCALLTYPE OnDeviceRemoved(__in LPCWSTR pwstrDeviceId){return 0;} virtual HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged(__in EDataFlow flow, __in ERole role, __in LPCWSTR pwstrDefaultDeviceId){return 0;} virtual HRESULT STDMETHODCALLTYPE OnPropertyValueChanged(__in LPCWSTR pwstrDeviceId, __in const PROPERTYKEY key){return 0;} };
我的所有子类,需要响应事件时,都从 MMNotificationClientBase继承,而不是直接继承IMMNotificationClient。这样如果子类A只对OnDeviceStateChanged感兴趣的话,它就只重载OnDeviceStateChanged;如果子类B只对OnPropertyValueChanged感兴趣,那它就只重载OnPropertyValueChanged。而不是所有的子类都脑残一样重载所有的纯虚函数,并提供一个空的函数体。下面的截图就是我们实际项目中的样例代码: