从工程角度看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 ...
随机推荐
- Lua4.0中getn陷阱
Lua4.0中getn潜规则 • 使用t[n] = nil方式删除元素会导致意外结果 t = {, , , , , , , , , }; t[] = nil; t = {, , , , , , , , ...
- PHP实现excel导出
首先去下载PHPExcel类,地址http://phpexcel.codeplex.com/ 方法如下第一步引入这个扩展类 Vendor('PHPExcel'); 第二部就是方法了,下面简单的实现方法 ...
- C++匿名名字空间
转自:http://blog.csdn.net/eric_arrow/article/details/8978905 名字空间(namespace),是C++提供的一个解决符合名字冲突的特性.标准规定 ...
- 2017 Multi-University Training Contest - Team 4 hdu6071 Lazy Running
地址:http://acm.split.hdu.edu.cn/showproblem.php?pid=6071 题目: Lazy Running Time Limit: 2000/1000 MS (J ...
- Submine Text 快捷键
Ctrl+Shift+P:打开命令面板 Ctrl+P:搜索项目中的文件 Ctrl+G:跳转到第几行 Ctrl+W:关闭当前打开文件 Ctrl+Shift+W:关闭所有打开文件 Ctrl+Shift+V ...
- TOSCA自动化测试工具ppt(正在整理)
1. 认识TOSCA 安装使用 2. TOSCA自动化测试工具的优点 1). 可扩展 Tosca Commander™ functionalities can be extended by us ...
- 带你走进ajax(2)
ajax原理介绍 传统的web应用 传统的web应用是客户端向服务器发送一个http请求后,客户端要一直等待服务器的响应,这时用户什么事也干不成. 最麻烦的是对于表单的提交.比如用户要填写用户信息,等 ...
- ultraedit 查看文件
转自:https://wenda.so.com/q/1481655902726192 1 UltraEdit在打开文件的时候,会对文件类型进行检查.如果是二进制文件,会自动转为16进制显示模式.如下图 ...
- PHP jsonencode 已经json中包含 汉字的处理
<?php $arr = array ( 'Name'=>'希亚', ); $jsonencode = json_encode($arr); echo $jsonencode; ?> ...
- 20145322何志威 《Java程序设计》课程总结
课程总结 每周读书笔记链接汇总 •第一周读书笔记 •第二周读书笔记 •第三周读书笔记 •第四周读书笔记 •第五周读书笔记 •第六周读书笔记 •第七周读书笔记 •第八周读书笔记 •第九周读书笔记 •第十 ...