C++类和接口的设计原则探讨
副标题#e#
我这篇文章的主旨是先容一部门类和接口的高质量设计的准则。这些准则不单应该担保设计而且实现的类可能接口自己有高质量代码,并且更重要的是在家产规模应该尽大概的使代码的更新和维护不影响客户的勾当,主要也就是保持二进制代码兼容(binary compatibility)和源代码兼容(source compatibility)。我但愿这些准则能辅佐刚从学校进入家产规模的伴侣尽快适应更高尺度的编程要求,尽快晋升本身的设计本领。
文中以C++类的设计为接头范畴。
总提
面向工具编程对付产出高质量,易维护的代码长短常有辅佐的。面向工具编程的观念构建于三个根基特征之上:封装,担任,多态。在C++中,class是面向工具编程观念的焦点和详细形式。class通过私有成员浮现“封装”,通过直接担任可能组合浮现“担任”,通过虚函数和动态绑定(dynamic binding)浮现“多态”。class的设计质量直接抉择了整个系统的质量。
从整体成果层面谈class设计,有这么三条原则:
·单一成果原则(Single Responsibility Principle)
一个class就其整体应该只提供单一的处事。假如一个class提供多样的处事,那么就应该把它拆分,反之,假如一个在观念上单一的成果却由几个class认真,这几个class应该归并。
·开放/关闭原则(Open/Close Principle)
一个设计并实现好的class,应该对扩充的行动开放,而对修改的行动关闭。也就是说,这个class应该是答允扩充的,但不答允修改。假如需要成果上的扩充,一般来说应该通过添加新类实现,而不是修改原类的代码。添加新类不光可以通过直接担任,也可以通过组合。
·最小惊奇道理(Least Surprise Principle)
#p#副标题#e#
在重载函数,可能子类实现父类虚函数时,应该根基维持函数本来所期望的成果。好比:
class Pet {
public:
virtual Talk() = 0;
};
class Cat : public Pet {
public:
void Talk() { cout << "miao"; }
};
class Dog : public Pet {
public:
void Talk() { BiteOwner(); }
};
class Dog 在实现虚函数Talk的时候,没有像我们期望的那样输出狗吠声,而是咬起主人来了。这是应该制止的。
接口和实现
在系统中,调查一个class有两个角度,从外部可能用户角度我们看到的是接口,从内部我们看到的是实现。因为系统必定要不绝修改,因此实现免不了不断的变革,可是接口又被要求只管保持不变。这两者的抵牾必需通过精采的设计只管制止,根基原则就是将实现细节与接口断绝。下面列出几条较量详细点的:
·接口的设计保持最小而完整
精简接口函数个数,使每一个函数有代表性,函数成果刚好包围class的职能。一个最小的接口可以使维护简朴,增加潜在的代码重用性,淘汰客户的疑惑,而且也可以缩小头文件长度和编译时间。当改造函数时,应该用雷同函数名实现改造而保存原函数,代码注释里应该有相应的说明。可以增加新函数,但不能删除旧函数。
·成员变量应该都为私有
显而易见,public变量粉碎封装性以及接口和实现的疏散;protected变量也大概使客户编写担任类而依赖于父类的实现细节。
·制止函数返回成员变量的指针或引用
这么做也会使客户代码依赖于实现细节。
·思量是否禁用编译器缺省发生的函数
这些函数包罗:复制结构函数,赋值操纵符(operator =)。假如我们不规划界说本身的版本而不禁用默认版本的话,大概使客户代码在不留意的环境下挪用这些函数。当实现产生窜改时就大概引起问题,好比class多了一个heap memory指针。假如我们答允工具拷贝,较量稳妥的要领是禁用它们,而界说一个专门的clone()函数。
兼容性(compatibility)
不消说,兼容性长短常重要的。Intel和Microsoft之所以如此乐成,个中一个重要方面就是他们的产物,不管是硬件照旧软件,都做到了很好的兼容老产物。代码的兼容也是如此。不可思议,假如客户依赖于你的library产物,而要因为你的产物的更新而不绝的重写他的代码,他还会继承用你的产物。
代码兼容可以简朴分为二进制兼容和源代码兼容。二进制兼容也就是说,客户的已编译代码可以在不消从头编译的环境下,直接利用你的差异版本的已编译代码。源代码兼容就是,假如你的代码更新了,客户的代码不需要修改,只需要从头编译就可正常运行。在C++中,接口一般是由头文件和library二进制代码提供,因此,任何大概造成library代码和旧的头文件纷歧致的环境都大概粉碎二进制兼容,因为客户代码必需和新的头文件从头编译一次。
因此,遵循几条准则可以使你更轻松地办理兼容性问题:
·不改变类的巨细可能改酿成员变量的顺序
#p#分页标题#e#
包罗几个方面:不增加或淘汰成员变量;不修改成员变量范例;不改酿成员变量的声明顺序;不改变虚函数的有无。显而易见,增加或淘汰成员变量会改变类的巨细,而且需要更新头文件,从而大概造成与客户代码不兼容。范例的变革也大概引起类的巨细的变革。成员变量的会见一般是由编译器按偏移量确定,顺序假如改变,偏移量也就会改变,粉碎了二进制兼容。至于虚函数的有无,抉择是否存在虚函数表指针,也就影响了类的巨细和成员变量的顺序。
·不利用inline函数
inline函数声明于头文件中,而且被编译于客户代码中,假如inline函数会见了private成员,该成员又改变了顺序,那么inline函数虚要被从头编译,粉碎了二进制兼容。
·接口函数不利用虚函数
虚函数的会见和成员变量雷同,是通过虚函数表中的偏移。虚函数顺序的改变会影响偏移。因此,在条件答允时,应该制止利用public虚函数。好比:
class Picture {
public:
virtual void Draw();
};
应该改为
class Picture {
public:
void Draw();
private:
virtual void DoDraw();
};
void Picture::Draw()
{
DoDraw();
}
·不改变接口函数的顺序
在许多嵌入式系统中,链接库通过输出函数表(exported function table)袒露接口以节减空间。此时,对接口函数的会见也是通过索引值举办,因此改变顺序也会粉碎兼容性。
·制止利用函数缺省参数
给函数形参设定缺省值可以利便客户,可是大概粉碎兼容。缺省值随头文件给出,缺省值的改变也就会引起兼容问题。
以上就是我能想到的了,但愿能对各人有辅佐。