數學與程序員的關係

本文是Common Lisp專家Peter Seibel對Google公司首席Java架構師Joshua Bloch的訪談,談到API對設計流程的影響和Google的Java觀,以及數學、散文與程序員的關係。

數學與程序員的關係

Joshua-Bloch Seibel:你認識有什麼偉大的程序員不會數學或者沒有接受過良好的數學教育的嗎?要成爲一個程序員,學習微積分、離散數學和其他的數學知識真的那麼重要?還是做程序員只需要一種思想方式,即使沒有受過這些數字訓練,也能擁有?

Bloch:我覺得是思想方式,學不學數學都能擁有這種思想。但是學一下確實有好處。我曾有個同事叫madbot,Mike McCloskey。 他很懂數學,但是沒有學過數論。他重寫了BigInteger的實現。原來的實現是C語言函數包的封裝,他發誓用Java重寫,要達到基於C語言版本的速 度。後來他做到了。爲此他學了大量的數論知識。如果他的數學不行,他肯定搞不定這個項目,而如果他本來就精通數論,就無需費力去學習了。

Seibel:但是,這本來就是個數學問題啊。

Bloch:對,這個例子不恰當。但是,我相信即使是跟數學無關的問題,學習數學培養出的思維方式對編程來說也是必不可少的。例如,歸納證明法和遞 歸編程的關係非常緊密,你不理解其中一個,就不可能真正理解另外一個。你可能不知道術語基本情況和歸納假設,但是如果你不能理解這些概念,你就沒有辦法寫 出正確的遞歸程序。所以,即使是在與數學無關的領域內,不理解這些數學概念的程序員也會遇到很多困難。

你剛纔提到了微積分,我覺得它不那麼重要。可笑的是這麼多年來似乎已經成爲了一種思維定勢了,只要你受過大學教育,那麼人們就認爲你應該懂微積分。微積分中有很多美妙的思想,可以讓人展開無窮的想象。

但是,你可以以連續或者離散這兩種不同的方式思維。我覺得對程序員來說,精通離散思維更爲重要。例如我剛提到的歸納證明法。你可以證明一種假設對所 有整數都成立。證明過程就像施魔法一樣。首先證明它對一個整數成立,然後證明針對這個整數成立意味着針對下一個整數也成立,這樣就能證明它適用於全部整 數。我認爲對程序員來說這比理解極限的概念要重要得多。

好在我們無需選擇。大學課程裏這兩樣都教得不少。所以即使你用微積分用得沒離散數學那麼多,學校裏還是會教授微積分的。但是我認爲離散的東西比連續的東西更重要。

散文與程序的關係

Seibel:前面你提到寫程序和寫散文有許多相似之處。儘管數學和計算機、編程的聯繫一直很緊密,但是不是可以認爲,寫Web框架或者基於Web框架的Web應用程序所需要的技能跟寫作的關係更爲緊密呢?

Bloch:是啊。前面你提到Java程序員有兩個不同的社羣。編寫庫、編譯器和底層框架的社羣,更需要數學知識。而如果你是在底層框架之上編寫 Web應用程序,那麼必須瞭解如何進行溝通,言語上的、視覺上的溝通都需要了解。遇到那些令我操作失誤的網站我就很惱火。顯然有些人完全沒有考慮過用戶怎 麼使用他們的產品。所以實質上,編程能力是一系列不同技能的結合。你擅長哪些技能,決定了你擅長編寫什麼樣的程序。但是,即使是庫、編譯器以及底層框架也 需要代碼可讀、可維護。如果你不擅長寫作,你就很難達到你的目標。

API對設計流程的影響

Seibel:你設計軟件的流程是什麼樣的?打開Emacs就開始寫代碼,然後改來改去直到程序寫好?還是坐到沙發上拿着一打紙先列個提綱?

Bloch:很多年前,我在OOPSLA(譯者注:面向對象編程、系統、語言和應用國際研討會。)上作了一個演講,題目是“如何設計一個好的API,以及這爲什麼很重要”。網上可以找到這個演講的幾個版本。它很好地解釋了我的設計流程。

最重要的是瞭解你到底要設計什麼,也就是你要解決的是什麼問題。需求分析的重要性怎麼強調也不過分。有人認爲:“噢,需求分析呀。跑到顧客那邊問問他需要什麼。得到客戶的答案不就成了嘛。”

事實絕非如此。這不僅是一個協商的過程,而且是一個理解的過程。許多顧客不會告訴你問題,而會告訴你一個解決方案。例如,顧客可能會說:“我需要你 給這個系統加上以下17個特性。”那麼你必須問:“爲什麼?你想用這個系統做什麼?你期望它怎麼發展?”等等。你要來來回回好幾次,直到弄明白顧客真正需 要軟件去做的所有事情。這些就是用例。

這個階段最重要的事情就是提出好的用例。一旦有了用例,你就有了用來比較所有備選解決方案優劣的基準。你可以花大量的時間去改進用例,因爲一旦用例錯了,你就徹底失敗了,所有後續的流程都會徒勞無功。

