简介
继承是面向对象编程的重要属性。有效地使用继承,可以复用现有的设计,加速产品开发进度。
公有继承塑造了is-a关系,如苹果is a水果,所以苹果可以以public方式继承水果,所有被改写的成员函数满足“不要求更多,也不承诺更少”的原则。
关于私有继承,它体现了is-implemented-in-terms-of关系,但这种关系又可以使用组合的方式来实现。
那么,私有继承与组合之间到底是什么关系?什么时候应该使用私有继承?什么时候又应该尽量使用组合以替代晦涩的私有继承?本文作一探讨。
从一个示例剖析
以下示例从一个模板类使用不同方式生成两个子类:
// base
template <class T>
class MyList
{
public:
bool Insert(const T&, size_t index);
T Access(size_t index) const;
size_t Size() const;
private:
T* buf_;
size_t bufsize_;
};
//继承方式
template <class T>
class MySet1 : private MyList<T>
{
public:
bool Add(const T&); // call Insert()
T Get(size_t index) const; // call Access()
using MyList<T>::Size;
//...
};
// 组合方式
template <class T>
class MySet2
{
public:
bool Add(const T&); // call impl_.Insert()
T Get(size_t index) const; // call impl_.Access()
size_t Size() const; // call impl_.Size()
//...
private:
MyList<T> impl_;
};
说明:
- MySet1和MySet2的功能完全相同,没有任何实际意义上的差别
- 私有继承表现is-implemented-in-terms-of(根据某物实作出)意义,使得子类可以使用基类的public/protected成份
- 组合表现has-a(有一个)意义,也连带有is-implemented-in-terms-of意义,它只能使用其他类的public成份
- 私有继承是单组合的一个超集,即,所有组成能完成的事情,私有继承也都能完成
- 私有继承时,子类只能拥有一份基类,如果需要该类的多个实体,只能使用组合
那么,什么时候需要使用私有继承?
- 当需要改写虚函数时,特别地,对于纯虚基类,只能继承
- 需要处理protected 成员时
- 当类之间的生命周期需要特别注意时,可能需要使用继承
- 需要分享某个共同的虚基类或者改写某个虚基类的构建程序时
- 基类是空基类时,即基类没有数据成员时,使用继承可以复用空基类的最佳化而获得空间优势
基于以上分析,示例中在构建MySet时没有理由需要使用继承。组合完美地完成了任务,并减少了类之间的耦合。
组合的优点:
- 允许使用多个其他类的实例
- 使得其他类作为一个成员使用,带来更多弹性
对MySet2稍作修改,可以得到一个更多弹性的版本:
template <class T, class Impl = MyList<T>>
class MySet3
{
public:
bool Add(const T&); // call impl_.Insert()
T Get(size_t index) const; // call impl_.Access()
size_t Size() const; // call impl_.Size()
//...
private:
Impl impl_;
};
有时候,可能需要私有继承与组合的灵活结合,以达到巧妙复用各自优点的效果。
根据如下基类:
class B
{
public:
virtual int Func1();
protected:
bool Func2();
private:
bool Func3(); // call Func1
};
需要改写虚函数Func1,或存取Func2,就需要使用继承。如下:
class D : private B
{
public:
int Func1();
//...
// maybe call B::Func2()
};
这份代码满足了要求,允许D改写Func1。
但是,它也允许所有D的成员取用Func2,并非所有成员都需要的。但私有继承还是把protected接口全部透露了出来。
很明显,私有继承是必要的,那么我们如何能够只导入真正需要的耦合呢?
增加一点代码,就可以做的更好。
// 私有继承
class DerivedImpl : private B
{
public:
int Func1();
//... need call Func2
};
// 组合
class Derived
{
// ... not use Func2
private:
DerivedImpl impl_;
};
看看,这样就良好地警卫并封装了对B的依赖。Derived只直接依赖B的public接口和DerivedImpl的public接口。
这也遵循了“一个类一个任务”的设计准则。
小结
继承往往被过度运用。在设计过程中,耦合关系要尽量减少。
如果类与类之间的关系可以有多种方式来表达,就使用其中关系最弱的一种。而继承几乎是最强烈的关系。
一般来说,可以参考以下:
- 如果组合可行,就不用考虑继承
- 如果私有继承ok,不要使用public继承
- 如果类之间可以使用多种关系,就使用耦合最弱的一种
- 避免使用多重继承
参考资料
《Exxceptional C++》