組件復(fù)用技術(shù)的局限性 常聽到有人講“我寫代碼很講究,,一直嚴(yán)格遵循DRY原則,, 把重復(fù)使用的功能都封裝成可復(fù)用的組件,,使得代碼簡(jiǎn)短優(yōu)雅,同時(shí)也易于理解和維護(hù)”,。顯然,,DRY原則和組件復(fù)用技術(shù)是最常見的改善代碼質(zhì)量的方法,,不 過,,在我看來以這類方法為指導(dǎo),,能幫助我們寫出“不錯(cuò)的程序”,但還不足以幫助我們寫出簡(jiǎn)短,、優(yōu)雅,、易理解、易維護(hù)的“好程序”,。對(duì)于熟悉Martin Fowler《重構(gòu)》和GoF《設(shè)計(jì)模式》的程序員,,我常常提出這樣一個(gè)問題幫助他們進(jìn)一步加深對(duì)程序的理解:
雖然基于函數(shù),、類等形式的組件復(fù)用技術(shù)從一定程度上消除了冗余,,提升了代碼的抽象層次,但是這種技術(shù)卻有著本質(zhì)的局限性,,其根源在于 每種組件形式都代表了特定的抽象維度,,組件復(fù)用只能在其維度上進(jìn)行抽象層次的提升,。比如,我們可以把常用的HashMap等功能封裝為類庫,,但是不管怎么封裝復(fù)用類永遠(yuǎn)是類,,封裝雖然提升了代碼的抽象層次,但是它永遠(yuǎn)不會(huì)變成Lambda,,而實(shí)際問題所代表的抽象維度往往與之并不匹配,。 以常見的二進(jìn)制消息的解析為例,組件復(fù)用技術(shù)所能做到的只是把讀取字節(jié),,檢查約束,,計(jì)算CRC等功能封裝成函數(shù),這是遠(yuǎn)遠(yuǎn)不夠的,。比如,,下面的表格定義了二進(jìn)制消息X的格式: 它的解析函數(shù)大概是這個(gè)樣子:
很明顯,雖然消息X的定義非常簡(jiǎn)單,,但是它的解析函數(shù)卻顯得很繁瑣,,需要小心翼翼地處理很多細(xì)節(jié)。在處理其他消息Y時(shí),,雖然雖然Y和X很相似,,但是 卻不得不再次在解析過程中處理這些細(xì)節(jié),就是組件復(fù)用方法的局限性,,它只能幫我們按照函數(shù)或者類的語義把功能封裝成可復(fù)用的組件,,但是消息的結(jié)構(gòu)特征既不 是函數(shù)也不是類,這就是抽象維度的失配,。 程序的本質(zhì)復(fù)雜性 上面分析了組件復(fù)用技術(shù)有著根本性的局限性,,現(xiàn)在我們要進(jìn)一步思考:
回答這個(gè)問題要從程序的本質(zhì)說起,。Pascal語言之父Niklaus Wirth在70年代提出:Program = Data Structure + Algorithm,,隨后邏輯學(xué)家和計(jì)算機(jī)科學(xué)家R Kowalski進(jìn)一步提出:Algorithm = Logic + Control。誰更深刻更有啟發(fā)性,?當(dāng)然是后者,!而且我認(rèn)為數(shù)據(jù)結(jié)構(gòu)和算法都屬于控制策略,綜合二位的觀點(diǎn),,加上我自己的理解,,程序的本質(zhì) 是:Program = Logic + Control,。換句話說,程序包含了邏輯和控制兩個(gè)維度,。 邏輯就是問題的定義,,比如,對(duì)于排序問題來講,,邏輯就是“什么叫做有序,,什么叫大于,什么叫小于,,什么叫相等”,?控制就是如何合理地安排時(shí)間和空間 資源去實(shí)現(xiàn)邏輯。邏輯是程序的靈魂,,它定義了程序的本質(zhì),;控制是為邏輯服務(wù)的,是非本質(zhì)的,,可以變化的,,如同排序有幾十種不同的方法,時(shí)間空間效率各不相 同,,可以根據(jù)需要采用不同的實(shí)現(xiàn),。 程序的復(fù)雜性包含了本質(zhì)復(fù)雜性和非本質(zhì)復(fù)雜性兩個(gè)方面。套用這里的術(shù)語,, 程序的本質(zhì)復(fù)雜性就是邏輯,,非本質(zhì)復(fù)雜性就是控制。邏輯決定了代碼復(fù)雜性的下限,,也就是說不管怎么做代碼優(yōu)化,,Office程序永遠(yuǎn)比Notepad程序復(fù)雜,這是因?yàn)榍罢叩倪壿嬀透鼮閺?fù)雜,。如果要代碼簡(jiǎn)潔優(yōu)雅,,任何語言和技術(shù)所能做的只是盡量接近這個(gè)本質(zhì)復(fù)雜性,而不可能超越這個(gè)理論下限,。 理解”程序的本質(zhì)復(fù)雜性是由邏輯決定的”從理論上為我們指明了代碼優(yōu)化的方向:讓邏輯和控制這兩個(gè)維度保持正交關(guān)系,。來看Java的Collections.sort方法的例子:
使用者只關(guān)心邏輯部份,即提供一個(gè)Comparator對(duì)象表明序在類型T上的定義,;控制的部分完全交給方法實(shí)現(xiàn)者,,可以有多種不同的實(shí)現(xiàn),這就是 邏輯和控制解耦,。同時(shí),,我們也可以斷定,這個(gè)設(shè)計(jì)已經(jīng)達(dá)到了代碼優(yōu)化的理論極限,,不會(huì)有本質(zhì)上比它更簡(jiǎn)潔的設(shè)計(jì)(忽略相同語義的語法差異),,為什么?因?yàn)?邏輯決定了它的本質(zhì)復(fù)雜度,,Comparator和Collections.sort的定義完全是邏輯的體現(xiàn),,不包含任何非本質(zhì)的控制部分。 另外需要強(qiáng)調(diào)的是,,上面講的“控制是非本質(zhì)復(fù)雜性”并不是說控制不重要,,控制往往直接決定了程序的性能,當(dāng)我們因?yàn)樾阅艿仍虮仨毑捎媚撤N控制的時(shí) 候,,實(shí)際上被固化的控制策略也是一種邏輯,。比如,當(dāng)你的需求是“從進(jìn)程虛擬地址ptr1拷貝1024個(gè)字節(jié)到地址ptr2“,,那么它就是問題的定義,,它就 是邏輯,這時(shí),,提供進(jìn)程虛擬地址直接訪問語義的底層語言就與之完全匹配,,反而是更高層次的語言對(duì)這個(gè)需求無能為力。 介紹了邏輯和控制的關(guān)系,,可能很多朋友已經(jīng)開始意識(shí)到了上面二進(jìn)制文件解析實(shí)現(xiàn)的問題在哪里,,其實(shí)這也是 絕大多數(shù)程序不夠簡(jiǎn)潔優(yōu)雅的根本原因:邏輯與控制耦合。上面那個(gè)消息定義表格就是不包含控制的純邏輯,,我相信即使不是程序員也能讀懂它,;而相應(yīng)的代碼把邏輯和控制攪在一起之后就不那么容易讀懂了。 熟悉OOP和GoF設(shè)計(jì)模式的朋友可能會(huì)把“邏輯與控制解耦”與經(jīng)常聽說的“接口和實(shí)現(xiàn)解耦”聯(lián)系在一起,,他們是不是一回事呢,?其實(shí),把這里所說的 邏輯和OOP中的接口劃等號(hào)是似是而非的,, 而GoF設(shè)計(jì)模式最大的問題就在于有意無意地讓人們以為“what就是interface, interface就是what”,,很多朋友一想到要表達(dá)what,要抽象,,馬上寫個(gè)接口出來,,這就是潛移默化的慣性思維,自己根本意識(shí)不到問題在哪里,。 其實(shí),,接口和前面提到的組件復(fù)用技術(shù)一樣,同樣受限于特定的抽象維度,,它不是表達(dá)邏輯的通用方法,,比如,我們無法把二進(jìn)制文件格式特征用接口來表示。 另外,,我們熟悉的許多GoF模式以“邏輯與控制解耦”的觀點(diǎn)來看,,都不是最優(yōu)的。比如,,很多時(shí)候Observer模式都是典型的以控制代邏輯,,來看一個(gè)例子:
基于Observer模式的實(shí)現(xiàn)是這樣的:
而基于純CSS的實(shí)現(xiàn)是這樣的:
通過對(duì)比,,您看出二者的差別了嗎,?顯然,Observer模式包含了非本質(zhì)的控制,,而CSS是只包含邏輯,。理論上講,CSS能做的事情,,JavaScript都能通過控制做到,,那么為什么瀏覽器的設(shè)計(jì)者要引入CSS呢,這對(duì)我們有何啟發(fā)呢,? 元語言抽象 好的,,我們繼續(xù)思考下面這個(gè)問題:
答案是:有!這就是元(Meta),,包括元語言(Meta Language)和元數(shù)據(jù)(Meta Data)兩個(gè)方面,。元并不神秘,我們通常所說的配置就是元,,元語言就是配置的語法和語義,,元數(shù)據(jù)就是具體的配置,它們之間的關(guān)系就是C語言和C程序之間 的關(guān)系,;但是,,同時(shí)元又非常神奇,因?yàn)樵仁菙?shù)據(jù)也是代碼,,在表達(dá)邏輯和語義方面具有無與倫比的靈活性,。至此,我們終于找到了讓代碼變得簡(jiǎn)潔,、優(yōu)雅,、易理 解、易維護(hù)的終極方法,這就是: 通過元語言抽象讓邏輯和控制徹底解耦,! 比如,,對(duì)于二進(jìn)制消息解析,經(jīng)典的做法是類似Google的Protocol Buffers,, 把消息結(jié)構(gòu)特征抽象出來,,定義消息描述元語言,,再通過元數(shù)據(jù)描述消息結(jié)構(gòu),。下面是Protocol Buffers元數(shù)據(jù)的例子,這個(gè)元數(shù)據(jù)是純邏輯的表達(dá),,它的復(fù)雜度體現(xiàn)的是消息結(jié)構(gòu)的本質(zhì)復(fù)雜度,,而如何序列化和解析這些控制相關(guān)的部分被 Protocol Buffers編譯器隱藏起來了。
元語言解決了邏輯表達(dá)問題,,但是最終要與控制相結(jié)合成為具體實(shí)現(xiàn),,這就是元語言到目標(biāo)語言的映射問題。通常有這兩種方法: 1) 元編程(Meta Programming),,開發(fā)從元語言到目標(biāo)語言的編譯器,,將元數(shù)據(jù)編譯為目標(biāo)程序代碼; 2) 元驅(qū)動(dòng)編程(Meta Driven Programming),,直接在目標(biāo)語言中實(shí)現(xiàn)元語言的解釋器,。 這兩種方法各有優(yōu)勢(shì),元編程由于有靜態(tài)編譯階段,,一般產(chǎn)生的目標(biāo)程序代碼性能更好,,但是這種方式混合了兩個(gè)層次的代碼,增加了代碼配置管理的難度,, 一般還需要同時(shí)配備Build腳本把整個(gè)代碼生成自動(dòng)集成到Build過程中,,此外,和IDE的集成也是問題,;元驅(qū)動(dòng)編程則相反,,沒有靜態(tài)編譯過程,元語 言代碼是動(dòng)態(tài)解析的,,所以性能上有損失,,但是更加靈活,開發(fā)和代碼配置管理的難度也更小,。除非是性能要求非常高的場(chǎng)合,,我推薦的是元驅(qū)動(dòng)編程,因?yàn)樗p 量,,更易于與目標(biāo)語言結(jié)合,。 下面是用元驅(qū)動(dòng)編程解決二進(jìn)制消息解析問題的例子,meta_message_x是元數(shù)據(jù),parse_message是解釋器:
這段代碼我用的是JavaScript語法,,因?yàn)閷?duì)于支持Literal的類似JSON對(duì)象表示的語言中,,實(shí)現(xiàn)元驅(qū)動(dòng)編程最為簡(jiǎn)單。如果是Java 或C++語言,,語法上稍微繁瑣一點(diǎn),,不過本質(zhì)上是一樣的,或者引入JSON配置文件,,然后解析配置,,或者定義MessageConfig類,直接把這個(gè)類 對(duì)象作為配置信息,。 二進(jìn)制文件解析問題是一個(gè)經(jīng)典問題,,有Protocol Buffers、Android AIDL等大量的實(shí)例,,所以很多人能想到引入消息定義元語言,,但是如果我們把問題稍微變換,能想到采用這種方法的人就不多了,。來看下面這個(gè)問題:
普通的實(shí)現(xiàn)是這個(gè)樣子的:
上面的實(shí)現(xiàn)就是按照組建復(fù)用的思想封裝了一下檢測(cè)email格式之類的通用函數(shù),,這和剛才的二進(jìn)制消息解析非常相似,,沒法在不同的表單之間進(jìn)行大規(guī)模復(fù)用,很多細(xì)節(jié)都必須被重復(fù)編寫,。下面是用元語言抽象改進(jìn)后的做法:
過定義表單屬性元語言,,整個(gè)邏輯頓時(shí)清晰了,細(xì)節(jié)的處理只需要在check_form中編寫一次,,完全實(shí)現(xiàn)了“簡(jiǎn)短,、優(yōu)雅、易理解,、以維護(hù)”的目 標(biāo),。其實(shí),,不僅Web表單驗(yàn)證可以通過元語言描述,整個(gè)Web頁面從布局到功能全部都可以通過一個(gè)元對(duì)象描述,,完全將邏輯和控制解耦,。此外,我編寫的用于 解析命令行參數(shù)的lineparser.js庫也是基于元語言的,,有興趣的朋友可以參考并對(duì)比它和其他命令行解析庫的設(shè)計(jì)差異,。 最后,我們?cè)賮韽拇a長(zhǎng)度的角度來分析一下元驅(qū)動(dòng)編程和普通方法之間的差異,。假設(shè)一個(gè)功能在系統(tǒng)中出現(xiàn)了n次,,對(duì)于普通方法來講,由于邏輯和控制的 耦合,,它的代碼量是n * (L + C),,而元驅(qū)動(dòng)編程只需要實(shí)現(xiàn)一次控制,代碼長(zhǎng)度是C + n * L,,其中L表示邏輯相關(guān)的代碼量,C表示控制相關(guān)的代碼量,。通常情況下L部分都是一些配置,,不容易引入bug,復(fù)雜的主要是C的部分,,普通方法中C被重復(fù) 了n次,,引入bug的可能性大大增加,同時(shí)修改一個(gè)bug也可能要改n個(gè)地方,。所以,,對(duì)于重復(fù)出現(xiàn)的功能,元驅(qū)動(dòng)編程大大減少了代碼量,,減小了引入bug 的可能,,并且提高了可維護(hù)性。 總結(jié) 《人月神話》的作者Fred Brooks曾在80年代闡述了它對(duì)于軟件復(fù)雜性的看法,,即著名的No Silver Bullet,。他認(rèn)為不存在一種技術(shù)能使得軟件開發(fā)在生產(chǎn)力、可靠性,、簡(jiǎn)潔性方面提高一個(gè)數(shù)量級(jí),。我不清楚Brooks這一論斷詳細(xì)的背景,但是就個(gè)人的開發(fā)經(jīng)驗(yàn)而言,,元驅(qū)動(dòng)編程和普通編程方法相比在生產(chǎn)力,、可靠性和簡(jiǎn)潔性方面的確是數(shù)量級(jí)的提升,在我看來它就是軟件開發(fā)的銀彈! |
|