在思考怎么寫(xiě)這一篇文章的時(shí)候,我又想到了以前討論正交概念的事情。如果一個(gè)系統(tǒng)被設(shè)計(jì)成正交的,他的功能擴(kuò)展起來(lái)也可以很容易的保持質(zhì)量這是沒(méi)錯(cuò)的,但是對(duì)于每一個(gè)單獨(dú)給他擴(kuò)展功能的個(gè)體來(lái)說(shuō),這個(gè)系統(tǒng)一點(diǎn)都不好用。所以我覺(jué)得現(xiàn)在的語(yǔ)言被設(shè)計(jì)成這樣也是有那么點(diǎn)道理的。就算是設(shè)計(jì)Java的那誰(shuí),他也不是傻逼,那為什么Java會(huì)被設(shè)計(jì)成這樣?我覺(jué)得這跟他剛開(kāi)始想讓金字塔的底層程序員也可以順利使用Java是有關(guān)系的。
?
難道好用的語(yǔ)言就活該不好擴(kuò)展碼?實(shí)際上不是這樣的,但是這仍然是上面那個(gè)正交概念的問(wèn)題。一個(gè)容易擴(kuò)展的語(yǔ)言要讓你覺(jué)得好用,首先你要投入時(shí)間來(lái)學(xué)習(xí)他。如果你想簡(jiǎn)單的借鑒那些不好擴(kuò)展的語(yǔ)言的經(jīng)驗(yàn)(如Java)來(lái)在短時(shí)間內(nèi)學(xué)會(huì)如何使用一個(gè)容易擴(kuò)展的語(yǔ)言(如C++/C#)——你的出發(fā)點(diǎn)就已經(jīng)投機(jī)了。所以這里有一個(gè)前提值得我再?gòu)?qiáng)調(diào)一次——首先你需要投入時(shí)間去學(xué)習(xí)他。
?
正如我一直在群里說(shuō)的:"C++需要不斷的練習(xí)——vczh"。要如何練習(xí)才能讓自己借助語(yǔ)言做出一個(gè)可擴(kuò)展的架構(gòu)呢?先決條件就是,當(dāng)你在練習(xí)的時(shí)候,你必須是在練習(xí)如何實(shí)現(xiàn)一個(gè)從功能上就要求你必須保證他的可擴(kuò)展性的系統(tǒng),舉個(gè)例子,GUI庫(kù)就是其中的一類(lèi)。我至今認(rèn)為,學(xué)會(huì)實(shí)現(xiàn)一個(gè)GUI庫(kù),比通過(guò)練習(xí)別的什么東西來(lái)提高自己的能力來(lái)講,簡(jiǎn)直就算一個(gè)捷徑了。
?
那么什么是擴(kuò)展呢?簡(jiǎn)單的來(lái)講,擴(kuò)展就是在不修改原有代碼的情況下,僅僅通過(guò)添加新的代碼,就可以讓原有的功能適應(yīng)更多的情況。一般來(lái)講,擴(kuò)展的主要目的并不是要增加新的功能,而是要只增加新代碼的前提下修改原有的功能。譬如說(shuō)原來(lái)你的系統(tǒng)只支持SQLServer,結(jié)果有一天你遇到了一個(gè)喜歡Oracle的新客戶(hù),你要把東西賣(mài)給他,那就得支持Oracle了吧。但是我們知道,SQLServer和Oracle在各種協(xié)議(asp.net、odbc什么的)上面是有偏好的,用DB不喜歡的協(xié)議來(lái)連接他的時(shí)候bug特別多,這就造成了你又可能沒(méi)辦法使用單一的協(xié)議來(lái)正確的使用各種數(shù)據(jù)庫(kù),因此擴(kuò)展的這個(gè)擔(dān)子就落在你的身上了。當(dāng)然這種系統(tǒng)并不是人人都要寫(xiě),我也可以換一個(gè)例子,假如你在設(shè)計(jì)一個(gè)GPU集群上的程序,那么這個(gè)集群的基礎(chǔ)架構(gòu)得支持NVidia和AMD的顯卡,還得支持DirectCompute、Cuda和OpenCL。然而我們知道,OpenCL在不同的平臺(tái)上,有互不兼容的不同的bug,導(dǎo)致你實(shí)際上并不可能僅僅通過(guò)一份不變的代碼,就充分發(fā)揮OpenCL在每一個(gè)平臺(tái)上的最佳狀態(tài)……現(xiàn)實(shí)世界的需求真是orz(OpenCL在windows上用AMD卡定義一個(gè)struct都很容易導(dǎo)致崩潰什么的,我覺(jué)得這根本不能用)……
?
在語(yǔ)言里面談擴(kuò)展,始終都離不開(kāi)兩個(gè)方面:編譯期和運(yùn)行期。這些東西都是用看起來(lái)很像pattern matching的方法組織起來(lái)的。如果在語(yǔ)言的類(lèi)型系統(tǒng)的幫助下,我們可以輕松做出這樣子的架構(gòu),那這個(gè)語(yǔ)言就算有可擴(kuò)展的類(lèi)型了。
?
編譯期對(duì)類(lèi)型的擴(kuò)展
?
這個(gè)其實(shí)已經(jīng)被人在C++和各種靜態(tài)類(lèi)型的函數(shù)式語(yǔ)言里面做爛了。簡(jiǎn)單的來(lái)講,C++處理這種問(wèn)題的方法就是提供偏特化。可惜C++的偏特化只讓做在class上面,結(jié)果因?yàn)榇蠹覍?duì)class的誤解很深,順便連偏特化這種比OO簡(jiǎn)單一萬(wàn)倍的東西也誤解了。偏特化不允許用在函數(shù)上,因?yàn)楹瘮?shù)已經(jīng)有了重載,但是C++的各種標(biāo)準(zhǔn)在使用函數(shù)來(lái)擴(kuò)展類(lèi)型的時(shí)候,實(shí)際上還是當(dāng)他是偏特化那么用的。我舉個(gè)例子。
?
C++11多了一個(gè)foreach循環(huán),寫(xiě)成for(auto x : xs) { … }。STL的類(lèi)型都支持這種新的for循環(huán)。C++11的for循環(huán)是為了STL的容器設(shè)計(jì)的嗎?顯然不是。你也可以給你自己寫(xiě)的容器加上for循環(huán)。方法有兩種,分別是:1、給你的類(lèi)型T加上T::begin和T::end兩個(gè)成員函數(shù);2、給你的類(lèi)型T實(shí)現(xiàn)begin(T)和end(T)兩個(gè)全局函數(shù)。我還沒(méi)有去詳細(xì)考證,但是我認(rèn)為缺省的begin(T)和end(T)全局函數(shù)就是去調(diào)用T::begin和T::end的,因此for循環(huán)只需要認(rèn)begin和end兩個(gè)全局函數(shù)就可以了。
?
那自己的類(lèi)型怎么辦呢?當(dāng)然也要去重載begin和end了?,F(xiàn)在全局函數(shù)沒(méi)有重載,因此寫(xiě)出來(lái)大概是:
template<typename
T> auto begin(const
T& t)->decltype(t.begin()) { return
t.begin(); }
template<typename
T> my_iterator<T> begin(const
my_container<T>& t);
template<typename
T> my_range_iterator<T> begin(pair<T, T> range);
?
如果C++的函數(shù)支持偏特化的話,那么上面這段代碼就會(huì)被改成這樣,而且for循環(huán)也就不去找各種各樣的begin函數(shù)了,而只認(rèn)定那一個(gè)std::begin就可以了:
template<typename
T> auto begin(const
T& t)->decltype(t.begin()) { return
t.begin(); }
template<typename
T> my_iterator<T> begin< my_container<T>>(const
my_container<T>& t);
template<typename
T> my_range_iterator<T> begin< pair<T, T>>( const
pair<T, T>& range);
?
為什么要偏特化呢?因?yàn)檫@至少保證你寫(xiě)出來(lái)的begin函數(shù)跟for函數(shù)想要的begin函數(shù)的begin函數(shù)的簽名是相容的(譬如說(shuō)不能有兩個(gè)參數(shù)之類(lèi)的)。事實(shí)上C++11的for循環(huán)剛開(kāi)始是要求大家通過(guò)偏特化一個(gè)叫做std::range的類(lèi)型來(lái)支持的,這個(gè)range類(lèi)型里面有兩個(gè)static函數(shù),分別叫begin和end。后來(lái)之所以改成這樣,我猜大概是因?yàn)镃++的每一個(gè)函數(shù)重載也可以是模板函數(shù),因此就不需要引入一個(gè)新的類(lèi)型了,就讓大家去重載好了。而且for做出來(lái)的時(shí)候,C++標(biāo)準(zhǔn)里面還沒(méi)有concept,因此也沒(méi)辦法表達(dá)"對(duì)于所有可以循環(huán)的類(lèi)型T,我們都有std::range<T>必須滿(mǎn)足這個(gè)叫做range_loopable<T>的concept"這樣的前置條件。
?
重載用起來(lái)很容易讓人走火入門(mén),很多人到最后都會(huì)把一些僅僅看起來(lái)像而實(shí)際上語(yǔ)義完全不同的東西用重載來(lái)表達(dá),函數(shù)的參數(shù)連相似性都沒(méi)有。其實(shí)這是不對(duì)的,這種時(shí)候就應(yīng)該把函數(shù)改成兩個(gè)不同的名字。假如當(dāng)初設(shè)計(jì)C++的是我,那我一定會(huì)把函數(shù)重載干掉,然后允許人們對(duì)函數(shù)進(jìn)行偏特化,并且加上concept。既然std::begin已經(jīng)被定義為循環(huán)的輔助函數(shù)了,那么你重載一個(gè)std::begin,他卻不能用來(lái)循環(huán)(譬如說(shuō)有兩個(gè)參數(shù)什么的),那有意義嗎?完全沒(méi)有。
?
這種例子還有很多,譬如如何讓自己的類(lèi)型可以被<<到wcout的做法啦,boost的那個(gè)serialization框架,還有各種各樣的庫(kù),其實(shí)都利用了相同的思想——對(duì)類(lèi)型做編譯期的擴(kuò)展,使用一些手段使得在不需要修改原來(lái)的代碼的前提下,就可以讓編譯器找到你新加進(jìn)去的函數(shù),從而使得調(diào)用的寫(xiě)法不用發(fā)生變化就可以對(duì)原有的功能支持更多的情況。至少我們讓我們自己的類(lèi)型支持for循環(huán)就不需要翻開(kāi)std::begin的代碼把我們的類(lèi)型寫(xiě)進(jìn)去,只需要在隨便什么空白的地方重載一個(gè)std::begin就可以了。這就是一個(gè)很好地體現(xiàn)。C++的標(biāo)準(zhǔn)庫(kù)一直在引導(dǎo)大家正確設(shè)計(jì)一個(gè)可擴(kuò)展的架構(gòu),可惜很多人都意識(shí)不到這一點(diǎn),為了自己那一點(diǎn)連正確性都談不上的強(qiáng)迫癥,放棄了很多東西。
?
很多靜態(tài)類(lèi)型的函數(shù)式語(yǔ)言使用concept來(lái)完成上述的工作。當(dāng)一個(gè)concept定義好了之后,我們就可以通過(guò)對(duì)concept的實(shí)現(xiàn)進(jìn)行偏特化來(lái)讓我們的類(lèi)型T滿(mǎn)足concept的要求,來(lái)讓那些調(diào)用這個(gè)concept的泛型代碼,可以在處理的對(duì)象是T的時(shí)候,轉(zhuǎn)而調(diào)用我們提供的實(shí)現(xiàn)。Haskell就是一個(gè)典型的例子,一個(gè)sort函數(shù)必然要求元素是可比較的,一個(gè)可以比較的類(lèi)型定義為實(shí)現(xiàn)了Ord這個(gè)type class的類(lèi)型。所以你只要給你自己的類(lèi)型T實(shí)現(xiàn)Ord這個(gè)type class,那sort函數(shù)就可以對(duì)T的列表進(jìn)行排序了。
?
對(duì)于C++和C#這種沒(méi)有concept或者concept不是主要概念的語(yǔ)言里面,對(duì)類(lèi)型做靜態(tài)的擴(kuò)展只需要你的類(lèi)型滿(mǎn)足"我可以這么這么干"就可以了。譬如說(shuō)你重載一個(gè)begin和end,那你的類(lèi)型就可以被foreach;你給你的類(lèi)型實(shí)現(xiàn)了operator<等函數(shù),那么一個(gè)包含你的類(lèi)型的容器就可以被sort;或者C#的只要你的類(lèi)型T<U>有一大堆長(zhǎng)得跟System.Linq.Enumerable里面定義的擴(kuò)展函數(shù)一樣的擴(kuò)展函數(shù),那么Linq的神奇的語(yǔ)法就可以用在你的類(lèi)型上等等。這跟動(dòng)態(tài)類(lèi)型的"只要它長(zhǎng)的像鴨子,那么它就是鴨子"的做法有異曲同工之效。如果你的begin函數(shù)的簽名沒(méi)寫(xiě)對(duì),編譯器也不會(huì)屌你,直到你對(duì)他for的時(shí)候編譯器才會(huì)告訴你說(shuō)你做錯(cuò)了。這跟很多動(dòng)態(tài)類(lèi)型的語(yǔ)言的很多錯(cuò)誤必須在運(yùn)行的時(shí)候才發(fā)現(xiàn)的性質(zhì)也是類(lèi)似的。
?
Concept對(duì)于可靜態(tài)擴(kuò)展的類(lèi)型的約束,就如同類(lèi)型對(duì)于邏輯的約束一樣。沒(méi)有concept的C++模板,就跟用動(dòng)態(tài)類(lèi)型語(yǔ)言寫(xiě)邏輯一樣,只有到用到的那一刻你才知道你到底寫(xiě)對(duì)了沒(méi)有,而且錯(cuò)誤也會(huì)爆發(fā)在你使用它的地方,而不是你定義它的地方。因此本著編譯器幫你找到盡可能多的錯(cuò)誤的原則,C++也開(kāi)始有concept了。
?
C#的擴(kuò)展方法用在Linq上面,其實(shí)編譯器也要求你滿(mǎn)足一個(gè)內(nèi)在的concept,只是這個(gè)概念無(wú)法用C#的語(yǔ)法表達(dá)出來(lái)。所以我們?cè)趯?xiě)Linq Provider的時(shí)候也會(huì)有同樣的感覺(jué)。Java的interface都可以寫(xiě)缺省實(shí)現(xiàn)了,但是卻沒(méi)有靜態(tài)方法。這就造成了我們實(shí)際上無(wú)法跟C++和C#一樣,在不修改原有代碼的前提下,讓原有的功能滿(mǎn)足更多的情況。因?yàn)镃#的添加擴(kuò)展方法的情況,到了Java里面就變成讓一個(gè)類(lèi)多繼承自一個(gè)interface,必須修改代碼了。Java的這個(gè)功能特別的雞肋,不知道是不是他故意想跟C#不一樣才設(shè)計(jì)成這個(gè)樣子的,可惜精華沒(méi)有抄去,卻抄了糟粕。
?
運(yùn)行期對(duì)類(lèi)型的擴(kuò)展
?
自從Java吧靜態(tài)類(lèi)型和面向?qū)ο罄壴谝黄鹬?,業(yè)界對(duì)"運(yùn)行期對(duì)類(lèi)型的擴(kuò)展"這個(gè)主題思考了很多年,甚至還出了一本著作叫《設(shè)計(jì)模式》,讓很多人捧為經(jīng)典。大家爭(zhēng)先恐后的學(xué)習(xí),而效果卻不怎么樣。這是因?yàn)椤对O(shè)計(jì)模式》不好嗎?不是。這是因?yàn)殪o態(tài)類(lèi)型和面向?qū)ο罄壴谝黄鹬?,設(shè)計(jì)一個(gè)可擴(kuò)展的架構(gòu)就很難嗎?也不是。真正的原因是,Java設(shè)計(jì)(好像也是抄的Simular?我記不太清楚了)的虛函數(shù)把這個(gè)問(wèn)題的難題提升了一個(gè)等級(jí)。
?
用正確的概念來(lái)理解問(wèn)題可以讓我們更容易的掌握問(wèn)題的本質(zhì)。語(yǔ)言是有魔力的,習(xí)慣說(shuō)中文的人,思考方式都跟中國(guó)人差不多。習(xí)慣說(shuō)英語(yǔ)的人,思考方式都跟美國(guó)人差不多。因此習(xí)慣了使用C++/C#/Java的人,他們對(duì)于面向?qū)ο蟮南敕ㄆ鋵?shí)也是差不多的。這是人類(lèi)的天性。盡管大家鼓吹說(shuō)語(yǔ)言只是工具,我們應(yīng)該掌握方法論什么的,但是這就跟要求男人面對(duì)一個(gè)萌妹紙不勃起一樣,違背了人類(lèi)的本性,難度簡(jiǎn)直太高了。于是我今天從虛函數(shù)和Visitor模式講起,告訴大家為什么虛函數(shù)的這種形式會(huì)讓"擴(kuò)展的時(shí)候不修改原有的代碼"變難。
?
絕大多數(shù)的系統(tǒng)的擴(kuò)展,都可以最后化簡(jiǎn)(這并不要求你非得這么做)為"當(dāng)它的類(lèi)型是這個(gè)的時(shí)候你就干那個(gè)"的這么件事。對(duì)于在編譯的時(shí)候就已經(jīng)知道的,我們可以用偏特化的方法讓編譯器在生成代碼的時(shí)候就先搞好。對(duì)于運(yùn)行的時(shí)候,你拿到一個(gè)基類(lèi)(其實(shí)為什么一定要有基類(lèi)?應(yīng)該有的是interface!參見(jiàn)上一篇文章——刪減語(yǔ)言的功能),那如何O(1)時(shí)間復(fù)雜度(這里的n指的是所有跟這次跳轉(zhuǎn)有關(guān)系的類(lèi)型的數(shù)量)就跳轉(zhuǎn)到你想要的那個(gè)分支上去呢?于是我們有了虛函數(shù)。
?
靜態(tài)的擴(kuò)展用的是靜態(tài)的分派,于是編譯器幫我們把函數(shù)名都hardcode到生成的代碼里面。動(dòng)態(tài)的類(lèi)型用的是動(dòng)態(tài)的分派,于是我們得到的當(dāng)然是一個(gè)相當(dāng)于函數(shù)指針的東西。于是我們會(huì)把這個(gè)函數(shù)指針保存在從基類(lèi)對(duì)象可以O(shè)(1)訪問(wèn)到的地方。虛函數(shù)就是這么實(shí)現(xiàn)的,而且這種類(lèi)型的分派必須要這么實(shí)現(xiàn)的。但是,寫(xiě)成代碼就一定要寫(xiě)程序函數(shù)嗎?
?
其實(shí)本來(lái)沒(méi)什么理由讓一個(gè)語(yǔ)言(或者library)長(zhǎng)的樣子必須有提示你他是怎么實(shí)現(xiàn)的功能。關(guān)心太多容易得病,執(zhí)著太多心生痛苦啊。所以好好的解決問(wèn)題就好了。至于原理是什么,下了班再去關(guān)心。估計(jì)還有一些人不明白為什么不好,我就舉一個(gè)通俗的例子。我們都知道dynamic_cast的性能不怎么樣,虛函數(shù)用來(lái)做if的性能要遠(yuǎn)遠(yuǎn)比dynamic_cast用來(lái)做if的性能好得多。因此下面所有的答案都基于這個(gè)前提——要快,不要dynamic_cast!
?
處理HTML
?
好了,現(xiàn)在我們的任務(wù)是,拿到一個(gè)HTML,然后要對(duì)他做一些功能,譬如說(shuō)把它格式化成文本啦,看一下他是否包含超鏈接啦等等。假設(shè)我們已經(jīng)解決HTML的語(yǔ)法分析問(wèn)題,那么我們會(huì)得到一顆靜態(tài)類(lèi)型的語(yǔ)法樹(shù)。這棵語(yǔ)法樹(shù)如無(wú)意外一定是長(zhǎng)下面這個(gè)樣子的。另外一種選擇是存成動(dòng)態(tài)類(lèi)型的,但是這跟面向?qū)ο鬅o(wú)關(guān),所以就不提了。
?
class
DomBase
{
public:
????virtual ~DomBase();
?
????static
shared_ptr<DomBase> Parse(const
wstring& htmlText);
};
?
class
DomText : public
DomBase{};
class
DomImg : public
DomBase{};
class
DomA : public
DomBase{};
class
DomDiv : public
DomBase{};
......
?
HTML的tag種類(lèi)繁多,大概有那么上百個(gè)吧。那現(xiàn)在我們要給他加上一個(gè)格式化成字符串的功能,這顯然是一個(gè)遞歸的算法,先把sub tree一個(gè)一個(gè)格式化,最后組合起來(lái)就好了。可能對(duì)于不同的非文本標(biāo)簽會(huì)有不同的格式化方法。代碼寫(xiě)出來(lái)就是這樣——基本上是唯一的作法:
?
class
DomBase
{
public:
????virtual ~DomBase();
????static
shared_ptr<DomBase> Parse(const
wstring& htmlText);
?
????virtual
void
FormatToText(ostream& o); // 默認(rèn)實(shí)現(xiàn),把所有subtree的結(jié)果合并
};
?
class
DomText : public
DomBase
{
public:
????void
FormatToText(ostream& o); // 直接輸出文字
};
class
DomImg : public
DomBase
{
public:
????void
FormatToText(ostream& o); // 輸出img的tag內(nèi)容
};
// 其它實(shí)現(xiàn)略
class
DomA : public
DomBase{};
class
DomDiv : public
DomBase{};
?
這已經(jīng)構(gòu)成一個(gè)基本的HTML的Dom Tree了。現(xiàn)在我提一個(gè)要求如下,要求在不修改原有代碼只添加新代碼的情況下,避免dynamic_cast,實(shí)現(xiàn)一個(gè)考察一顆Dom Tree是否包含超鏈接的功能。能做嗎?
?
無(wú)論大家如何苦思冥想,答案都是做不到。盡管這么一看可能覺(jué)得這不是什么大事,但實(shí)際上這意味著:你無(wú)法通過(guò)添加模塊的方式來(lái)給一個(gè)已知的Dom Tree添加"判斷它是否包含超鏈接"的這個(gè)功能。有的人可能會(huì)說(shuō),那把它建模成動(dòng)態(tài)類(lèi)型的樹(shù)不就可以了?這是沒(méi)錯(cuò),但這實(shí)際上有兩個(gè)問(wèn)題。第一個(gè)是著顯著的增加了你的測(cè)試成本,不過(guò)對(duì)于充滿(mǎn)了廉價(jià)勞動(dòng)力的web行業(yè)來(lái)說(shuō)這好像也不是什么大問(wèn)題。第二個(gè)更加本質(zhì)——HTML可以這么做,并不代表所有的東西都可以裝怎么做事吧。
?
那在靜態(tài)類(lèi)型的前提下,要如何解決這個(gè)問(wèn)題呢?很久以前我們的《設(shè)計(jì)模式》就給我們提供了visitor模式,用來(lái)解決這樣的問(wèn)題。如果把這個(gè)Dom Tree修改成visitor模式的代碼的話,那原來(lái)FormatToText就會(huì)變成這個(gè)樣子:
?
class
DomText;
class
DomImg;
class
DomA;
class
DomDiv;
?
class
DomBase
{
public:
????virtual ~DomBase();
????static
shared_ptr<DomBase> Parse(const
wstring& htmlText);
?
????class
IVisitor
????{
????public:
????????virtual ~IVisitor();
?
????????virtual
void Visit(DomText* dom) = 0;
????????virtual
void Visit(DomImg* dom) = 0;
????????virtual
void Visit(DomA* dom) = 0;
????????virtual
void Visit(DomDiv* dom) = 0;
????};
?
????virtual
void Accept(IVisitor* visitor) = 0;
};
?
class
DomText : public
DomBase
{
public:
????void Accept(IVisitor* visitor)override
????{
????????visitor->Visit(this);
????}
};
class
DomImg : public
DomBase
{
public:
????void Accept(IVisitor* visitor)override
????{
????????visitor->Visit(this);
????}
};
class
DomA : public
DomBase
{
public:
????void Accept(IVisitor* visitor)override
????{
????????visitor->Visit(this);
????}
};
class
DomDiv : public
DomBase
{
public:
????void Accept(IVisitor* visitor)override
????{
????????visitor->Visit(this);
????}
};
?
class
FormatToTextVisitor : public
DomBase::IVisitor
{
private:
????ostream& o;
public:
????FormatToTextVisitor(ostream& _o)
????????:o(_o)
????{
?
????}
?
????void Visit(DomText* dom){} // 直接輸出文字
????void Visit(DomImg* dom){} // 輸出img的tag內(nèi)容
????void Visit(DomA* dom){} // 默認(rèn)實(shí)現(xiàn),把所有subtree的結(jié)果合并
????void Visit(DomDiv* dom){} // 默認(rèn)實(shí)現(xiàn),把所有subtree的結(jié)果合并
?
????static
void Evaluate(DomBase* dom, ostream& o)
????{
????????FormatToTextVisitor visitor(o);
????????dom->Accept(&visitor);
????}
};
?
看起來(lái)長(zhǎng)了不少,但是我們驚奇地發(fā)現(xiàn),這下子我們可以通過(guò)提供一個(gè)Visitor,來(lái)在不修改原有代碼的前提下,避免dynamic_cast,實(shí)現(xiàn)判斷一顆Dom Tree是否包含超鏈接的功能了!不過(guò)別高興得太早。這兩種做法都是有缺陷的。
?
虛函數(shù)的好處是你可以在不修改原有代碼的前提下添加新的Dom類(lèi)型,但是所有針對(duì)Dom Tree的操作緊密的耦合在了一起,并且邏輯還分散在了每一個(gè)具體的Dom類(lèi)型里面。你添加一個(gè)新功能就要修改所有的DomBase的子類(lèi),因?yàn)槟阋o他們都添加你需要的虛函數(shù)。
?
Visitor的好處是你可以在不修改原有代碼的前提下添加新的Dom操作,但是所有的Dom類(lèi)型卻緊密的耦合在了一起,因?yàn)镮Visitor類(lèi)型要包含所有DomBase的子類(lèi)。你每天加一個(gè)新的Dom類(lèi)型就得修改所有的操作——即IVisitor的接口和所有的具體的Visitor。而且還有另一個(gè)問(wèn)題,就是虛函數(shù)的默認(rèn)實(shí)現(xiàn)寫(xiě)起來(lái)比較鳥(niǎo)了。
?
所以這兩種做法都各有各的耦合。
?
碰撞系統(tǒng)
?
看了上面對(duì)于虛函數(shù)和Visitor的描述,大家大概知道了虛函數(shù)和Visitor其實(shí)都是同一個(gè)東西,只是各有各的犧牲。因此他們是可以互相轉(zhuǎn)換的——大家通過(guò)不斷地練習(xí)就可以知道如何把一個(gè)解法表達(dá)成虛函數(shù)的同時(shí)也可以表達(dá)成Visitor了。但是Visitor的代碼又臭又長(zhǎng),所以下面我只用虛函數(shù)來(lái)寫(xiě),懶得敲太多代碼了。
?
虛函數(shù)只有一個(gè)this參數(shù),所以他是single dynamic dispatch。對(duì)于碰撞系統(tǒng)來(lái)說(shuō),不同種類(lèi)的物體之間的碰撞代碼都是不一樣的,所以他有兩個(gè)"this參數(shù)",所以他是multiple dynamic dispatch。在接下來(lái)的描述會(huì)發(fā)現(xiàn),只要遇上了multiple dynamic dispatch,在現(xiàn)有的架構(gòu)下避免dynamic_cast,無(wú)論你用虛函數(shù)還是visitor模式,做出來(lái)的solution全都是不管操作有沒(méi)有偶合在一起,反正類(lèi)型是肯定會(huì)偶合在一起的。
?
現(xiàn)在我們面對(duì)的問(wèn)題是這樣的。在物理引擎里面,我們經(jīng)常需要判斷兩個(gè)物體是否碰撞。但是物體又不只是三角形組成的多面體,還有可能是標(biāo)準(zhǔn)的球形啊、立方體什么的。因此這顯然還是一個(gè)繼承的結(jié)構(gòu),而且還有一個(gè)虛函數(shù)用來(lái)判斷一個(gè)對(duì)象跟另一個(gè)對(duì)象是否碰撞:
?
class
Geometry
{
public:
????virtual ~Geometry();
?
????virtual
bool IsCollided(Geometry* second) = 0;
};
?
class
Sphere : public
Geometry
{
public:
????bool IsCollided(Geometry* second)override
????{
????????// then ???
????}
};
?
class
Cube : public
Geometry
{
public:
????bool IsCollided(Geometry* second)override
????{
????????// then ???
????}
};
?
class
Triangles : public
Geometry
{
public:
????bool IsCollided(Geometry* second)override
????{
????????// then ???
????}
};
?
大家猛然發(fā)現(xiàn),在這個(gè)函數(shù)體里面也不知道second到底是什么東西。這意味著,我們還要對(duì)second做一次single dynamic dispatch,這也就意味著我們需要添加新的虛函數(shù)。而且這不是一個(gè),而是很多。他們分別是什么呢?由于我們已經(jīng)對(duì)first(也就是那個(gè)this指針)dispatch過(guò)一次了,所以我們要把dispatch的結(jié)果告訴second,要讓它在dispatch一次。所以當(dāng)first分別是Sphere、Cube和Triangles的時(shí)候,對(duì)second的dispatch應(yīng)該有不同的邏輯。因此很遺憾的,代碼會(huì)變成這樣:
?
class
Sphere;
class
Cube;
class
Triangles;
?
class
Geometry
{
public:
????virtual ~Geometry();
?
????virtual
bool IsCollided(Geometry* second) = 0;
????virtual
bool IsCollided_Sphere(Sphere* first) = 0;
????virtual
bool IsCollided_Cube(Cube* first) = 0;
????virtual
bool IsCollided_Triangles(Triangles* first) = 0;
};
?
class
Sphere : public
Geometry
{
public:
????bool IsCollided(Geometry* second)override
????{
????????return
second->IsCollided_Sphere(this);
????}
?
????bool IsCollided_Sphere(Sphere* first)override
????{
????????// Sphere * Sphere
????}
?
????bool IsCollided_Cube(Cube* first)override
????{
????????// Cube * Sphere
????}
?
????bool IsCollided_Triangles(Triangles* first)override
????{
????????// Triangles * Sphere
????}
};
?
class
Cube : public
Geometry
{
public:
????bool IsCollided(Geometry* second)override
????{
????????return
second->IsCollided_Cube(this);
????}
?
????bool IsCollided_Sphere(Sphere* first)override
????{
????????// Sphere * Cube
????}
?
????bool IsCollided_Cube(Cube* first)override
????{
????????// Cube * Cube
????}
?
????bool IsCollided_Triangles(Triangles* first)override
????{
????????// Triangles * Cube
????}
};
?
class
Triangles : public
Geometry
{
public:
????bool IsCollided(Geometry* second)override
????{
????????return
second->IsCollided_Triangles(this);
????}
?
????bool IsCollided_Sphere(Sphere* first)override
????{
????????// Sphere * Triangles
????}
?
????bool IsCollided_Cube(Cube* first)override
????{
????????// Cube * Triangles
????}
?
????bool IsCollided_Triangles(Triangles* first)override
????{
????????// Triangles * Triangles
????}
};
?
大家可以想象,如果還有第三個(gè)Geometry參數(shù),那還得給Geometry加上9個(gè)新的虛函數(shù),三個(gè)子類(lèi)分別實(shí)現(xiàn)他們,加起來(lái)我們一共要寫(xiě)13個(gè)虛函數(shù)(3^0 + 3^1 + 3^2)39個(gè)函數(shù)體(3^1 + 3^2 + 3^3)。
?
結(jié)尾
?
為什么運(yùn)行期的類(lèi)型擴(kuò)展就那么多翔,而靜態(tài)類(lèi)型的擴(kuò)展就不會(huì)呢?原因是靜態(tài)類(lèi)型的擴(kuò)展是寫(xiě)在類(lèi)型的外部的。假設(shè)一下,我們的C++支持下面的寫(xiě)法:
?
bool IsCollided(switch
Geometry* first, switch
Geometry* second);
bool IsCollided(case
Sphere* first, case
Sphere* second);
bool IsCollided(case
Sphere* first, case
Cube* second);
bool IsCollided(case
Sphere* first, case
Triangles* second);
bool IsCollided(case
Cube* first, case
Sphere* second);
bool IsCollided(case
Cube* first, case
Cube* second);
bool IsCollided(case
Cube* first, case
Triangles* second);
bool IsCollided(case
Triangles* first, case
Sphere* second);
bool IsCollided(case
Triangles* first, case
Cube* second);
bool IsCollided(case
Triangles* first, case
Triangles* second);
?
最后編譯器在編譯的時(shí)候,把所有的"動(dòng)態(tài)偏特化"收集起來(lái)——就像做模板偏特化的時(shí)候一樣——然后替我們生成上面一大片翔一樣的虛函數(shù)的代碼,那該多好啊!
?
Dynamic dispatch和解耦這從一開(kāi)始以來(lái)就是一對(duì)矛盾,要徹底解決他們其實(shí)是很難的。雖然上面的作法看起來(lái)類(lèi)型和操作都解耦了,可實(shí)際上這就讓我們失去了本地代碼的dll的功能了。因?yàn)榫幾g器不可能收集到以后才動(dòng)態(tài)鏈接進(jìn)來(lái)的dll代碼里面的"動(dòng)態(tài)偏特化"的代碼對(duì)吧。不過(guò)這個(gè)問(wèn)題對(duì)于像CLR一樣基于一個(gè)VM一樣的支持JIT的runtime來(lái)講,這其實(shí)并不是個(gè)大問(wèn)題。而且Java的J2EE也好,Microsoft的Enterprise Library也好,他們的IoC(Inverse of Control)其實(shí)也是在模擬這個(gè)寫(xiě)法。我認(rèn)為以后靜態(tài)類(lèi)型語(yǔ)言的方向,肯定是朝著這個(gè)路線走的。盡管這些概念再也不能被直接map到本地代碼了,但是這讓我們從語(yǔ)義上的耦合中解放了出來(lái),對(duì)于寫(xiě)需要穩(wěn)定執(zhí)行的大型程序來(lái)說(shuō),有著莫大的助。
posted on 2013-11-10 01:06
陳梓瀚(vczh) 閱讀(10129)
評(píng)論(6) 編輯 收藏 引用 所屬分類(lèi):
啟示