在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++观察者模式中的接口是否需要提供默认的实现的更多相关文章

  1. 从源码的角度看 React JS 中批量更新 State 的策略(下)

    这篇文章我们继续从源码的角度学习 React JS 中的批量更新 State 的策略,供我们继续深入学习研究 React 之用. 前置文章列表 深入理解 React JS 中的 setState 从源 ...

  2. 深度挖坑:从数据角度看人脸识别中Feature Normalization,Weight Normalization以及Triplet的作用

    深度挖坑:从数据角度看人脸识别中Feature Normalization,Weight Normalization以及Triplet的作用 周翼南 北京大学 工学硕士 373 人赞同了该文章 基于深 ...

  3. 从源码的角度看 React JS 中批量更新 State 的策略(上)

    在之前的文章「深入理解 React JS 中的 setState」与 「从源码的角度再看 React JS 中的 setState」 中,我们分别看到了 React JS 中 setState 的异步 ...

  4. 特征真的越多越好吗?从特征工程角度看“garbage in,garbage out”

    1. 从朴素贝叶斯在医疗诊断中的迷思说起 这个模型最早被应用于医疗诊断,其中,类变量的不同值用于表示患者可能患的不同疾病.证据变量用于表示不同症状.化验结果等.在简单的疾病诊断上,朴素贝叶斯模型确实发 ...

  5. 对博弈活动中蕴含的信息论原理的讨论,以及从熵角度看不同词素抽象方式在WEBSHELL文本检测中的效果区别

    1. 从赛马说起 0x1:赛马问题场景介绍 假设在一场赛马中有m匹马参赛,令第i匹参赛马获胜的概率为pi,如果第i匹马获胜,那么机会收益为oi比1,即在第i匹马上每投资一美元,如果赢了,会得到oi美元 ...

  6. SSD-Tensorflow 从工程角度进行配置

    目录 SSD-Tensorflow 工程角度配置 Download from the github 数据集转化tfrecords格式 训练模型(pre-train) 训练方案一 训练方案二 训练方案3 ...

  7. 从人类社会的角度看OO(独家视角)

    引言 在OO的工作中,我们一定会涉及到类,抽象类和接口.那么类和抽象类以及接口到底扮演的什么角色? 本文主要是从人类社会的角度阐述类与抽象类以及接口的"社会"关系,从而让我们抛弃书 ...

  8. 【阿里云产品公测】以开发者角度看ACE服务『ACE应用构建指南』

    作者:阿里云用户mr_wid ,z)NKt#   @I6A9do   如果感觉该评测对您有所帮助, 欢迎投票给本文: UO<claV   RsfTUb)<   投票标题:  28.[阿里云 ...

  9. [置顶] 从引爆点的角度看360随身wifi的发展

    从引爆点的角度看360随身wifi的发展 不到一个月的时间,随身wifi预定量就数百万.它的引爆点在哪里,为什么相同的产品这么多它却能火起来,通过对随身wifi的了解和我知识层面分析,主要是因为随身w ...

随机推荐

  1. 『NiFi 学习之路』使用 —— 主要组件的使用

    一.概述 大部分 NiFi 使用者都是通过 NiFi 的 Processor 来实现自己的业务的.因此,我也主要就 NiFi 官方提供的 Porcessor 进行介绍. 二.Processor 如果你 ...

  2. Spring-1-H Number Sequence(HDU 5014)解题报告及测试数据

    Number Sequence Time Limit:2000MS     Memory Limit:65536KB     64bit IO Format:%I64d & %I64u Pro ...

  3. IDEA 安装插件报错 Plugin Lombok Plugin was not installed: Cannot download

    报错 解决 成功下载插件 3

  4. DNS ARP地址解析原理

    ARP是地址解析协议 主机A与主机B之间如果要进行数据间的传输,需要获取对方的IP与物理地址(MAC),在只清楚ip的情况下,主机A向主机B请求链接,会先查找ARP高速缓存表,是否存在对应的主机B的i ...

  5. WCF可靠性会话之服务分流

    可靠性绘画答条件: (1)服务绑定支持可靠性绘画:特别是netTcpBinding.wsHttpBinding及wsDualHttpBinding. wsDualHtttpBinding可靠性绘画始终 ...

  6. web.xml servlet、servlet-mapping配置

    Servlet常称为服务器端小程序,即运行在服务器端的程序,用于处理及响应客户的请求. Servlet类是个特殊的java类,继承于HttpServlet. --------------------- ...

  7. 安装完kali需要做的一些事情

    1. 没有声音的问题[ kali ] 参考:http://tieba.baidu.com/p/4343219808 用pulseaudio --start会看到一些信息,提示类似root用户之类的 我 ...

  8. [BZOJ4552]排序

    Description 在2016年,佳媛姐姐喜欢上了数字序列.因而他经常研究关于序列的一些奇奇怪怪的问题,现在他在研究一个难题 ,需要你来帮助他.这个难题是这样子的:给出一个1到n的全排列,现在对这 ...

  9. java基础笔试题二(集合关系)

    知识点:java集合继承关系(Collection,Map) 1.集合框架体系图 2.java的集合层次 来自博客(http://blog.csdn.net/stubbornaccepted/arti ...

  10. 爬虫之JSON案例

    糗事百科实例: 爬取糗事百科段子,假设页面的URL是 http://www.qiushibaike.com/8hr/page/1 要求: 使用requests获取页面信息,用XPath / re 做数 ...