一些编程中的小技巧(纯虚接口,工厂,单件)
公司里项目是一个二次开发项目,原先招聘的程序员新人比较多。因为原有代码量比较大且杂乱,未能有人深入理解原代码,在找出了一些重点性能瓶颈后,确定进行新的代码编写。
此时却出现了问题,原先的程序只敢进行修改,完全不敢从0编写。由于是基于之前的一套代码二次开发的,各模块之间的关联已经拥有,所以可以允许各自为政的进行串行开发,但重新进行模块编写后,是需要多个程序进行并行开发的,结果再次令人尴尬,缺乏经验,于是整理出下文,以后再有类似人员直接给文档进行说明即可。(适用人群范围:纯新手。。。)
Author:FreeKnight
1:使用纯虚接口。
无论内部实现如何,先按照策划案或者功能需求说明书设计好类和核心函数。其中核心函数使用纯虚函数标示。这种设计就是要求以行为为中心的思路,所以在编程规范中我有提到,要以行为为重心,不要以数据为中心。
例如,一个音频模块可以这么去做。
class IAudioMgr
{
public:
virtual ~IAudioMgr(){};
virtual bool PlaySound( Handle p_hSound ) = 0; // 注:Handle不是Win的HANDLE,是一种弱引用。
virtual bool StopSound( Handle p_hSound ) = 0;
virtual void RegistStopCallback( LPFunc p_pCallbackFunc ) = 0;
}
其中我们根本不需要去管内部是如何的实现,先设计出整个音频模块所需要遵循的设计规则,然后再去实现它。同时,负责其他模块的人员若涉及了该模块,也是无需理解里面实现方式的,只要知道接口函数有哪些,就可以进行编码了。大幅度的降低了模块的耦合度,提高了并行可能。
当然,进行该模块编码的程序,强制需要实现内部的全部功能。
例如:
class COpenALAudioMgr : public IAudioMgr { ….// 所有函数都必须继承实现。 }
同样,若使用DirectSound也可继承实现
class CDSoundAudioMgr : public IAudioMgr { ….// 所有函数都必须继承实现。 }
调用时,只需要这样写。
ISoundMgr* pMgr = new COpenALAudioMgr ();
if( pMgr == NULL )
{
pMgr = new CDSoundAudioMgr ();
}
if( pMgr != NULL )
{
pMgr->PlaySound( ... );
}
这样,我们可以看出可以很容易的创建出一个音频模块的新的实现。而且它隐藏了实现细节,同时,允许了在运行两个类间的互相切换。
我们在上面也看到了,虽然隐藏了子类实现细节,但是子类类型依然对外暴露,最土的说法,就是“用户”需要包含COpenALAudioMgr 的头文件,有时候我们甚至连COpenALAudioMgr ,CDSoundAudioMgr 这里的内部函数声明都不希望让人知道,或者不希望让人关心。有时候,我们要防止“用户”知道了底层类型后,很“聪明”的进行类型转换,试图获得更多的行为,这样将丧失了抽象接口的意义。
于是,推荐使用工厂。
例如:
class CAudioMgrFactory
{
public:
enum ENUM_AudioMgrType{ DSound = 1, OpenAL = 2, };
static IAudioMgr* CreateAudioMgr( ENUM_AudioMgrType p_eType );
}
根据类型new出不同的子类对象就可以了。这样,使用者甚至不需要知道具体的子类类型就可以进行调用了。
一般来说,工厂类都是单件设计,单件设计原理和方式都很简单,但是,它有三个问题:
1:多线程使用。
2:构造位置不明确。
3:析构顺序不明确。
其中“构造位置不明确”这点很容易解决,可以在main初始化时候,直接调用 CClass::GetSingleton() ; 强制初始化。虽然浪费些内存,却可以更安全的控制构造顺序。
而多线程使用时,除了注意加锁以外,还要加引用计数。但是,强烈建议避免使用智能指针。因为一旦使用智能指针,则完全无法控制该单件的生命周期和析构时序了。
有些人喜欢在new单件对象的时候直接在atexit()注册析构。也不推荐,atexit()函数同样无法正确定位析构顺序。
所以还是麻烦点,手动顺序析构吧,安全很多。