从工程角度看C++观察者模式中的接口是否需要提供默认的实现

在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。而不是所有的子类都脑残一样重载所有的纯虚函数,并提供一个空的函数体。下面的截图就是我们实际项目中的样例代码:

原文地址:https://www.cnblogs.com/swnuwangyun/p/7845404.html