[譯者注]前不久翻譯了Eric Raymond對幾大開發語言的評價,
引起了網友的熱烈討論。其中涉及到Eric Raymond對OO的批
評,引起大家的爭議。為此我再翻譯他的一段相關文字,請
大家閱讀思考。
模組化 —— Keep it clean, keep it simple
程式員所面對的複雜性日益增大,而劃分代碼的方法也有一個自然的發展過程。一開始,軟體不過是一大塊機器代碼。最早的過程化語言帶來了“依據子常式劃分代碼”的觀念,接下來我們發明了程式庫,為不同程式提供公用服務。再下來我們發明了獨立地址空間和處理序間通訊。如今,我們已經慣常於把程式系統分布在彼此相隔萬裡的互聯主機上。
Unix的早期開發人員同時也是軟體模組化思想的先鋒。在他們之前,模組化只是電腦科學的思想,還不是工程實踐。其弘旨是:要開發可正確工作的複雜軟體的唯一途徑,就是用定義良好的介面把諸多簡單模組連結起來,形成整個系統。只有如此,大部分局部問題才可能在局部得到修正或者最佳化,而不至於破壞整體。
今天所有的程式員都被教育要在子程式的層次上進行模組化。有些幸運者掌握了在抽象資料類型(ADT)層次上的模組化能力,就已經被認為是好的設計者了。如今的設計模式運動,就是希望把這個層次再提高一步,發現那些有助於對程式大型結構進行良好組織的成功的設計抽象。
封裝和最佳模組體積
模組代碼的第一重要特質乃封裝。封裝良好的模組決不互相暴露內部資訊。它們不去窺測其他模組的實現,不胡亂地共用全域資料,而是通過API互相通訊。
模組之間的API有雙重身份。在實現層次,API函數扼守模組之間的連接點,阻止內部資訊外泄。在設計層次,API實際上定義了你的系統架構。
模組分解越細緻,模組越小,API的定義越重要。整體的複雜度、錯誤的可能性也隨之降低。
然而,如果分解過度,導致過小的模組,會得到意想不到的情況。下面的圖來自Hatton 1997年的統計資料。可見到圖形是U形的。
Hatton的資料是在不同語言和不同平台上經過廣泛統計的道德,故具有可信性。可見,代碼量在200到400邏輯行之間的模組效果最佳。
緊湊性和正交性
代碼並不是唯一具有所謂“最佳單塊體積”的軟體要素。語言和APIs同樣受到人類認識能力的限制而逃不出Hatton的U曲線。
因此,Unix程式員在設計APIs、命令集、協議以及其他東西的艱苦思索過程中發現了模組化的兩個要素:緊密性和正交性。
緊湊性
緊湊性是指設計對於人腦而言“易於理解和接受的程度”。
緊湊的軟體工具跟順手的日常手工工具一樣擁有很多優點。它讓人們樂於使用,用起來方便自然,大大提高你的效率,而且安全可靠,不像那些複雜的工具,動不動就弄傷你。
緊湊並不意味著功能弱,如果一個設計建築在容易理解的抽象基礎之上,那麼它可以非常強大和靈活,同時又保持緊湊。緊湊也不意味著容易學習,你可能必須先理解抽象之下精緻的概念性模型,之後才能感到容易。
軟體很少有絕對緊湊的,但是很多軟體是相對緊湊的,它們有一個緊湊的工作集,一個功能子集,可以滿足80%甚至更多的專家級使用者的日常需求。
舉例來說,Unix系統調用API就是緊湊的,而C標準庫則不是。Unix工具中make(1)是緊湊的,autoconf(1)和automake(1)則不是。標記語言中,HTML是緊湊的,而DocBook不是。Man(7)是緊湊的,troff(1)不是。通用語言中,C和Python是緊湊的,Perl, Java, Emacs Lisp和Shell不是。C++則是“反緊湊”的——該語言的設計者自己都承認,他不指望有人能夠完全理解C++。
不過也不是說不緊湊的設計就是邪惡的或者糟糕的。有些問題域太複雜,緊湊的設計無法實現。這裡強調緊湊性,其目的並不是希望讀者把它看成是一個絕對要求,而是像Unix程式員那樣,合理對待,儘力實踐,決不輕易放棄。
正交性
正交性有助於你將複雜設計緊湊化,在這一點上,它的重要性非常突出。在純正交的設計中,操作沒有副作用,每一個行動只改變一件事情,不影響其它東西。對於系統中的每一個屬性,有且只有一條途徑去改變它。
電腦監視器是正交性的良好範例,你可以調明暗而不影響飽和度,色彩平衡的控制也彼此獨立。如果不是如此,想象一下你會遇到多大麻煩!
軟體中的非正交性設計太多了。舉個例子,格式轉換函式的作者經常會不假思索地要一個源檔案的路徑名作為參數,可是輸入經常來自一個開啟的檔案控制代碼,如果設計成正交的,則該函數不應該有“開啟檔案”的副作用,則將來這個函數可以處理來自各種源頭的資料流。
Doug McIloy的名言“只做好一件事”經常被認為是關於簡單性的箴言,而其實這句話裡對於正交性的強調至少是同樣分量的。
非正交性的主要問題是副作用擾亂了程式員和使用者的思維模型,而且經常被遺忘,帶來程度不同的災難。
Unix API整體上是一個很好的正交設計範例,正因為如此,在其他平台上的C庫都儘力模仿它。所以就算你不是Unix程式員,也值得研究它。
SPOT法則
The Pragmatic Programmer 一書中指出了一類特別重要的正交性,“Don’t Repeat Yourself”法則:任何知識點應當是唯一的,無歧義的,在系統中以確定無疑的方式存在的。在本書裡,我遵循Brain Kernighan的建議,把這個法則稱為Single Point Of Truth,或簡稱SPOT法則。
重複導致不一致,對代碼構成潛在的危害。因為如果你要改變重複資訊中的一個,就必須記得改變它所有的化身。這體現出你根本沒有清晰地組織你的代碼。
軟體是多層的
寬泛地說,當你在設計函數或者對象階層(hierarchy)時,有兩個方向可供選擇,而你的選擇對於代碼的分層(layering)將有重大的影響。
自頂向下,自底向上
一個方向是自下而上,從問題域中一定會用到具體的操作出發向上——從具體到抽象。舉個例子,如果你要為磁碟機開發一個韌體(firmware),則在低層可以有一些操作原語如“磁頭移至某物理塊”,“讀物理快”,“寫物理快”,“切換LED”等。
另一個方向是自上而下,從抽象到具體,從最頂層的程式或者邏輯整體描述規範出發向下到個別的操作。比如某人設計一個可以控制不同介質的大型存放控制器,可以從抽象的操作出發,比如“定址邏輯塊”,“讀邏輯塊”,“寫邏輯塊”,“切換指示裝置”。這上面所說的硬體層次的操作很不相同,
一個大一些的例子是Web瀏覽器。自頂向下的設計從一個規範說明出發——能接受哪些URL類型,能顯示哪些映像,對Java和JavaScript支援如何,等等。與這個自頂向下的視圖相對應的實現層是應用的主事件迴圈。
同時Web瀏覽器必須要調用大量的專用元操作(primitives)。比如建立網路連接,發送資料,接受響應,比如GUI相關的操作,比如HTML解析操作。
從哪端開始,這事關重大,因為你的起點很可能對你的終點構成了限制。如果你完全的自頂向下,到一定時候你可能會尷尬地發現,邏輯上所需要的元操作實際上不能完全實現。如果你完全的自低向上,你會發現自己做了大量與程式無關的事情。
從1960年代起,初級程式員們就被教導說,寫程式應該“自頂向下,逐漸細分”。自頂向下在下面三個條件成立的時候,是很好的經驗:a. 你可以事先經確定義程式的需求,b. 在實現過程中,該規範不大可能變化,c. 在最底層,你有充分的自由來選擇完成工作的方式。
程式層次越高,這些條件越容易被滿足。然而,即使在最高層次的應用程式開發中,這些條件仍然經常不成立。
出入自我保護,程式員試圖雙管齊下。一方面以自頂向下的應用邏輯表達抽象規範,另一方面用函數和庫來歸納領域內的元操作,在高層設計發生變化時可以複用之。
Unix程式員主要做系統程式設計,所以傾向於自底向上的開發方式。
一般來說,自底向上的開發更有吸引力,它使你以一種探索的方式開發,給你相對充裕的時間去細化含糊的規範,也更加符合程式員天生的懶惰——一旦出錯,報廢的代碼通常要少得多。
不過實際的代碼一般是自頂向下和自底向上向結合的。兩者經常在一個項目中運用,這直接導致了膠合層(glue layer)的出現。
膠合層
當自頂向下和自底向上的汽車撞在一起的時候,情形通常是一片混亂。頂層的應用邏輯和底層的元操作必須由膠合層來阻隔。
幾十年來,Unix程式員明白了一個道理,膠合層是令人厭惡的東西,應該讓粘結層越薄越好,此乃性命攸關之大事!膠合層應該用來把東西粘在一起,而不是用來掩蓋層與層之間的衝突和裂痕。
拿上面那個瀏覽器的例子來說,粘結層包括:把由HTML解析而來的文檔對象應設為顯示緩衝區裡的位元影像。這部分代碼是聲名狼藉的難寫,錯誤百出。HTML解析和GUI庫的錯誤和缺陷都會在這層裡表現出來。
瀏覽器的膠合層不僅要在規範和元操作之間充當中介者,還要在若干不同的外部規範中間充當中介者——HTTP網路通訊協定的行為,HTML文檔結構,不同的圖形和多媒體格式,以及來自GUI的使用者行為。
一層膠合層已經很容易出錯了,但這還不是最糟糕的。如果一個設計者意識到膠合層的存在,並且試圖去用自己的一套資料結構或者對象把這個膠合層組織到一個中介層中,那麼結果就會是多出兩個膠合層——一個在那個中介層之上,一個在其下。那些聰明但卻欠缺曆練的程式員經常積極地跳到這個陷阱裡去。他們把基本的類(應用邏輯,中層和元操作)做得像課本上的例子那樣漂亮,最後卻為了把這些漂亮的代碼粘合到一起而在很多個越來越厚的膠合層中忙得團團轉,直到困死。
C語言本身被認為是薄膠合層的良好範例。
Unix和物件導向語言
自1980年代中期開始,新的語言紛紛宣稱自己對物件導向編程提供直接支援。
OO設計的概念首先在圖形系統,GUI系統和模擬系統裡被證明是很有價值的。然而曆史證明,在這些領域之外,OO並沒有帶來明顯的益處,這令很多人感到吃驚,感到幻滅。應該試圖去理解其中的道理,這將會是很有意義的事情。
在Unix傳統的模組化技術與圍繞OO語言發展起來的模式之間,存在著一些衝突和張力。Unix程式員較之其他人對於OO抱有更大的懷疑態度。原因之一是多樣性法則。OO被說成是軟體複雜性問題唯一正確的解決之道,這未免令人生疑。不過,還有更深層的原因。
我們剛才提到,Unix的模組化傳統中,薄膠合層是一個重要原則,從頂層程式對象到下層硬體之間的抽象層越少越好。
這部分是因為C的影響。在C中間類比真正的對象是件很費力的工作。因此,疊置一大堆抽象層簡直是要人老命的事情。因此,在C中的對象層次傾向於平坦和透明。長此以往,Unix程式員使用其他語言也習慣於薄粘接/淺層次。
OO語言使得抽象變得容易了——也許是太容易了。它鼓勵整個架構具有厚厚的、精緻的膠合層。如果問題域確實複雜,確實需要大量的抽象,這可能是好事。但是這也是很糟糕的事,因為程式員最後會把很簡單的事情用很複雜的辦法來做,僅僅因為他們可以這麼做。
所有的OO語言都有有一些傾向,吸引程式員跳進“過度分層”的陷阱裡。對象架構和物件瀏覽器並不能取代好的設計和文檔,但是卻經常被看成一回事。太多的層次破壞了透明性——我們很難看穿下面的東西,很難在思想上對於代碼的功能建立清晰的模型。簡單性、明晰性和透明性一口氣全被破壞了,結果代碼充滿了晦澀的錯誤,帶來嚴重的維護性問題。
這種情況還在繼續惡化,很多培訓班把厚厚的軟體分層當成好東西傳授——你擁有那一大堆類被認為是資料中所潛藏的知識的體現。問題在於,在膠合層中的“smart data”經常與程式所操作的自然實體無關,而僅僅只是膠合本身。(一個確切的標誌就是抽象子類的不斷增值,以及所謂的“minxins”。)
Unix程式員對這些問題有本能的直覺。Unix中OO語言沒有能夠替代非OO語言如C,Perl(雖然支援OO,但很少有人用到),Shell等,這大概是原因之一。Unix世界裡對於OO的批評比別的領域中要尖刻得多。Unix程式員知道什麼時候不應該用OO,就算是要用OO,他們也儘可能的保持對象設計的簡潔。正如Michael Padlipsky所說:“如果你知道你在幹什麼,三層足夠;如果你不知道你在幹什麼,十七層也沒用。”
OO在GUI、模擬和圖形領域裡取得成功的原因,可能是因為在這些領域中,相對而言,比較容易解決“類型存在與否”的問題。例如,在GUI和圖形系統中,類和可視對象之間存在著自然的映射關係。如果你發現自己所增加的類並不直接映射可視對象,則你也可能就會發現膠合層已經變得很厚。