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