簡介
繼承是面向對象編程的重要屬性。有效地使用繼承,可以複用現有的設計,加速產品開發進度。
公有繼承塑造了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++》