以前在設計DirectUI界面庫(該界面庫現已開源, 可到
這里 下載)架構時,遇到一個接口繼承相關的問題,當時沒有太好的解決方案,卻一直個耿耿于懷, 現在重新思考整理下。
我們的DirectUI控件層次大概如下:

其中, 類名以 I 開頭的都是接口:
IObject表示框架的基本接口, 要求實現類似COM里IUnknown的功能,
IControl表示控件的基本接口, 所有控件都從該接口繼承,
IControlContainer表示容器類控件的基本接口,
IButton表示Button類的基本接口,
IPanel表示某種容器控件接口。
當然上面的框架是簡化的情況,實際情況比上面的復雜的多, 但該圖已經可以幫我們說明這里的情況。
在真正實現Panel和Button時,我們會發現大量的代碼是重復和可以共用的,因此在實際實現時, 我們的框架可能會變成這樣:

也就是說我們會出現接口和實現交叉繼承的情況,實際上我自己在實現時就是用這種方法的, 我想大部分人都會用這種方法(實際上WPF也是用這種方法的)。
這種方法的缺點是顯而易見的, 接口中包含了實現,基本上讓接口失去了它應有的作用, 這在組件式編程中是致命的,比如本來在C++中我可以封裝成DLL,然后以類似COM的方式暴露接口給外部, 現在用這種方式卻沒法做到了(只能用導出類的方式)。
那么我們怎樣才能既基于接口編程, 又能在實現時實現代碼重用呢? 這個東西實際上是個語法糖, 即如何既符合C++語法又能實現我們這個需求。
于是,我們想到了如下的實現方式:

我們的這種實現方式基于C++模板, 總的來說就是把我們要實現的接口通過模板參數傳到繼承類體系的最底層, 該方式的代碼大概如下:
class IObject
{
};
class IControl: public IObject
{
};
class IButton: public IControl
{
};
template<typename TBase>
class CObjectImpl: public TBase
{
};
template<typename TBase>
class CControlImpl: public TBase
{
};
template<typename T, typename TBase>
class CButtonImpl: public TBase
{
};
class CButton: public CButtonImpl<CButton, IButton>
{
};
該方式基本上完全滿足我們上面的需求,既實現了代碼重用,又是基于接口編程,但是你有沒有發現它有一個致命的缺點, 這個缺點就是C++模板導致的代碼膨脹, 我們在
C++模板會使代碼膨脹嗎 對模板導致的代碼膨脹有相關分析。也就是說我們上面的設計會導致每種控件繼承類都有一份重復的代碼, 即
CControlImpl<IButton>和CControlImpl<IPanel>因為是不同的類實例, 因此它們會生成2分代碼。你可能會覺得這個不算什么, 但是想想控件的繼承類可能有好幾十甚至上百,最終的可執行文件會被撐大不少。
那么有沒有其他的方法來實現呢? 既能基于接口編程, 又能實現代碼重用,還沒有代碼膨脹的問題。
于是,我們想到了下面這種實現方式:

這種方式是最原始的方式, 實際上就是把接口體系單獨獨立出來, 把實現體系也單獨獨立出來, 然后在最終類(Button和Panel)里繼承組合起來。 當然這種方式也有缺點, 就是我們要多做些工作,因為我們要在最終類(Button)里實現接口(IButton), 在實現時我們要把所有接口需要實現的方法轉發給實現類(CButtonImpl)。
最后,總結下上面三種方法:
第一種實現和接口混合繼承的方法最簡單,也最容易理解, 缺點是沒法完全基于接口編程; 第二種基于模板的方法比較難理解,實現上也比較簡單, 缺點是代碼膨脹; 第三種多重繼承的方法也比較容易理解, 缺點是我們要多做一些工作。
我暫時就想到這些方法, 不知道其他朋友對上面的問題一般是怎么解決的, 有什么好的解決方法?
下面這種方案是評論中的朋友提到而新增的:

上面這種方案相對于我們最后一種解決方案,可能更通用,缺點是繼承體系比較復雜, 會出現菱形繼承,只能用虛擬繼承。
注: 再深入一點,我們會發現上面的接口和實現其實是接口和抽象類的差別,IObject和IControl是接口, CObjectImpl和CControlImpl是抽象類, 接口強調暴露給外部的行為, 而抽象類是沒法直接實例化的抽象實現。
看到云風也遇到過類似的問題:
C++ 中的接口繼承與實現繼承
posted on 2013-02-08 19:40
Richard Wei 閱讀(2549)
評論(5) 編輯 收藏 引用 所屬分類:
架構體系