第2章 是懶人造就了方法
“僰 道有蜀王兵蘭 ,亦有神作大灘江中。其崖嶄峻不可破,(冰)乃積薪燒之。”
——《華陽國志》
1. 是懶人造就了方法
戰國時期的李冰鑿了一座山。
史記中說是“蜀守冰鑿離堆”,是說李冰在成都的時候鑿出了離堆。一說是李冰將都江堰附近的玉壘山鑿了一個大口子,叫寶瓶口,而鑿的石頭就堆成了離堆。另一說,則是李的確是鑿了一座“(溷)崖”,但是是在沫水,亦即是今天的大渡河。
在哪里鑿的山,是史學家都說不清楚的事。但的確鑿了一座山,而且方法是“(因)其崖嶄峻不可破,(冰)乃積薪燒之”。
我們已經看到事物的進化了。同是戰國時代,《列子·湯問篇》里的愚公就要“碎石擊壤”,而李冰就已經懂得“積薪燒之”了。
會有人說愚公是“碎石”,并沒有說他“碎石”的方法究竟是“斧鉞以鑿之”,還是“積薪以燒之”。但想想那個時代,如果有人懂得了燒石頭這個方法,哪能不立即載文志之,永世傳承。
再說了,愚公嘛。愚者怎么會呢?這還需要分析嗎?需要嗎?
所以愚公會鑿,而李冰會燒。那李冰又是為什么會用“燒”這種方法來碎石的呢?如果李冰也象愚公那樣日復一日地督促著他的團隊鑿石開山,那他一定沒有時間來學習、尋找或者觀察,當然也不會發現“燒”這種方法可以加快工程進度,使得一大座山短時間就被嘩啦嘩啦地給“碎”掉了。
要知道李冰的團隊可是成百上千人,要修堰筑壩,還要“鑿離堆”,當然還要吃喝啦撒睡。所以李冰如果忙起來的話,他必然是“受命以來,夙夜憂嘆”,必然食難下咽,睡無安枕。反之,李冰一定是個閑人,可以閑到沒事去看火能不能把石頭燒爆。
這么大個工程里,如果有一個人會閑到看火燒石頭,那他一定很懶。那么多事堆著不去做,去看燒石頭,你說他不是懶是什么。
正是一個懶人造就了“燒石頭”這個“碎石”的方法。愚公太勤快了,勤快得今天可以比昨天多鑿一倍的石頭。或者在愚公的項目計劃案的首頁里就寫著朱筆大字:“吾今勝昨倍許,明勝今倍許,而山不加增,何苦而不快。”
但是越發的勤快,愚公將越發沒有機會找到更快的方法, 人的精力終歸是有極限的。提出新的“方法”,解決的將是影響做事成效的根本問題。而愚公可以多吃點飯,多加點班,但突破不了人的精力的極限。
記住,在兩千年前的某一天,閑極無聊的李冰下廚給夫人炒了一個小菜,他突然發現壘灶的鵝卵石被燒得爆裂開來,遇水尤甚。從此《史記》上記下了“蜀守冰鑿離堆”,而《華陽國志》上記下了他做這件事的方法“積薪燒之”。
在差不多同一時間,愚公在山北之塞“碎石擊壤”。
2. 一百萬行代碼是可以寫在一個文件里的
早期寫程序,都是將代碼打在穿孔紙帶上,讓計算機去讀的。要讓計算機讀的紙帶當然是連續的,這無需多講。其實我也沒有那樣寫過程序,真實的苦楚我也不知道。
后來有了匯編語言,可以寫一些代碼了。這時的代碼是寫在文本文件里,然后交給一個編譯器去編譯,再由一個鏈接器去鏈接,這樣就出來了程序。
第一個寫匯編的人,可能寫的是有名的“Hello World”程序,那個程序寫在一個文件里就行了。所以后來就成了習慣,大家都把代碼寫到一個文件里。早期的匯編語言里,GOTO 語句是用得非常非常頻繁的,將一個語句 GOTO到另一個文本文件里去,既不現實也不方便。所以大家習以為常,便統統地把代碼寫到一個文件里。
再后來出了高級語言,什么 C 呀,Pascal 呀之類的。既然大家已經形成習慣了,所以很自然地會把一個程序寫到一個文件里。無論這個程序有多大,多少行代碼,寫到一個文件里多方便呀。
直到如今語言發展得更高級了。可是程序員的習慣還是難改,一旦得了機會,還是喜歡把代碼寫到一個文件里的。
好了,有人說我是想當然爾。En,這當然是有實據的。記得 Delphi 1.0 版發布的時候,全世界一片叫好聲。連“不支持雙字節”這樣的大問題,都不影響他在華語地區的推廣。然而不久,爆出了一個大 BUG!什么大 BUG 呢?Delphi 1.0 的編譯器居然不支持超過 64K 的源代碼文件!
這被 Fans 們一通好罵。直到我用 Delphi 2.0 時,一個從 VB 陣營轉過來的程序員還跑過來問我這件事。好在Delphi 2.0 改了這個 BUG,這讓當時我的面子上好一陣風光。
64k 的文件是什么概念呢?
1 行代碼大概(平均)是 30 字節,64k 的源代碼是 2184行,如果代碼風格好一點,再多一些空行的話,差不多也就是 3000 行上下。
也就是說,在 Delphi 1 的時代(以及其后的很多很多時代),程序員把 3000 行代碼寫到一個文件里,是司空見慣的事了。如果你不讓他這樣寫,還是會被痛罵的呢。
所以呢,按照這一部分人的邏輯,一百萬行代碼其實是可以寫在一個文件里的。不單可以,而且編譯器、編輯器等等也都必須要支持。這才是正統的軟件開發。
勤快的愚公創造不了方法。這我已經說過了。對于要把“一百萬行代碼寫到一個文件”,查找一個函數要在編輯器里按五千次 PageDown/PageUp 鍵的勤快人來說,是不能指望他們創造出“單元文件(Unit)”這樣的開發方法來的。
然而單元文件畢竟還是出現了。這個世界上,有勤快人就必然有懶人,有懶人也就必然有懶人的懶方法。
有了單元文件,也就很快出現了一個新的概念:模塊。把一個大模塊分成小模塊,再把小模塊分成更細的小小模塊,一個模塊對應于一個單元。于是我們可以開始分工作了,一部分人寫這幾個單元的代碼,另一部分則寫那幾個。
很好,終于可以讓源代碼分散開來。結構化編程的時代終于開始了,新的方法取代了舊的方法,而這一切的功勞,是要歸終于那個在按第 5001 次PageDown鍵時,突然崩潰的程序師。他發自良心地說:不能讓這一切繼續下去了,我一定要把下一行代碼寫到第二個文件里去。我發誓,我要在編譯器里加入一個Unit關鍵字。①
3. 你桌上的書是亂的嗎?
幾周之前,在一所電腦培訓學校與學生座談時,一個學員問我:“為什么我學了一年的編程,卻還是不知道怎么寫程序呢”。
我想了想,問了這個學員一個問題:“你桌上的書是亂的嗎?”
他遲疑了一下,不過還是回答我道:“比較整齊。”
我當時便反問他:“你既然知道如何把書分類、歸整得整整齊齊地放在書桌,那怎么沒想過如何把所學的知道分類一下,歸納一下,整整齊齊地放在腦子里呢?”
如果一個人學了一年的編程,他的腦袋里還是昏乎乎的,不知道從哪里開始,也不知道如何做程序。那想來只有一個原因:他學了,也把知識學進去了,就是不知道這些知識是干什么的。或者說,他不知道各種知識都可以用來做什么。
① Turbo Pascal 3.0 中才開始有了Uses和Unit關鍵字。在ANSI Pascal 標準里并沒有它。
其實結構化編程的基本單位是“過程(Procedure)”,而不是上一小節說到的“單元(Unit)”。然而在我看來,過程及其調用是 CPU 指令集所提供的執行邏輯,而不是普通的開發人員在編程實踐中所總結和創生的“方法”。
這里要提及到CPU指令集的產生。產生最初的指令集的方式我已經不可考證,我所知道的是CISC指令集與RISC指令集之爭在 1979 年終于爆發。前者被稱為復雜指令集,然而經過Patterson等科學家的研究,發現 80%的CISC指令只有在 20%的時間內才會用到;更進一步的研究發現,在最常用的 10 條指令中,包含的流程控制只有“條件分支(IF...THEN...)①”、“跳轉(JUMP)” 和“調用返回(CALL/RET)”……
于是 CISC 被 RISC(精簡指令集計算機)替代了。動搖CISC 指令集地位的方法,就是分類統計。
正如 CISC 指令集攪亂了一代程序設計師的思路一樣,大量的知識和資訊攪亂了上面給我提問的那位學員的思想。他應該嘗試一下分類,把既有的知識象桌子上的書一樣整理一下,最常用的放在手邊,而最不常用的放在書柜里。如果這樣的話,我想他已經在九個月前就開始寫第一個軟件產品了。
① 在x86 系統中,循環是用條件分支來實現的,而且條件分支指令 并不是IF...THEN...,這里用這兩個關鍵字,僅用于說明問題。
你桌上的書還是亂的嗎?
4. 我的第一次思考:程序 = 算法 + 結構 + 方法
我的第一次關于程序的本質的思考其實發生在不久前。那是我在 OICQ 上與 Soul 的一次談話。
Soul(王昊)是DelphiBBS現任的總版主,是我很敬重的一位程序員。那時我們正在做DelphiBBS的一個“B計劃II”,也就是出第二本書。他當時在寫一篇有關“面向對象(OOP)”的文章。而我正在寫《Delphi源代碼分析》,在初期的版本里,有“面向對象”這一部分的內容。我們的對話摘要如下①:
Soul:我在寫書討論“面向對象的局限性”
我 :En.這個倒與我的意見一致。哈哈哈。
我 :“絕對可以用面向過程的方法來實現任意復雜的系統。要知道,航天飛機也是在面向過程的時代上的天。但是,為了使一切變得不是那么復雜,還是出現了‘面向對象程序設計’的方法。”
我 :——哈,我那本書里,在“面向對象”一部分前的引文中。就是這樣寫的。
① 這段對話的確很長。如果你不是非常有經驗的程序員,那么不能完整地閱讀和理解這段文字是很正常的。部分讀者甚至可以跳過這段引文,直接閱讀后面的結論。而有興趣的讀者,可以在我的網站上讀到它的全文(http://www.doany.net/)。
Soul:現在的程序是按照馮。諾伊曼的第一種方案做的,本來就是順序的,而不是同步的。CPU怎么說都是一條指令一條指令執行的。
我 :面向過程是對“流程”、“結構”和“編程方法”的高度概括。而面向對象本身只解決了“結構”和“編程方法”的問題,而并沒有對“流程”加以改造。
Soul:確實如此。確實如此。
我 :對流程進一步概括的,是“事件驅動”程序模型。而這個模型不是OO提出的,而是Windows的消息系統內置的。所以,現在很多人迷惑于“對象”和“事件”,試圖通過OO來解決一切的想法原本就是很可笑的。
Soul:我先停下來,和你討論這個問題,順便補充到書里去。
我 :如果要了解事件驅動的本質,就應該追溯到Windows內核。這樣就
涉及到線程、進程和窗體消息系統這些與OO無關的內容。所以,整
個RAD的編程模型是OO與OS一起構建的。現在很多的開發人員只知
其OO的外表,而看不到OS的內核,所以也就總是難以提高。
Soul:OO里面我覺得事件的概念是很牽強的,因為真正的對象之間是相互
作用,就好像作用力和反作用力,不會有個“順序”的延時。
我 :應該留意到,整個的“事件”模型都是以“記錄”和“消息”的方
式來傳遞的。也就是說,事件模型停留在“面向過程”編程時代使
用的“數據結構”的層面上。因此,也就不難明白,使用/不使用
OO都能寫Windows程序。
我 :因為流程還是在“面向過程”時代。
Soul:所以所謂的面向對象的事件還是“順序”的。所以我們經常要考慮
一個事件發生后對其他過程的影響,所以面向對象現在而言是牽強
的。
我 :如果你深入OS來看SEH,來看Messages,就知道這些東西原本就不
是為了“面向對象”而準備的。面向對象封裝了這些,卻無法改造
它們的流程和內核。因為OO的抽象層面并不是這個。
我 :事件的連續性并不是某種編程方法或者程序邏輯結構所決定的。正如你前面所說的,那是CPU決定的事。
Soul:比如條件選擇,其實也可以用一種對象來實現,而事實沒有。這個是因為cpu的特性和面向對象太麻煩。
我 :可能,將CPU做成面向對象的可能還是比較難于想象和理解。所以MS才啟動.NET Framework。我不認為.NET在面向對象方法上有什么
超越,也不認為它的FCL庫會有什么奇特的地方。——除了它們足
夠龐大。但是我認為,如果有一天OS也是用.NET Framework來編寫
的,OS一級的消息系統、異常機制、線程機制等等都是.NET的,都
是面向對象的。那么,在這個基礎上,將“事件驅動”并入OO層面
的模型,才有可能。
Soul:所以我發覺面向對象的思維第一不可能徹底,第二只能用在總體分
析層上。在很多時候,實質上我們只是把一個順序的流程折疊成對
象。
我 :倒也不是不可能徹底。有絕對OO的模型,這樣的模型我見過。哈
哈~~但說實在的,我覺得小應用用“絕對OO”的方式來編寫,有
失“應用”的本意。我們做東西只是要“用”,而不是研究它用的
是什么模型。所以,“Hello World”也用OO方式實現,原本就只
是出現在教科書中的Sample罷了。哈哈。
Soul:還有不可能用徹底的面向對象方法來表達世界。 因為這個世界不
是面向對象的。 是關系網絡圖,面向對象只是樹,只能片面的表
達世界。所以很多時候面向對象去解決問題會非常痛苦。所以編程
退到數據結構更合理,哈哈。
我 :如果內存是“層狀存取”的,那么我們的“數據結構”就可以基于
多層來形成“多層數據結構”體系。如果內存是“樹狀存取”的,
那么我們當然可以用“樹”的方式來存取。——可惜我們只有順序
存取的內存。
我 :程序=數據+算法
——這個是面向過程時代的事。
程序=數據+算法+方法
——在OO時代,我們看到了事件驅動和模型驅動,所以出現了“方法”問題。
Soul:我的經驗是:總體結構->面向對象,關系->數據結構,實現->算法
Soul:看來我們對面向對象的認識還是比較一致的。
我第一次提到我對程序的理解是“程序=數據+算法+方法”,便是在這一次與 Soul 的交談之中。在這次的交談中的思考仍有些不成熟的地方,例如我完全忽略了在面向過程時代的“方法”問題。實際上面向過程開發也是有相關的“方法”的。
所謂“面向過程開發”,其實是對“結構化程序設計”在代碼階段的一個習慣性的說法。而我忽略了這個階段的“方法”的根本原因,是即使沒有任何“方法”的存在,只需要有了“單元(Unit)”和“模塊(Module)”的概念,在面向過程時代,一樣可以做出任意大型的程序。在那個時代,“方法”問題并不會象鼻子一樣凸顯在每一個程序員的面前。
面向過程開發中,“過程(procedure)”是 CPU 提供的,“單元(unit)”則是編譯器提供的(機制)。程序員不需要(至少是不必須)再造就什么“方法”,就可以進行愚公式的開發工作了。
如果不出現面向對象的話,這樣偉大的工程可能還要再干一百年……
而與“面向對象”是否出現完全無關的一個東西,卻因為“過程”和“單元”的出現而出現了。這就是“工程(engineering)”。
周愛民(Aimingoo) 2013-08-24 22:12:04