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

  • 相关阅读:
    Win下循环进入目录启动执行某任务
    Mysql数据库搭建-Windows
    Linux的服务器初始优化脚本。
    Linux下Find命令的使用
    一些判断Linux是否被黑的经验
    搭建docker私有仓库
    进程退出:SIGINT、SIGTERM和SIGKILL区别
    dockerfile使用
    k8s-ingress安装
    k8s-service
  • 原文地址:https://www.cnblogs.com/swnuwangyun/p/7845404.html
Copyright © 2020-2023  润新知