我見過這樣的事。有人找來一幫聰明人,還沒搞清到底要做個什麼樣的系統,就開工了。辛苦地工作了6個月,寫出來247頁的系統規範文件。這是最糟糕 的情況。因爲6個月後他們精確制定出來的系統可能毫無用處。他們往往會說:“我們已經投資了那麼多,制定出來規範文件,我們必須把這個系統做出來。”所以 他們創造了一個沒有任何用處的系統,這個系統也從未投入使用過。多恐怖啊。如果沒有用例就做好了軟件,那麼當你試圖做點非常簡單的操作時就會發現:“哦, 我的天,像選擇一個XML文檔並打印這麼簡單的事情,需要這麼多的代碼啊。”這是很恐怖的。

所以先獲取這些用例,然後編寫骨架API。骨架API應該很短很短,也就一頁紙的內容吧,一般正好是一頁,無需非常精確。你要聲明包、類和方法,如果還不清楚他們應該是什麼樣的話,可以放一句話的描述。不過這不是產品發佈要求的那種質量文檔。

中心思想就是在這個階段保持敏捷,逐步完善API,使其滿足用例,爲原始的API添加代碼,看是否可以滿足需求。真是不可思議,很多事情事後看真是 太淺顯了,但設計API的時候,甚至是構思用例時,你還是會犯各種錯誤。用代碼實現用例時你會說:“哦,我的天,全都錯了。類太多了。這些可以合併,這些 需要拆開。”或者類似這樣的話。好在API文檔只有一頁長,改起來也很容易。

你對API越來越有信心,代碼也就越寫越長。但是,核心原則是,先寫使用API的代碼,然後再寫實現它們的代碼。因爲,如果實現代碼被廢棄,之前的 工作就都白做了。事實上,應該在給出設計規範前寫API的代碼,否則你可能把時間浪費在給最後完全不需要的東西設計規範上。這就是我設計軟件的方法。

Seibel:設計Java集合類這樣的,一個具體的自包含的API,設計規範需要有多具體?

Bloch:我敢說比你想的要粗略多了。任何複雜的編程都需要API設計,因爲大程序都需要模塊化,你必須設計模塊之間的接口。

優秀的程序員把問題分塊,孤立地去看他們。這樣做的理由有幾條。比如,你可能會在無意中創造出好用的、可重用的模塊。如果你寫一個單一的系統,它越 來越大,等你想分塊的時候,就無法找到清晰的邊界,最後系統就變成了一個無法維護的垃圾。所以我斷言,無論你是否把自己看成API設計者,把問題分塊都是 最好的編程方法。

這就是說,編程的世界非常廣闊。如果對你來說編程就是寫HTML代碼,那麼這也許不是最好的編程方法。但是,我認爲對於大多數編程來說,這就是最好的方法。

Seibel:所以你希望系統由不同的模塊鬆散地耦合在一起。要達到這樣的目標,現在有兩種不同的看法。一種是坐下來實現設計模塊間的API,像你前面提到的那樣。另外一種是,“構建可運行的最簡系統,然後毫不留情地重構”。

Bloch:我不認爲這兩種方法有什麼衝突。某種程度上,我談的就是測試先行編程,以及對API的重構。如何測試你的API呢?在實現API之前編 寫它的測試用例。雖然我還不能運行用例,但我在進行測試先行的編程:實現用例後看API是否能完成任務,我用這樣的方法測試API的質量。

Seibel:也就是說你寫好使用API的用戶代碼,然後評審代碼:“這就是我要的代碼嗎?”

Bloch:對!有時候你都不用走到評審用戶代碼的這個階段。寫代碼的時候可能就會有感悟,“寫不出來,我忘了這部分API的功能了。”或者“這代碼寫起來太乏味了,一定哪裏出錯了。”

這跟你多麼優秀無關。不用API寫代碼,就不可能看出API有什麼問題。設計了一個東西,使用了才知道:“哦,錯的這麼離譜。”如果這是在你浪費大 量時間基於這個API寫了無數代碼之前的話,那麼這就是一個重大的勝利。所以,我談得更多的是測試先行編程和對API的重構,而不是重構API的實現代 碼。

說到能夠運行的最簡程序,我完全贊同這種提法。API設計有一條基本原則:疑則不用。它必須是完全滿足你關心的所有用例的最簡系統。而不是說“把亂 七八糟的代碼堆在一起”。有很多格言警句說明了這點。我最喜歡的一條是:“簡單沒那麼容易做到。”坊間認爲就是Thelonious Monk說的,實際 不是,是誤傳。

沒人喜歡爛軟件。人們提倡“構建可運行的最簡系統,然後毫不留情地重構”,而不提倡“寫垃圾代碼”,更不會說“不要做前期設計”。我曾跟 Martin Fowler討論過這個問題。他堅信,只有仔細推敲要做的東西,系統纔會有合理的形狀和結構。他說過,“不要在寫代碼前先寫下247頁的設 計規範。”我很贊同。

我不贊同Martin的一點是:我認爲測試不能用來取代文檔。只要你寫了別人編程時可以利用的代碼,你就需要做出精確的說明,而測試確保這些代碼符合你給出的說明。

