《Imperfect C++》中展示了一種叫“螺栓”的技術,然而,這本書中的討論並不足夠深入。當然,我也相信Matthew是故意的,從而讓我們這些“三道販子”(Matthew自稱是二道販子)也能夠獲得一點點成就感。
考慮這樣一個接口設計:
struct IRefCount;
struct IReader : public IRefCount;
在Reader中實現接口:
<!--[if !supportEmptyParas]--> class Reader : public IReader;
在上述的繼承結構中,IRefCount是一個結構性的類,用來實現引用計數,實際上它和領域邏輯部分IReader沒有什麼關系。我們打算在IRefCount的基礎上,建立了一套工具來管理對象生命周期和幫助實現異常安全的代碼 (例如,smart pointer) 。現在來考慮Reader的實現,Reader除了需要實現IReader的接口,還必須實現IRefCount的接口。這一切看起來似乎順理成章,讓我們繼續看下面的設計<!--[if !supportEmptyParas]-->:
struct IWriter : public IRefCount;
<!--[if !supportEmptyParas]--> class Writer : public IWriter;
現在來考慮Writer的實現,和Reader一樣,Writer除了要實現IWriter的接口外,同時還需要實現IRefCount的接口。現在,我們來看看IRefCount是如何定義的:
struct IRefCount {
virtual void add() = 0;
virtual void release() = 0;
virtual int count() const = 0;
virtual void dispose() = 0;
virtual ~IRefCount(){}
};
在Reader中的IRefCount的實現:
virtual void add() { ++m_ref_count;}
virtual void release() {--m_ref_count;}
virtual int count() const{return m_ref_count;}
virtual void dispose() { delete this;}
…
int m_ref_count;
同樣,在Writer的實現中,也包含了一模一樣的代碼,這違背了DRY原則(Don’t Repeat Yourself)。況且,隨著系統中的類增加,大家都意識到,需要將這部分代碼復用。一個能夠工作的做法是把IRefCount的實現代碼直接放到IRefCount中去實現,通過繼承,派生類就不必再次實現IRefCount了。我們來看一下dispose的實現:
virtual void dispose() { delete this;}
這裡,采用了delete來銷毀對象,這就意味著Reader必須在堆上分配,才可能透過IRefCount正確管理對象的生命周期,沒關系,我們還可以override dispose方法,在Reader如下實現dispose:
virtual void dispose() { }
但是,這樣又帶來一個問題,Reader不能被分配在堆上了!如果你夠狠,當然,你也可以這麼解決問題:
class HeapReader : IReader;
class StackReader : HeapReader{ virtual void dispose() { } };
問題是,StackReader 是一個HeapReader嗎?為了代碼復用,我們完全不管什麼概念了。當然,如果你和我一樣,看重維護概念,那麼這麼實現吧:
class HeapReader : IReader;
class StackReader : IReader;
這樣一來,IReader的實現將被重復,又違背了DRY原則,等著被將來維護的工程師詛咒吧!或許,那個維護工程師就是3個月後的你自己。如果這樣真的能夠解決問題,那麼也還是可以接受的,很快,我們有了一個新的接口:
struct IRWiter : IReader, IWriter;
class RWiter : public IRWiter;
考慮一下IRefCount的語義:它用來記錄對所在對象的引用計數。很顯然,我從IReader和IWriter中的任意一個分支獲得的IRefCount應該都是獲得一樣的引用計數效果。但是現在,這個繼承樹存在兩個IRefCount的實例,我們不得不在RWiter當中重新重載一遍。這樣,從IReader和IWriter繼承來的兩個實例就作廢了,而且,我們可能還浪費了8個字節。為了解決這個問題,我們還可以在另一條危險的道路上繼續前進,那就是虛擬繼承:
struct IReader : virtual public IRefCount;
struct IWriter : virtual public IRefCount;
還記得大師們給予的忠告嗎--“不要在虛基類中存放數據成員”。“這樣有什麼問題嗎,我們不必對大師盲目崇拜”,你一定也聽過這樣的建議。如果大師們不能說服這些人,那麼我也不能。於是,我們進一步在所有的接口中提供默認實現,包括IReader和IWriter.
現在的問題是:
struct IRWiter : IReader, IWriter;
還是
struct IRWiter : virtual IReader, virtual IWriter ?
如果你沒有選擇virtual,那麼IRWiter被派生後,那麼派生類的繼承樹中可能存在多個IReader實現,如果這個派生類要求只能提供一份IReader的語義怎麼辦?除了重新實現接口還能怎樣?反過來,如果我們選擇了virtual繼承,那麼派生類需要多個實現怎麼辦?真是個麻煩事。“這是典型的過度設計,我們為什麼要考慮這麼多?”你可以這麼說,但事實上,即使是一個數百文件的小型系統,也完全可能迫使你作出選擇。雖然,我們仍然有辦法作出挽救措施,但是也只是苟延殘喘而已。正如我前面所說,這是一個危險的道路,聰明如你,是斷然不會讓自己陷入這樣的泥潭的。
讓我們離開虛擬繼承,先回到重復代碼的問題上來。有沒有更好的解決辦法呢?還好,在C++的世界裡,我們有神奇的template,讓我們來消除重復的代碼:
template<typename Base>
class ImpReader : public Base{
constraint(is_base_derive(IReader, Base))
Implementation IReader
<!--[if !supportEmptyParas]--> };
class HeapReader : ImpReader<IReader>{};
class StackReader : ImpReader <IReader>{
virtual void dispose() {};
<!--[if !supportEmptyParas]--> };
請注意,我們還是假設IRefCount已經提供了一個默認實現。現在,情況好了很多,所有的代碼都只有一份,而且,概念也沒有被破壞。假設,Writer也同樣需要類似的能力,那麼,我們又多了StackWriter和HeapWriter.事實上,真的有人用到了StackWriter嗎?我不知道,只是,提供了StackReader,沒有理由不提供StackWriter啊。讓我們繼續。
現在,我們發現,需要改進內存分配的性能問題,於是,我們希望通過內存池來分配對象,相應的dispose也需要修改:
virtual void dispose(){ distory(this);}
於是,我們又多出兩個類,PoolReader和PoolWriter。這真是糟糕,組合爆炸可不是什麼好兆頭。
從我們前述的變化來看,都是IRefCount在變化,為什麼不把這種變化分離出來呢?不必為IRefCount提供默認實現,借鑒ImpReader的手法:
template<typename Base>
class ImpHeapRefCount : public Base{
constraint(is_base_derive(IRefCount, Base));
..};
類似的:
template<typename Base> class ImpStackRefCount : public Base;
<!--[if !supportEmptyParas]--> template<typename Base> class ImpPoolRefCount : public Base; <!--[endif]-->
再看看,我們如何實現所有的Reader.
typedef ImpReader< ImpHeapRefCount<IReader> > HeapReader;
typedef ImpReader< ImpStackRefCount<IReader> > StackReader;
typedef ImpReader< ImpPoolRefCount<IReader> > PoolReader;
以HeapReader為例,實際的繼承關系是這樣的:
ImpReaderàImpHeapRefCountàIReaderàIRefCount;
對於Writer,我們完全可以采取同樣的手法來實現。對於上述的typedef可以預先定義,也完全可以不定義,交給最終用戶去組裝吧。現在,類的設計者再也不必為選擇實現而痛苦了,你只要提供不同的磚頭,客戶程序員可以輕而易舉的建立起大廈。還有比這更讓一個設計師幸福的嗎?
繼續深入,考察ImpHeapRefCount和ImpStackRefCount的實現,我們提到,dispose方法的實現是不一樣的,但是,其他部分:add,releasee和count的實現完全可以相同。然而我們現在又分別實現了一遍,為了不違背DRY原則,我們如下處理:
template<typename Base>
class ImpPartialRefCount : public Base{
//實現add, release和count.
};
template<typename Base>
class ImpHeapRefCount : public Base{
virtual void dispose() { delete this;}
};
template<typename Base>
class ImpStackRefCount : public Base{
virtual void dispose() { }
};
然後,我們可以這樣定義Reader:
typedef ImpReader<ImpHeapRefCount<ImpPartialRefCount<Ireader> > > HeapReader;
請注意,我們在這裡展示了一種能力,不必在一個實現當中完整的實現整個接口,可以把一個接口的實現分拆到多個實現當中。這個能力是非凡的,借助於此,我們可以提供更小粒度的實現單位,給最終用戶來組裝。具體拆分到什麼樣的程度完全取決於代碼復用的需求,以及概念維護的需要。
我們提供了高度的復用能力,同時避免了繼承帶來的強耦合,以及對推遲設計決策的支持,這些能力對於軟件設計師而言,正如Matthew在《Imperfect C++》中所說的,這簡直就是現實中的烏托邦!
現在我們把這種手法首先針對單繼承做一個小結。對於任意的接口IInterface,我們提供如下的實現:
template<typename Base>
class ImpInterface : public Base{
constraint(is_base_derive(IInterface, Base));
};
請注意,一個接口可以有任意多個實現,並且可以是任意的部分實現。<!--[if !supportEmptyParas]--> <!--[endif]-->
假設我們有如下接口繼承樹:
InterfaceN àInterfaceN_1àInterfaceN_2à…àInterface0
並且提供了實現類ImpInterface0 ~ ImpInterfaceN.
那麼,InterfaceN的實例類型就是:
typedef ImpInterfaceN<
ImpInterfaceN_1<
ImpInterfaceN_2<
…
ImpInterface0<InterfaceN> …> > > ConcreteClassN;
我們注意到,定義ConcreteClassN的時候,我們的ImpInterface是按照順序來的,我認為這是合適的做法。當然了,最後組裝的權力已經交給客戶了,客戶愛怎麼組裝就怎麼組裝吧。然而我們還是有一些需要注意的問題。
1.假定,我需要在ImpInterfaceI中引用基類的方法,記住,不要使用這樣的手法:
ImpInterfaceI_K::SomeMethod();
這樣調用不具有多態性,而應該這樣:
this-> SomeMethod();
2.不要在自己的ImpInterfaceI實現中覆蓋基類接口的其他已經實現的方法,如果你一定要這麼做,那麼務必在文檔中說明,因為在組裝的時候,順序將是極其關鍵的了。
3.這個方法和設計模式中的Template Pattern目的是不一樣的。Template Pattern是在基類中定義了一個算法,讓派生類定制算法的某些步驟。這裡的方法針對的是接口模型的概念,提供接口和實現分離的技術。
關於第二條,應該盡量避免發生。這裡說的覆蓋是指基類實現已經實現了該方法,而後繼實現又覆蓋該方法。基類實現可以是一個部分實現,對於沒有實現的那些方法,在派生接口的實現類中實現則是常見的。一方面,我們盡量合理分解層次之間的功能,另一個方面,可以通過定制實現模板類,來保證順序。盡可能的讓語言本身來保證正確性,而不是依賴文檔。我們可以像這樣預先裝配一些東西:
template<typename Base>
class SomeComponent : public ImpPartA < ImpPartB <Base> >{};
可惜,C++暫時還不支持模板的不完全typedef,否則,我們還可以如下定以:
template<typename Base>
typedef ImpPartA< ImpPartB<Base> > SomeComponent;
不過,C0x很可能會支持類似的語法。這樣,我們使用SomeComponent來作為一個預制品,來保證一些安全性,而不必完全依賴文檔了。 <!--[endif]-->
看看ConcreteClassN的定義,也許你和我一樣,並不喜歡這種嵌套的、遞歸的定義方式,太難看了。讓世界稍微美好一點吧!於是我們提供一個輔助類:
<!--[if !supportEmptyParas]--> template<typename T>struct Empty{};
template<typename I, typename template<class> class B>
struct Merge{ <!--[if !supportEmptyParas]-->typedef B<I> type;};
template<typename I >
struct Merge<I, Empty >{
typedef I type;
<!--[if !supportEmptyParas]--> }; <!--[endif]-->
template
<
typename I,
typename template<class> class B1,
typename template<class> class B2 = Empty,
…
typename template<class> class Bn = Empty,
>
struct Reform{
typedef typename Merge<
typename Merge<
typename Merge<I, B1>::type
, B2>::type , …,Bn>::type type;
};
現在,我們可以這樣定義ConcreteClassN了:
Typedef Reform<InterfaceN, ImpInterface0, ImpInterface1,
…ImpInterfaceN>::type ConcreteClassN;
是不是清爽了很多?
在繼續下面內容以前,請回味一下這個不是問題的問題:
假設IReader有3種實現,IRefCount有3種實現,我們將如何漂亮地解決掉他們。 <!--[endif]-->
現實世界總是要復雜得多,讓我們進入真實的世界。回顧這個接口:
struct IRWiter : IReader, IWriter;
假設我們確實需要IReader, IWriter,但是並不需要IRWrite,可不可以讓一個對象同時支持這兩個接口呢,就像COM一樣?當然可以,我們借助於這樣一個輔助模版:
template<typename B1, typename B2>
struct Combine : B1, B2{
typedef B1 type1;
typedef B1 type2;
<!--[if !supportEmptyParas]--> }; <!--[endif]-->
typedef Reform< Combine<IReader, IWriter>, ImpRefCount, ImpWriter, ImpReader >::type ConcreteRWiter
為了現實需要,我們可以提供Combine的多個特化版本以支持任意數量的接口組合。如果僅僅是為了去掉一個IRWiter就引入一個Combine,雖有好處,但是意義也不大。那麼,考慮這樣一個例子。
struct IHttpReader : IReader;
struct IFileReader : IReader;
我們需要一個對象,同時支持從網絡和從文件讀取的能力。先看不引入Combine的做法:
struct IFileHttpReader : IFileReader , IHttpReader;
typedef Reform<IFileHttpReader, ImpRefCount, ImpHttpReader,
ImpFileReader>::type ConcreteRWiter;
覺得有什麼問題嗎?ImpReader同時實現了IFileReader分支和IHttpReader分支中的IReader,但是,和IRefCount不同的是,我們完全有理由相信,這兩個分支其實需要不同的IReader的實現。即使IReader確實可以是同樣的實現,另一個嚴重的問題是,ImpReader是一個不完整的實現,ImpFileReader和ImpHttpReader都分別重載了IReader中的一部分方法,例如,兩者都實現了如下方法:
virtual bool open(const char* url);
如何解決這個問題?讓我們回顧一下IFileHttpReader,首先這個接口就是個問題產物:
open到底open什麼?文件,還是HTTP連接,還是兩個都打開?也就是說,從概念上來講,IFileHttpReader就存在矛盾,這樣的概念很顯然是難以維護的。其次,我們完全沒有辦法為兩個分支提供不同的實現,當然,其根源是IFileHttpReader的錯誤設計導致的,不采用我們這裡提到的技術,問題依然存在。現在引入一個結論:如果某個接口的基類樹中多次出現同一個接口,我們的技術無法為這些接口分別提供不同的實現。這裡的解決方案是拋棄IFileHttpReader,引入Combine, 我們可以這樣解決問題:
typedef Reform<
Combine< ImpFileReader <IFileReader>, ImpHttpReader <IHttpReader> >,
ImpRefCount, ImpReader
>::type ConcreteFileHttpReader;
假設,ImpReader不能同時滿足兩個分支的要求,我們可以這麼做:
typedef Reform <
Combine< ImpFileReader < ImpReaderA<IFileReader> >,
ImpHttpReader < ImpReaderB <IHttpReader> >
>,
ImpRefCount
>::type ConcreteFileHttpReader;
利用Combine,我們可以充分發揮多重繼承的組合能力,我們既享受了接口設計和實現分離的好處—更容易維護概念了,也充分享有代碼復用的能力。並且,將設計決策充分推遲:甚至客戶程序員完全可以定制自己的接口實現從而和現有系統結合,這是一個完美的Open-Close的設計手段。
<!--[if !supportLists]--><!--[if !supportLists]--><!--[if !supportLists]--><!--[if !supportLists]--> 現在,總結一下在多重繼承中的注意事項。
1.接口盡量是單繼承的。
2.多重繼承的接口必須意識到,所有繼承樹的相同接口只能共享同一份實現。
3.嚴苛地去維護接口的概念,不要為了實現問題定義中間的接口(就象那個IFileHttpReader)
4.合理地利用多重繼承的組合能力。<!--[if !supportEmptyParas]-->
關於最後一條,您可以做一些有趣的探索。給出一個空基類:
struct Over{};
當然,也可以是其它非模板類。把所有的類都實現成模版形式:ImpClassA<T>, ImpClassB<T>,借助於Combine,我們可能給出這樣的定義:
typedef Combine<ImpClassA< Combine<ImpClassB< Over >, ImpClassC< Over > > >,Combine<ImpClassF<Over>, ImpClassB<ImpClassD< Over > >>,ImpClassE<Over>>::type ConcreteSomeClass;
我們注重於將這些ImpClasses拆成盡可能小的正交模塊。那麼借助組合技術,可能獲得很高的復用性。但是,有句老話,不要為了復用而復用,反正,這裡的探索我也是淺嘗辄止,出了什麼事情和我無關。特別提醒一下,上面代碼中Combine裡面出現了一個type,你可以嘗試在上面施加你喜歡的TMP手法嘛。
把那些有趣的探索先放在一邊。現在,我已經把這種技術完整地呈現出來了。然而,沒有一項技術是完美的,這裡也不例外。這個技術有兩個小小的缺陷。
第一個缺陷則是構造函數的問題。回顧Combine的實現,我們無法為Combine額外提供合適的構造函數。不過,這個問題並不是特別嚴重,我們完全可以定制自己的Combine。並且,這些不同的Combine可以混合使用。另外,在組裝的時候需要小心的維護構造函數的調用鏈,這可能傷害到復用性。Assignment中也存在類似的問題。運算符重載也可能導致混亂,不過,我一直認為,在值語義之外的類當中重載運算符可是要非常謹慎的,很顯然,我們這裡展示的技術並不適合值語義的類型設計。
另一個缺陷是工程上的。因為上述的實現都是模板類,所以,我們通常需要將實現在頭文件裡面提供,這個可能是有些人不願意的。我將展現一種解決的方法,這就是另一個利器:Pimpl慣用法。
以IReader為例,假設如下接口:
struct IReader : IRefCount
{
virtual bool open(const char* url) = 0;
virtual void read(Buffer& buf, size_t size) = 0;
};
現在,我們只實現read方法:
class ConcreteImpReader;//前置申明
template<typename Base>
class ImpPartialReader : public Base
{
Pimpl<ConcreteImpReader> m_reader;
public:
ImpPartialReader() : m_reader(this), Base(){}
virtual void read(Buffer& buf, size_t size) { m_reader->read(buf, size);}
};
現在,給出一個原始的Pimpl實現:
template<typename T>
struct Pimpl
{
T* imp;
template<typename Host>
explicit Pimpl(Host* host) : imp(new T(host)){}
T* operator->() const{return imp;}
~Pimpl(){delete imp;}
…
};
在單獨的文件中實現:
class ConcreteImpReader
{
ConcreteImpReader(IReader * host) : m_host(host){}
void read(Buffer& buf, size_t size) { …}
…
};
ConcreteImpReader中可以引用所在的host對象的其他方法,但是自己實現的方法除外。如果我們願意,也可以把接口的實現分拆到多個具體的實現類當中,只是我們無法獲得象多重繼承那樣強大的組合能力。