大家看到這個(gè)標(biāo)題肯定會(huì)歡呼雀躍了,以為功能少的語(yǔ)言就容易學(xué)。其實(shí)完全不是這樣的。功能少的語(yǔ)言如果還適用范圍廣,那所有的概念必定是正交的,最后就會(huì)變得跟數(shù)學(xué)一樣。數(shù)學(xué)的概念很正交吧,正交的東西都特別抽象,一點(diǎn)都不直觀的。不信?出門轉(zhuǎn)左看Haskell,還有抽象代數(shù)。因此刪減語(yǔ)言的功能是需要高超的技巧的,這跟大家想的,還有跟go那幫人想的,可以斷定完全不一樣。
首先,我們要知道到底為什么需要?jiǎng)h減功能。在這里我們首先要達(dá)成一個(gè)共識(shí)——人都是很賤的。一方面在發(fā)表言論的時(shí)候光面堂皇的表示,要以需求變更和可維護(hù)性位中心;另一方面自己寫(xiě)代碼的時(shí)候又總是不惜“后來(lái)的維護(hù)者所支付的代價(jià)代價(jià)”進(jìn)行偷懶。有些時(shí)候,人就是被語(yǔ)言慣壞的,所以需要對(duì)功能進(jìn)行刪減的同時(shí),又不降低語(yǔ)言的表達(dá)能力,從而讓做不好的事情變得更難(完全不讓別人做不好的事情是不可能的),這樣大家才會(huì)傾向于寫(xiě)出結(jié)構(gòu)好的程序。
于是,語(yǔ)法糖到底是不是需要被刪減的對(duì)象呢?顯然不是。一個(gè)好的語(yǔ)言,采用的概念是正交的。盡管正交的概念可以在拼接處我們需要的概念的時(shí)候保持可維護(hù)性和解耦,但是往往這么做起來(lái)卻不是那么舒服的,所以需要語(yǔ)法糖。那如果不是語(yǔ)法糖,到底需要?jiǎng)h減什么呢?
這一集我們就來(lái)討論面向?qū)ο蟮恼Z(yǔ)言的事情,看看有什么是可以去掉的。
在面向?qū)ο髣倓偭餍衅饋?lái)的時(shí)候,大家就在討論什么搭積木編程啊、is-a、has-a這些概念啊、面向接口編程啊、為什么硬件的互相插就這么容易軟件就不行呢,然后就開(kāi)始搞什么COM啊、SOA啊這些的確讓插變得更容易,但是部署起來(lái)又很麻煩的東西。到底是什么原因造成OO沒(méi)有想象中那么好用呢?
之所以會(huì)想起這個(gè)問(wèn)題,其實(shí)是因?yàn)樽罱谖覀冄芯吭旱墓の簧铣霈F(xiàn)了一個(gè)相機(jī)的三腳架,這個(gè)太三腳架用來(lái)固定一個(gè)手機(jī)干點(diǎn)邪惡的事情,于是大家就圍繞這個(gè)事情展開(kāi)了討論——譬如說(shuō)為什么手機(jī)和三腳架是正交的,中間只要一個(gè)前凸后凹的用來(lái)插的小鐵塊就可以搞定,而軟件就不行呢?
于是我就在想,這不就是跟所謂的面向接口編程一樣,只要你全部東西都用接口,那軟件組合起來(lái)就很簡(jiǎn)單了嗎。這樣就算剛好對(duì)不上,只要寫(xiě)個(gè)adaptor,就可以搞定了。其實(shí)這種做法我們現(xiàn)在還是很常見(jiàn)的。舉個(gè)例子,有些時(shí)候我們需要Visual C++ 2013這款全球最碉堡的C++ IDE來(lái)開(kāi)發(fā)世界上最好的復(fù)雜的軟件,不過(guò)自帶的那個(gè)cl.exe實(shí)在是算不上最好的。那怎么辦,為了用一款更好的編譯器,放棄這個(gè)IDE嗎?顯然不是。正確的解決方法是,買intel的icc,然后換掉cl.exe,然后一切照舊。
其實(shí)那個(gè)面向接口編程就有點(diǎn)這個(gè)意思。有些時(shí)候一個(gè)系統(tǒng)大部分是你所需要的,別人又不能滿足,但是剛好這個(gè)系統(tǒng)的一個(gè)重要部分你手上又有更好的零件可以代替。那你是選擇更好的零件,還是選擇大部分你需要的周邊工具呢?為什么就非得二選一呢?如果大家都是面向接口編程,那你只需要跟cl.exe換成icc一樣,寫(xiě)個(gè)adaptor就可以上了。
好了,那接口是什么?其實(shí)這并沒(méi)有什么深?yuàn)W的理解,接口指的就是java和C#里面的那個(gè)interface,是很直白的。不知道為什么后來(lái)傳著傳著這條建議就跟一些封裝偶合在一起,然后各種非面向?qū)ο笳Z(yǔ)言就把自己的某些部分曲解為interface,成功地把“面向接口編程”變成了一句廢話。
不過(guò)在說(shuō)interface之前,有一個(gè)更簡(jiǎn)單但是可以類比的例子,就是函數(shù)和lambda expression了。如果一個(gè)語(yǔ)言同時(shí)存在函數(shù)和lambda expression,那么其實(shí)有一個(gè)是多余的——也就是函數(shù)了。一個(gè)函數(shù)總是可以被定義為初始化的時(shí)候給了一個(gè)lambda expression的只讀變量。這里并不存在什么性能問(wèn)題,因?yàn)檫@種典型的寫(xiě)法,編譯器往往可以識(shí)別出來(lái),最終把它優(yōu)化成一個(gè)函數(shù)。當(dāng)我們把一個(gè)函數(shù)名字當(dāng)成表達(dá)式用,獲得一個(gè)函數(shù)指針的時(shí)候,其實(shí)這個(gè)類型跟lambda expression并沒(méi)有任何區(qū)別。一個(gè)函數(shù)就只有這兩種用法,因此實(shí)際上把函數(shù)去掉,留下lambda expression,整個(gè)語(yǔ)言根本沒(méi)有發(fā)生變化。于是函數(shù)在這種情況下就屬于可以刪減的功能。
那class和interface呢?跟上面的討論類似,我主張class也是屬于可以刪減的功能之一,而且刪減了的話,程序員會(huì)因?yàn)槿祟惖谋拘远鴮?xiě)出更好的代碼。把class刪掉其實(shí)并沒(méi)有什么區(qū)別,我能想到的唯一的區(qū)別也就是class本身從此再也不是一個(gè)類型,而是一個(gè)函數(shù)了。這有關(guān)系嗎?完全沒(méi)有,你用interface就行了。
class和interface的典型區(qū)別就是,interface所有的函數(shù)都是virtual的,而且沒(méi)有局部變量。class并不是所有的函數(shù)都是virtual的——java的函數(shù)默認(rèn)virtual但是可以改,C++和C#則默認(rèn)不virtual但是可以改。就算你把所有的class的函數(shù)都改成virtual,那你也會(huì)因此留下一些狀態(tài)變量。這有什么問(wèn)題呢?假設(shè)C++編譯器是一個(gè)接口,而Visual C++和周邊的工具則是依賴于這個(gè)class所創(chuàng)造出來(lái)的東西。如果你想把cl.exe替換成icc,實(shí)際上只要new一個(gè)新的icc就可以了。而如果C++編譯器是一個(gè)class的話,你就不能替換了——就算class所有的函數(shù)都是virtual的,你也不可能給出一個(gè)規(guī)格相同而實(shí)現(xiàn)不同的icc——因?yàn)槟阋呀?jīng)被class所聲明的構(gòu)造函數(shù)、析構(gòu)函數(shù)以及寫(xiě)好的一些狀態(tài)變量(成員變量)所綁架了!
那我們可以想到的一個(gè)迫使大家都寫(xiě)出傾向于比以前更可以組合的程序,要怎么改造語(yǔ)言才可以呢?其實(shí)很簡(jiǎn)單,只需要不把class的名字看成一個(gè)類型,而把他看成一個(gè)函數(shù)就可以了。class本身有多個(gè)構(gòu)造函數(shù),其實(shí)也就是這個(gè)意思。這樣的話,所有原本要用到class的東西,我們就會(huì)去定義一個(gè)接口了。而且這個(gè)接口往往會(huì)是最小化的,因?yàn)橥耆珱](méi)有必要去聲明一些用不到的函數(shù)。
于是跟去掉函數(shù)而留下匿名函數(shù)(也就是lambda expression)類似,我們也可以去掉class而留下匿名class的。Java有匿名class,所以我們完全不會(huì)感到這個(gè)概念有多么的陌生。于是我們可以來(lái)檢查一下,這樣會(huì)不會(huì)讓我們喪失什么表達(dá)方法。
首先,是關(guān)于類的繼承。我們有四種方法來(lái)使用類的繼承。
1、類似于C#的Control繼承出Button。這完全是接口規(guī)格的繼承。我們繼承出一個(gè)Button,不是為了讓他去實(shí)現(xiàn)一個(gè)Control,而是因?yàn)锽utton比Control多出了一些新東西,而且直接體現(xiàn)在成員函數(shù)上面。因此在這個(gè)框架下,我們需要做的是IControl繼承出IButton。
2、類似于C#的TextReader繼承出StreamReader。StreamReader并不是為了給TextReader添加新功能,而是為了給TextReader指定一個(gè)來(lái)源——Stream。因此這更類似于接口和實(shí)現(xiàn)的區(qū)別。因此在這個(gè)框架下,我們需要的是用CreateStreamReader函數(shù)來(lái)創(chuàng)建一個(gè)ITextReader。
3、類似于C#的XmlNode繼承出XmlElement。這純粹是數(shù)據(jù)的繼承關(guān)系。我們之所以這么做,不是因?yàn)閏lass的用法是設(shè)計(jì)來(lái)這么用的,而是因?yàn)镃++、Java或者C#并沒(méi)有別的辦法可以讓我們來(lái)表達(dá)這些東西。在C里面我們可以用一個(gè)union加上一個(gè)enum來(lái)做,而且大家基本上都會(huì)這么做,所以我們可以看到這實(shí)際上是為了拿到一個(gè)tag來(lái)讓我們知道如何解釋那篇內(nèi)存。但是C語(yǔ)言的這種做法只有大腦永遠(yuǎn)保持清醒的人可以使用,而且我們可以看到在函數(shù)式語(yǔ)言里面,Haskell、F#和Scala都有自己的一種獨(dú)有的強(qiáng)類型的union。因此在這個(gè)框架下,我們需要做的是讓struct可以繼承,并且提供一個(gè)Nullable<T>(C#也可以寫(xiě)成T?)的類型——等價(jià)于指向struct的引用——來(lái)讓我們表達(dá)“這里是一個(gè)關(guān)于數(shù)據(jù)的union:XmlNode,他只可能是XmlElement、XmlText、XmlCData等有限幾種可能”。這完全不關(guān)class的事情。
4、在Base里面留幾個(gè)純虛函數(shù),讓Derived繼承自Base并且填補(bǔ)他們充當(dāng)回調(diào)使用——臥槽都知道是回調(diào)了為什么還要用class?設(shè)計(jì)模式幫我們準(zhǔn)備好了Template Method Pattern,我們完全可以把這幾個(gè)回調(diào)寫(xiě)在一個(gè)interface里面,讓Base的構(gòu)造函數(shù)接受這個(gè)interface,效果完全沒(méi)有區(qū)別。
因此我們可以看到,干掉class留下匿名class,根本不會(huì)對(duì)語(yǔ)言的表達(dá)能力產(chǎn)生影響。而且這讓我們可以把所有需要的依賴都從class轉(zhuǎn)成interface。interface是很好adapt的。還是用Visual C++來(lái)舉例子。我們知道cl.exe和icc都可以裝,那gcc呢?cl.exe和icc是兼容的,而gcc完全是另一套。我們只需要簡(jiǎn)單地adapt一下(盡管有可能不那么簡(jiǎn)單,但總比完全不能做強(qiáng)多了),就可以讓VC++使用gcc了。class和interface的關(guān)系也是類似的。如果class A依賴于class B,那這個(gè)依賴是綁死的。盡管class A我們很欣賞,但是由于class B實(shí)現(xiàn)得太傻比從而導(dǎo)致我們必須放棄class A這種事情簡(jiǎn)直是不能接受的。如果class A依賴于interface IB,就算他的缺省實(shí)現(xiàn)CreateB()函數(shù)我們不喜歡,我們可以自己實(shí)現(xiàn)一個(gè)CreateMyB(),從而吧我們自己的IB實(shí)現(xiàn)給class A,這樣我們又可以提供更好的B的同時(shí)不需要放棄我們很需要的A了。
不過(guò)其實(shí)每次CreateA(CreateMyB())這種事情來(lái)得到一個(gè)IA的實(shí)現(xiàn)也是很蠢得,優(yōu)點(diǎn)化神奇為腐朽的意思。不過(guò)這里就是IoC——Inverse of Control出場(chǎng)的地方了。這完全是另一個(gè)話題,而且Java和C#的一些類庫(kù)(包括我的GacUI)已經(jīng)深入的研究了IoC、正確使用了它并且發(fā)揮得淋漓盡致。這就是另一個(gè)話題了。如何用好interface,跟class是否必須是類型,沒(méi)什么關(guān)系。
但是這樣做還有一個(gè)小問(wèn)題。假設(shè)我們?cè)趯?xiě)一個(gè)UI庫(kù),定義了IControl并且有一個(gè)函數(shù)返回了一個(gè)IControl的實(shí)現(xiàn),那我們?cè)陂_(kāi)發(fā)IButton和他的實(shí)現(xiàn)的時(shí)候,要如何利用IControl的實(shí)現(xiàn)呢?本質(zhì)上來(lái)說(shuō),其實(shí)我們只需要?jiǎng)?chuàng)造一個(gè)IControl的實(shí)現(xiàn)x,然后把IButton里面所有原本屬于IControl的函數(shù)都重定向到這個(gè)x上面去,就等價(jià)于繼承了。不過(guò)這個(gè)寫(xiě)起來(lái)就很痛苦了,因此我們需要一個(gè)語(yǔ)法糖來(lái)解決它,傳說(shuō)中的Mixin就可以上場(chǎng)了。不知道Mixin?這種東西跟prototype很接近但是實(shí)際上他不是prototype,所以類似的想法經(jīng)常在javascript和ruby等動(dòng)態(tài)語(yǔ)言里面出現(xiàn)。相信大家也不會(huì)陌生。
上面基本上論證了把class換成匿名class的可能性(完全可能),和他對(duì)語(yǔ)言表達(dá)能力的影響(毫無(wú)影響),以及他對(duì)系統(tǒng)設(shè)計(jì)的好處(更容易通過(guò)人類的人性的弱點(diǎn)來(lái)引導(dǎo)我們寫(xiě)出比現(xiàn)在更加容易解耦的系統(tǒng))。盡管這不是銀彈,但顯然比現(xiàn)在的做法要強(qiáng)多了。最重要的是,因?yàn)閏lass不是一個(gè)類型,所以你沒(méi)辦法從IX強(qiáng)轉(zhuǎn)成XImpl了,于是我們只能夠設(shè)計(jì)出不需要知道到底誰(shuí)實(shí)現(xiàn)了IX的算法,可靠性迅速提高。如果IY繼承自IX的話,那IX可以強(qiáng)轉(zhuǎn)成IY就類似于COM的QueryInterface一樣,從“查看到底是誰(shuí)實(shí)現(xiàn)的”升華到了“查看這個(gè)IX是否具有IY所描述的功能”,不僅B格提高了,而且會(huì)讓你整個(gè)軟件的質(zhì)量都得到提高。
因此把class換成匿名class,讓原本正確使用OO的人更容易避免無(wú)意識(shí)的偷懶,讓原本不能正確使用OO的人迅速掌握如何正確使用OO,封死了一大堆因?yàn)橥祽卸茐馁|(zhì)量的后門,具有相當(dāng)?shù)纳鐣?huì)意義(哈哈哈哈哈哈哈哈)。
我之所以寫(xiě)這篇文章是為了告訴大家,通過(guò)刪減語(yǔ)言的功能來(lái)讓語(yǔ)言變得更好完全是可能的。但這并不意味著你能通過(guò)你自己的口味、偷懶的習(xí)慣、B格、因?yàn)橹巧痰投鴮W(xué)不會(huì)等各種奇怪的理由來(lái)衡量一個(gè)語(yǔ)言的功能是否應(yīng)該被刪除。只有冗余的東西在他帶來(lái)危害的時(shí)候,我們應(yīng)該果斷刪除它(譬如在有interface前提下的class)。而且通常我們?yōu)榱吮苊庹坏母拍钏举|(zhì)上所不可避免的增加理解難度所帶來(lái)的問(wèn)題,我們還需要相應(yīng)的往語(yǔ)言里面加入語(yǔ)法糖或者新的結(jié)構(gòu)(匿名class、強(qiáng)類型union等)。讓語(yǔ)言變得更簡(jiǎn)單從來(lái)不是我們的目標(biāo),讓語(yǔ)言變得更好用才是。而且一個(gè)語(yǔ)言不容易學(xué)會(huì)的話,我們有各種方法可以解決——譬如說(shuō)增加常見(jiàn)情況下可以解決問(wèn)題的語(yǔ)法糖、免費(fèi)分享知識(shí)、通過(guò)努力提高自己的智商(雖然有一部分人會(huì)因此感到絕望不過(guò)反正社會(huì)上有那么多職業(yè)何必非得跟死程死磕)等等有效做法。
于是在我自己設(shè)計(jì)的腳本里面,我打算全面實(shí)踐這個(gè)想法。
posted on 2013-10-19 05:51
陳梓瀚(vczh) 閱讀(9461)
評(píng)論(17) 編輯 收藏 引用 所屬分類:
啟示