公司里项目是一个二次开发项目,原先招聘的程序员新人比较多。因为原有代码量比较大且杂乱,未能有人深入理解原代码,在找出了一些重点性能瓶颈后,确定进行新的代码编写。

此时却出现了问题,原先的程序只敢进行修改,完全不敢从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()函数同样无法正确定位析构顺序。

所以还是麻烦点,手动顺序析构吧,安全很多。