惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

MyScale Blog
MyScale Blog
C
CXSECURITY Database RSS Feed - CXSecurity.com
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
阮一峰的网络日志
阮一峰的网络日志
罗磊的独立博客
博客园 - 叶小钗
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
美团技术团队
酷 壳 – CoolShell
酷 壳 – CoolShell
雷峰网
雷峰网
宝玉的分享
宝玉的分享
大猫的无限游戏
大猫的无限游戏
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Last Week in AI
Last Week in AI
爱范儿
爱范儿
小众软件
小众软件
K
Kaspersky official blog
P
Proofpoint News Feed
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - Franky
V
Vulnerabilities – Threatpost
博客园_首页
Microsoft Security Blog
Microsoft Security Blog
C
Cybersecurity and Infrastructure Security Agency CISA
V
V2EX
C
Check Point Blog
S
Schneier on Security
P
Palo Alto Networks Blog
IT之家
IT之家
GbyAI
GbyAI
T
Threat Research - Cisco Blogs
Hugging Face - Blog
Hugging Face - Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Apple Machine Learning Research
Apple Machine Learning Research
C
Cyber Attacks, Cyber Crime and Cyber Security
T
Tailwind CSS Blog
Project Zero
Project Zero
Y
Y Combinator Blog
V
Visual Studio Blog
Simon Willison's Weblog
Simon Willison's Weblog
T
Threatpost
Scott Helme
Scott Helme
L
LINUX DO - 热门话题
S
Securelist
C
CERT Recently Published Vulnerability Notes
A
Arctic Wolf
M
MIT News - Artificial intelligence
人人都是产品经理
人人都是产品经理

博客园 - woaiusd

claude各种客户端的关系 pytorch对大模型推理,实现简单的对话 Agent原理与模拟 为红米Note 5 Pro编译Lineage OS 15.1的各种坑 C语言调用DIRECT3D的实例代码,通过lpVtbl字段进行 OBS插件学习入门:一个非常简单的、调节音量的filter Windows音频SDK的发展历程 闲话__stdcall, __cdecl, __fastcall出现的历史背景以及各自解决的问题 为什么需要超出48K的音频采样率,以及PCM到DSD的演进 wireshark使用笔记 managed_shared_memory.construct造成的性能损失 java.lang.NullPointerException Ubuntu中QT使用FFmpeg的奇怪问题 QT开启C++11的支持 尝鲜CodeBlocks ops中set_sysclk set_clkdiv set_pll详解 在内核中异步请求设备固件firmware的测试代码 探究MaxxBass音效 Alsa驱动snd_soc_read的底层实现 Device Drivers Should Not Do Power Management 探究platform_driver中“多态”思想 - woaiusd 探究platform_driver中的shutdown用途
从工程角度看C++观察者模式中的接口是否需要提供默认的实现
woaiusd · 2017-11-16 · via 博客园 - woaiusd

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