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

从工程角度看C++观察者模式中的接口是否需要提供默认的实现的更多相关文章
- 从源码的角度看 React JS 中批量更新 State 的策略(下)
这篇文章我们继续从源码的角度学习 React JS 中的批量更新 State 的策略,供我们继续深入学习研究 React 之用. 前置文章列表 深入理解 React JS 中的 setState 从源 ...
- 深度挖坑:从数据角度看人脸识别中Feature Normalization,Weight Normalization以及Triplet的作用
深度挖坑:从数据角度看人脸识别中Feature Normalization,Weight Normalization以及Triplet的作用 周翼南 北京大学 工学硕士 373 人赞同了该文章 基于深 ...
- 从源码的角度看 React JS 中批量更新 State 的策略(上)
在之前的文章「深入理解 React JS 中的 setState」与 「从源码的角度再看 React JS 中的 setState」 中,我们分别看到了 React JS 中 setState 的异步 ...
- 特征真的越多越好吗?从特征工程角度看“garbage in,garbage out”
1. 从朴素贝叶斯在医疗诊断中的迷思说起 这个模型最早被应用于医疗诊断,其中,类变量的不同值用于表示患者可能患的不同疾病.证据变量用于表示不同症状.化验结果等.在简单的疾病诊断上,朴素贝叶斯模型确实发 ...
- 对博弈活动中蕴含的信息论原理的讨论,以及从熵角度看不同词素抽象方式在WEBSHELL文本检测中的效果区别
1. 从赛马说起 0x1:赛马问题场景介绍 假设在一场赛马中有m匹马参赛,令第i匹参赛马获胜的概率为pi,如果第i匹马获胜,那么机会收益为oi比1,即在第i匹马上每投资一美元,如果赢了,会得到oi美元 ...
- SSD-Tensorflow 从工程角度进行配置
目录 SSD-Tensorflow 工程角度配置 Download from the github 数据集转化tfrecords格式 训练模型(pre-train) 训练方案一 训练方案二 训练方案3 ...
- 从人类社会的角度看OO(独家视角)
引言 在OO的工作中,我们一定会涉及到类,抽象类和接口.那么类和抽象类以及接口到底扮演的什么角色? 本文主要是从人类社会的角度阐述类与抽象类以及接口的"社会"关系,从而让我们抛弃书 ...
- 【阿里云产品公测】以开发者角度看ACE服务『ACE应用构建指南』
作者:阿里云用户mr_wid ,z)NKt# @I6A9do 如果感觉该评测对您有所帮助, 欢迎投票给本文: UO<claV RsfTUb)< 投票标题: 28.[阿里云 ...
- [置顶] 从引爆点的角度看360随身wifi的发展
从引爆点的角度看360随身wifi的发展 不到一个月的时间,随身wifi预定量就数百万.它的引爆点在哪里,为什么相同的产品这么多它却能火起来,通过对随身wifi的了解和我知识层面分析,主要是因为随身w ...
随机推荐
- string与CString对比
string是标准C++库中的字符串类,CString是在Windows开发环境下常用的字符串类,CString目前已从MFC中分离出来可以单独使用,只需包含atlstr.h即可. 相比string, ...
- 图层的使用要点(CALayer)
A,图层和路径 基本图层 CALayer 动画的主角 形状图层 CAShapeLayer 绘制不规则图形 渐变图层 CAGradientLayer 颜色渐变.阴影 复制图层 CAReplicatorL ...
- Java设计原则—接口隔离原则(转)
接口隔离原则 Interface Segregation Principle 定义: 客户端不应该依赖它不需要的接口 类间的依赖关系应该建立在最小的接口上 我们可以把这两个定义概括为一句话:建立 ...
- Bar Mitzvah Attack分析
结合RC4算法实现,简单分析一下Bar Mitzvah Attack攻击的原理,并以WEP破解过程来举例说明通用的破解方式.(个人觉得RC4早已经过时,要想避免该攻击,应该采用更好的加密算法,如:cc ...
- 浅谈WLAN干扰与抗干扰技术
一. 无线干扰的分类和来源 无线干扰按照类型可划分为WLAN干扰和非WLAN干扰.WLAN干扰是指干扰源发送的RF信号也符合802.11标准,除此之外都是非WLAN干扰.对WLAN干扰,可进一步按照频 ...
- linux下多线程之pthread_detach(pthread_self())
写个碰到的问题,记录下自己的技术之路点滴pthread_detach(pthread_self())linux线程执行和windows不同,pthread有两种状态joinable状态和unjoina ...
- Web.xml中自动扫描Spring的配置文件及resource时classpath*:与classpath:的区别
Web.xml中自动扫描Spring的配置文件及resource时classpath*:与classpath:的区别 一.Web.xml中自动扫描Spring的配置文件(applicationCont ...
- 20145310《Java程序设计》第4次实验报告
20145310<Java程序设计>第4次实验报告 实验内容 搭建Android环境 运行Android 修改代码并输出自己的学号 实验步骤 搭建Android环境 安装Android S ...
- 20145313张雪纯 《Java程序设计》第1周学习总结
20145313 <Java程序设计>第1周学习总结 教材学习内容总结 java有三大平台,分别为Java SE(J2SE).Java EE(J2EE).Java ME(J2 ME). J ...
- Quick Search FAQ
Q: Why does it jump to an incorrect page? A: Some categories, such as twitter, may need to log in in ...