如果說,類的設(shè)計(jì)思路,是以數(shù)據(jù)為基礎(chǔ)的縱向組織結(jié)構(gòu),只有唯一的分類方式,有相同基類的,就意味著其相似性,共同點(diǎn)都體現(xiàn)在基類上;那么,接口就是以功能以性質(zhì)從橫向上,來看待類的相似性,并且存在無數(shù)的橫向視角(否則就失去意義)。
靜態(tài)面向?qū)ο笳Z言,這里不考慮template,c++的template是鴨子類型,本質(zhì)上,c++編譯期就是一個(gè)功能完備的動(dòng)態(tài)語言。代碼上的復(fù)用就只能以基類為粒度來進(jìn)行,比如,函數(shù)int fn(Base* bb),只有Base的子類,才有資格成為函數(shù)fn的會(huì)員。函數(shù)fn之所以聲明其變量bb的類型為Base,就是為了使用類型Base里面的一些東西,一般就是成員函數(shù)(對(duì)于清教徒來說,不是一般,而是必然)。假如,函數(shù)fn的實(shí)現(xiàn)中,就用到Base的幾個(gè)成員函數(shù),比如說f1,f2,…,fn。換句話說,雖然fn(Base* bb)表面上要求一定要Base的子孫后代才能擔(dān)當(dāng)重任,但實(shí)際上,只要?jiǎng)e的class,不必跟Base有半毛錢關(guān)系,只要這個(gè)class里面支持f1,f2,…,fn這些操作,那么原則上他就有資格到fn里面一游。天下唯有德者居之,不必講究什么貴族。但是,在沒有接口的等級(jí)森嚴(yán)的封建社會(huì)里面,就算你有驚天之地之能,就因?yàn)槟銢]有某種高貴的血統(tǒng),所以你就不行。
在單根類的王國(guó)中,所有對(duì)象都源于Object,也可以通過反射,通過函數(shù)名字運(yùn)行時(shí)獲取串f1,f2,fn等成員函數(shù),然后再人肉編譯器關(guān)于參數(shù)信息和返回值類型,以擺脫Base的類型桎梏,但是,估計(jì)也只有在最特殊的時(shí)候,才會(huì)這樣玩。這樣玩,簡(jiǎn)直置編譯器的類型檢查于不顧,靜態(tài)語言就是要盡可能的挖掘編譯器類型檢查的最后一絲潛力。
接口的出現(xiàn),就在縱向的類型關(guān)系上撕開一道道口子,從而盡最大限度釋放對(duì)象的能力。時(shí)代不同了,現(xiàn)在接口IBase里面聲明f1,f2,fn等函數(shù),然后函數(shù)fn的入?yún)?/span>IBase,也即是 int fn(IBase* bb),以明確表示fn里面只用到IBase的函數(shù),語義的要求上更加精準(zhǔn)。然后,任何class,只要其實(shí)現(xiàn)了接口IBase,就有資格被fn接納,不必再是Base之后了。所以說,要面向接口編程,就是要面向功能來搬磚,選擇的樣本空間就廣闊了很多。接口是比具體類型要靈活,但不意味著所有的地方就必須只出現(xiàn)接口,class類型就沒用了,當(dāng)然不是,有些地方就很有必要用具體類型,比如說string類型,比如說復(fù)數(shù)這些,就必須明確規(guī)定具體類型,無須用到接口的靈活性。總之,還是那句話,沒有銀彈,具體問題具體分析。
使用對(duì)象,其實(shí)就是在使用對(duì)象的成員函數(shù),那么,接口也可以看成是成員函數(shù)的粒度管理工具。所以,接口就表示了一批成員函數(shù),需要用一批成員函數(shù)的時(shí)候,用接口最為方便。坊間有一些犯virtual恐懼癥的c++猿猴,高高興興地用一批function代替接口,罔顧其性能(時(shí)間空間)的損失、使用上的不便,哎!面向?qū)ο笫菑?qiáng)有力的抽象工具,比之于面向過程,函數(shù)式,有著獨(dú)特的優(yōu)點(diǎn),反正代碼構(gòu)架上,優(yōu)先使用面向?qū)ο?,絕不會(huì)錯(cuò)。而面向?qū)ο?,就必然回避不了接口?/span>
坊間支持面向?qū)ο笳Z言中對(duì)接口的支持,當(dāng)以rust,scala的trait機(jī)制最為令人喜歡,非侵入式啊,自然狗語言的也還好,但是,本人最反感,反正,狗語言上一切獨(dú)有特性,本人都本能地毫無理由排斥。自然,java、C#或者c++的多繼承,最為笨拙,呆板。
java、C#里面,類能夠?qū)崿F(xiàn)的接口,在類的定義中,就已經(jīng)定下來了。類一旦定義完畢,與該類相關(guān)的接口就定下來,鐵板一塊,密不透風(fēng),不能增不能減也不能改。你明明看到一個(gè)類就已經(jīng)實(shí)現(xiàn)了某個(gè)接口的所有方法(函數(shù)名字和簽名一模一樣),但就是因?yàn)樵擃悰]有在定義中明確說明實(shí)現(xiàn)該接口,所以編譯器就死活不承認(rèn)該類實(shí)現(xiàn)這個(gè)接口。只能用適配器模式,也即是新造一個(gè)class,實(shí)現(xiàn)該接口,包含舊類的對(duì)象,將接口的所有方法都委托給對(duì)象的相應(yīng)函數(shù)來做。java的繁文縟節(jié)就是這樣來的,規(guī)規(guī)矩矩,畢恭畢敬,一步一個(gè)腳印。更麻煩的是,每次傳遞參數(shù)都要new一個(gè)適配器對(duì)象來滿足參數(shù)的要求,這是最讓人難受的地方。
java、C#的這種接口機(jī)制,實(shí)在與現(xiàn)實(shí)對(duì)不上號(hào),真是找不到任何原型,任何類型的物品,就算是新造的東西,我們都不可能一開始就窮盡它的所有性質(zhì)所有功能。就算是藥物,都有可能是歪打正著的功能,比如偉哥的功能,是其研發(fā)階段中意想不到的。java、c#的這種接口,會(huì)很干擾類的完整最小化的設(shè)計(jì)原則,進(jìn)而加大類的設(shè)計(jì)難度。當(dāng)然,它也非一無是處,起碼,類支持多少接口,一眼就看出來了,毫無疑義。問題是,接口這種東西,本質(zhì)上就應(yīng)該是不確定的橫向視角來考察類的關(guān)系。java、C#下的接口問題,大大限制了接口的使用場(chǎng)合。
其次,繼承時(shí),子類就繼承了基類的所有東西,包括其實(shí)現(xiàn)的接口。但是,有些時(shí)候,子類并不想擁有父類的某些接口。比如,鴨子應(yīng)該算是鳥類的一個(gè)子類,而鳥類支持“會(huì)飛”這個(gè)接口,但是鴨子顯然不會(huì)飛,也就是說,雖然鴨子包含了鳥類的所有數(shù)據(jù),但是它不擁有會(huì)飛這個(gè)功能。對(duì)此,我們希望在編譯期間,就能在要求會(huì)飛的場(chǎng)合下,傳鴨子對(duì)象進(jìn)去時(shí),編譯器報(bào)錯(cuò)。但是,對(duì)此,只能在運(yùn)行中報(bào)錯(cuò),而且,還是在調(diào)用會(huì)飛的成員函數(shù)里面才報(bào)錯(cuò)。原則上,編譯器是可以知道鴨子不會(huì)飛這個(gè)概念的,但是,由于java、C#的接口控制粒度單一,滿足不了這種要求。
再次,接口不能組合,比如說,函數(shù)fn的參數(shù),假設(shè)名字為pp,pp要求同時(shí)實(shí)現(xiàn)接口IA,IB。對(duì)此,java、C#中是沒有語法滿足這種多個(gè)接口的要求。遇到這種需求時(shí),只能用強(qiáng)制類型轉(zhuǎn)換,先隨便讓參數(shù)類型為IA或者IB,然后在必要時(shí),強(qiáng)制轉(zhuǎn)換為另外的類型,只能在運(yùn)行時(shí)報(bào)錯(cuò)。又或者是,新造一個(gè)接口IAB從IA,IB上繼承,然后函數(shù)fn的參數(shù)pp的類型為IAB,但是這樣,依然存在不足,假如某個(gè)類實(shí)現(xiàn)IA和IB,但是沒有表明它實(shí)現(xiàn)IAB,那么還是不能滿足參數(shù)的要求。接口組合的問題,不管是go、rust,都沒有很好的支持,只能到運(yùn)行時(shí)類型轉(zhuǎn)換才能發(fā)生。
最重要的是,這種接口機(jī)制違反了零懲罰的機(jī)制。就以c++為例來說明,就只論接口好了,也即是只有虛函數(shù)但是沒有成員字段的基類。為了方便描述,還是舉例子。
struct IA {virtual void fa() = 0;};
struct IB {virtual void fb() = 0;};
struct Base{…};
struct Derived : public Base, public IA, public IB{…};
接口IA有虛函數(shù),里面就要有一個(gè)指針指向其虛函數(shù)表,所以其內(nèi)存占用就是一個(gè)指針的大?。煌恚?/span>IB也如此。表面的意思是Derived實(shí)現(xiàn)了接口IA,IB,實(shí)際上,在C++中,接口實(shí)現(xiàn)就是繼承,也就是說每個(gè)Derived的實(shí)例都要包含IA,IB里面的數(shù)據(jù),指向?qū)?yīng)虛函數(shù)表的指針字段,也即是有兩個(gè)指針。這里做不到零懲罰的意思,是說, Derived為了表明自己有IA、IB的能力,每個(gè)對(duì)象付出了兩個(gè)多余的內(nèi)存指針空間的代價(jià),即便是對(duì)象不需要在IA、IB的環(huán)境下使用,這個(gè)代價(jià)都避免不了。零懲罰抽象,就是要用到的時(shí)候才付出代價(jià),哪怕這個(gè)代價(jià)可以大一點(diǎn)。用不到時(shí),則不必消耗哪怕一點(diǎn)點(diǎn)空間時(shí)間上的浪費(fèi)??臻g上浪費(fèi)的問題不在于節(jié)省內(nèi)存,而在于喪失了精致的內(nèi)存布局,進(jìn)而影響到二進(jìn)制的復(fù)用。這一點(diǎn),非侵入式接口就不用也沒辦法在對(duì)象身上包含其所支持的所有接口的虛函數(shù)表指針,因?yàn)轭愋投x完畢,后面還可能在其上添加新的接口實(shí)現(xiàn)。
而由這幾點(diǎn)問題引申出來的其他缺陷就不必提了。反正,C++,包括java,C#的這種接口機(jī)制最不討人喜歡了。
至于狗語言的鴨子接口,有時(shí)會(huì)出現(xiàn)函數(shù)名字沖突的小問題,稍微改一下名字就好了。主要是這種接口機(jī)制只要一個(gè)類包含了某個(gè)接口的所有成員函數(shù),就隱式認(rèn)為它實(shí)現(xiàn)了這個(gè)接口。這里會(huì)有暗示(誤導(dǎo),誘惑),就是定義類的成員函數(shù)時(shí),會(huì)有意或者無意地遷就現(xiàn)有接口的成員函數(shù),同樣,聲明接口成員函數(shù)時(shí),也會(huì)有意無意地往現(xiàn)有類的成員函數(shù)上靠。從而導(dǎo)致真正函數(shù)的語義上把控不夠精準(zhǔn)。并且,這種機(jī)制太過粗暴,萬一這個(gè)類雖然支持某個(gè)接口的所有函數(shù),但是并不一定就意味著它就要實(shí)現(xiàn)這個(gè)接口了。狗語言最令人反感之處就是各種自作聰明自以為是的規(guī)定。當(dāng)然,由于狗語言的成員函數(shù)可以非侵入式,這個(gè)問題造成的不便一定程度上有所減輕,但是,說實(shí)在,就連非侵入式的成員函數(shù),本座也不太喜歡了。另外,僅僅從語言層面上,不借助文檔,很難知道一個(gè)類到底實(shí)現(xiàn)那些接口,某個(gè)接口被那些類實(shí)現(xiàn),java、C#的接口在這一點(diǎn)的表現(xiàn)上就很卓越。其實(shí),本座反感狗語言的最大原因還是因?yàn)楣贩郏啾戎拢?/span>java粉、php粉等粉,就可愛多了。
rust以trait形式實(shí)提供的接口機(jī)制就不多說了,語法形式上簡(jiǎn)潔漂亮,基本上夢(mèng)寐以求的接口樣子就是這樣子的了。
以上語言的接口,全部屬于靜態(tài)接口,也即是類型所實(shí)現(xiàn)的接口在編譯期間就全部定下來了,運(yùn)行時(shí)就不再有任何變化。但是,如果對(duì)象一直在變化,好比生物,就說人類好了,有嬰兒少年青年中年老年死亡這些變化階段,顯然每一階段的行為能力都大不一樣,也擁有不同頭銜,不同身份。也就是說,現(xiàn)實(shí)中,活生生對(duì)象的接口集合并非一成不變,它完全可以現(xiàn)在就不支持某個(gè)接口,高興時(shí)候又可以支持了,不高興時(shí)就又不支持了,聾了就聽不到聲音,盲了就看不見,好似消息發(fā)送那樣子,顯然以上語言是不支持這種動(dòng)態(tài)需求的接口的。
另外,com的接口查詢雖然發(fā)生在運(yùn)行時(shí),但是,com的規(guī)范,比如對(duì)稱性、傳遞性、時(shí)間無關(guān)性等規(guī)則,硬是把com從動(dòng)態(tài)接口降維到靜態(tài)接口,這也可以理解,因?yàn)閯?dòng)態(tài)接口的應(yīng)用場(chǎng)景真的并不多。這些都沒什么,com最根本的問題,還是在于接口要承載類的功能,當(dāng)然,這樣也有好處,比如語言的無關(guān)性。IUnknown的三大成員函數(shù)分明就是類的本職工作,AddRef,Release管理對(duì)象的生命周期,Query查詢所要的接口。生命周期由對(duì)象粒度細(xì)化為接口粒度,就顯得太瑣碎,要謹(jǐn)記好幾條規(guī)則,要小心翼翼地應(yīng)付AddRef,Release的函數(shù)調(diào)用,智能指針也只能減輕部分工作量,這就是粒度過小帶來的痛苦。而Query的本質(zhì)就是對(duì)象所實(shí)現(xiàn)接口集合,這是對(duì)象的本分工作,現(xiàn)在搞成接口與接口之間的關(guān)系。由于接口越俎代庖,承接了類的職責(zé),就要求每個(gè)接口都要繼承IUnknown,本來接口之間就應(yīng)該沒什么關(guān)聯(lián)性的才對(duì),還導(dǎo)致com的實(shí)現(xiàn)以及使用,在c++下,非常繁復(fù)麻煩,令人頭皮發(fā)麻。所以說,類與接口,一體兩面,誰也不能代替誰。
---------------------------------------------------------------------------------------------------------------------------------
備注:現(xiàn)實(shí)世界中,一種或幾種功能就能推導(dǎo)出來其他性質(zhì),對(duì)應(yīng)到接口中,就是如果對(duì)象實(shí)現(xiàn)某些接口,就表示它能實(shí)現(xiàn)另外其他接口。目前的語言,也就是接口繼承,子接口繼承父接口,那么,如果一個(gè)類實(shí)現(xiàn)了子接口,就表示它也實(shí)現(xiàn)了父接口,語言明面上只支持這種接口的蘊(yùn)含關(guān)系。對(duì)于其他的蘊(yùn)含情況,只能用適配器來湊數(shù),而在非侵入式接口中,其語言形式就顯得更加的累贅,這一點(diǎn),在java上尤為突出。其實(shí),說到底,適配器模式只是彌補(bǔ)語言不支持接口蘊(yùn)含機(jī)制的產(chǎn)物。