所以兩大陣營確實有些不同意見,但是我認爲他們之間的鴻溝沒有某些人想象的那麼大。

Seibel:既然你提到了Fowler,咱們就聊聊他。他寫了很多關於UML的書,你把UML當設計工具用過嗎?

Bloch:沒有。我覺得用UML做些圖表讓其他人理解起來可能更容易。但是說實話,我根本記不住那些組件應該是方的還是圓的。

Google是否可以多用點Java

Seibel:作爲Google公司裏面的Java程序員,你有沒有想過Google是否可以多用點Java?如果不考慮現實因素,假如輕揮一下魔棒就可以把Google所有的C++代碼用Java代替,這樣行嗎?

Bloch:某種程度上是可以的。系統的大部分都可以用Java編寫,而且現狀,也是逐漸往這個方向發展的。但是對系統的絕對核心,例如索引服務器 的內循環來說,性能上的一丁點兒提升都有巨大的價值。當這段代碼運行在很多機器上的時候,你讓它稍微快一點,那麼無論是在經濟上,還是從環保角度看,都會 獲得很大的收益。所以有些代碼你恨不得用匯編來寫,彙編就比C語言更好嗎?

我不是對某件事物特別虔誠的那種人。能用就好。我寫了20年的C語言代碼。從消耗多少程序員的時間的角度來看,使用更現代的編程語言更有效率,而且 更現代化的編程語言更安全、更便利,表達能力更強。在大多數情況下,程序員的時間比計算機的時間更寶貴。但是當你的程序運行在成千上萬臺機器上的時候,就 完全不同了。所以我們寫的有些程序,使用那些可能不那麼安全的語言,榨出每一點值得榨出的性能。現在程序員們使用的現代語言效率都差不多,如果有人說他們 的語言效率高十倍,那麼多半是在騙你。

但是從工程師寫程序耗時的角度去看,差異很大。首先,更現代的語言已經排除了大量的錯誤實踐。其次,它們包含了大量的工具,可以提高工程師的工作效 率。可以說這是一種文化,是人們在學校學的語言。但是它也是工作中的基礎工程問題。例如,假如一種語言有宏處理器,那麼就很難給它寫出好的工具。解析 C++比解析Java要難多了。

現在,Google用Java寫的代碼比以前多多了。我不知道具體的數量,但就算還沒有達到臨界點,估計也快了。不過,各種語言都有多少行代碼和在 各種語言下執行多少個循環是有很大區別的。試圖把索引服務器的內循環用Java改寫很愚蠢,不值得稱道。如果你是初創一個公司要做類似的事情,可以用 Java或者其他現代的安全的語言來寫大部分代碼,但是在不需要它們的時候,不要用它們。我們有自己的工程基礎架構。代碼庫、監控工具等所有的東西都維繫 着它。就算Java最終不能獲得同等的地位,也會在這些系統中有很多用處,這就不錯。我剛到Google的時候,還不是這樣的。

如果很早就着手建立公司的DNA,就能夠獲得巨大的成功,但是這也令他們很難換掉那些早期應用良好、現在已經過時的技術。我記得1982年左右,我 在約克鎮高地的IBM研究中心實習的時候,那裏的主流還是批處理系統。甚至當他們已經開始做分時系統的時候,他們還用虛擬讀卡機(編程卡片)、虛擬打孔器 這樣的術語交流。什麼東西都用80列的記錄。而DEC一直將思維禁錮在分時系統上。我估計微軟也面對這樣的問題,就是他們的思維能否超越桌面PC系統。

Seibel:20年內,人們將會談論Google爲何只能在互聯網上賣廣告。

Bloch:沒錯。畢竟,在Google還有一部分人認爲Java太慢而且不可靠。有這種看法的原因很顯然,那就是1999年左右發佈的用於 Linux的Blackdown Java(譯者注:一個非官方移植的虛擬機),它確實又慢又不可靠。既有的看法總是很頑固的,很難改變。事實上 Google在很多核心功能上使用Java,甚至包括廣告。

所以某種程度上,他們知道Java既快又可靠。但是在實際的搜索流程中,對機器循環最敏感的領域,所有的東西都基於C++,這麼做很明顯的一個原因就是公司的“基因”。這將在很長一段時間裏影響着我們。

Seibel:你實際編程中用哪些工具?

Bloch:我就知道你遲早要問這個問題,我是老幫菜了,提這個都覺得丟人。Emacs的鍵盤快捷方式在我的腦子裏面已經根深蒂固了。而且我喜歡寫小的程序,代碼庫之類的。所以,我寫代碼的時候幾乎不用現代的工具。但是我知道,很多現代的工具可以提高效率。

寫大程序的時候我確實使用IntelliJ,因爲我們整個團隊都在用,但是我不是這方面的專家。這個工具給我留下了深刻印象,我喜歡這些工具對代碼 做的靜態分析。我找用Eclipse、NetBean以及FindBug的人來幫我審閱《Java解惑》,書中的很多錯誤陷阱都可以被這些工具自動檢測 到,太了不起了。

(本文來自《程序員》雜誌10年12期,更多精彩內容敬請關注12期雜誌)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章