內容簡介
《軟體框架設計的藝術》幫助你解決api 設計方面的問題,共分3 個部分,分別指出學習api 設計是需要進行科學的訓練的、java 語言在設計方面的理論及設計和維護api 時的常見情況,並提供了各種技巧來解決相應的問題。《軟體框架設計的藝術》作者是netbeans的創始人,也是netbeans 項目最初的架構師。相信在api 設計中遇到問題時,本書將不可或缺。
《軟體框架設計的藝術》適用於軟體設計人員閱讀。
作譯者回到頂部↑
本書提供作譯者介紹
Jaroslav Tulach NetBeans的創始人,也是NetBeans項目最初的架構師。有著豐富的項目開發經驗,一直致力於如何提高開發人員的設計技巧,從而保證了NetBeans項目的成功。
王磊 工學學士及碩士。2006年起任職於普元公司,一直擔任普元公司主任架構師。同時是自由軟體Aquarius ORM Studio的作者。
朱興 軟體工程師。一直從事軟體研發工作,熟悉Java、Eclipse外掛程式開發、API設計等相關技術。
摘要
本書幫助你解決API 設計方面的問題,共分3 個部分,分別指出學習API 設計是需要進行科學的訓練的、Java 語言在設計方面的理論及設計和維護API 時的常見情況,並提供了各種技巧來解決相應的問題。
本書作者是NetBeans 的創始人,也是NetBeans 項目最初的架構師。相信在API 設計中遇到問題時,本書將不可或缺。
本書適用於軟體設計人員閱讀。
序言
讀者也許會想:“在程式開發領域中,講述軟體設計的技術圖書是不是太多了?”,的確如此,因而你有理由來質疑,為什麼我還要寫一本這樣的書而你又憑什麼還要再讀這樣一本書?說起軟體設計的經典圖書,那本由GoF執筆的《設計模式》,對每一個想要掌握面向對象技術的開發人員來說,已經成為案頭必備之書。此外,對於不同類型的套用開發,也存在大量專業的軟體設計模式圖書。還有Effective Java,這本傳世之作已經成為眾口相傳的Java程式開發聖經。基於以上事實,還有必要再多一本關於軟體設計的圖書嗎?
我相信這自有其必要性。從1997年開始,我一直從事NetBeans的API設計工作。在此期間,與其他設計框架或者通用功能庫的人一樣,我也經歷了各種酸甜苦辣。剛開始時,我一邊嘗試將其他語言的一些好的代碼風格照搬到Java語言上,一邊慢慢地找感覺。隨後我對Java語言使用漸趨熟練,將已知的模式套用到我寫的Java語言代碼上,看起來也就容易多了,當然後來,我又發現事情遠不是想像中的那么簡單。我認識到,要設計NetBeans這樣一種面向對象的套用框架,直接套用傳統的模式並不恰當,不管那些傳統的模式有多成功,它所需要的是一門全新的技藝。
NetBeans中最早的API要追溯到1997年了,距今已有10多年的歷史,至今仍然有不少用戶在使用這些API,而且程式運行一切正常,當然坦率地說其中有很多內容與開始時的設計已經不盡相同。在這10多年中,我們也經常根據新的需求來調整和擴展類庫的功能,同時對開始時犯下的一些錯誤進行修正。儘管如此,那些使用了這些API的客戶仍然可以使用最新版本的類庫來編譯並運行他們早先的程式。這一切並非偶然,因為我們一直在盡最大的努力來保證類庫的向後兼容性。即使客戶使用我們10多年前提供的類庫來編寫程式,然後用最新版本進行編譯和運行,這些程式仍然可以平穩地工作。這種有效地保護客戶原有的軟體投資的理念非常重要,但在常見的設計圖書中卻無法找到,至少在我讀過的那些書中無人提及。當然,在NetBeans平台的開發過程中,並不是所有的API 的演化之路都是一帆風順的,但我相信,NetBeans的團隊成員已經爐火純青地掌握了這方面的API設計技巧,而其他組織的開發人員也同樣需要了解這些技巧。基於這個原因,向後兼容性這個話題在本書中占用了大量篇幅,書中還大量介紹了特殊的API設計模式,有助於編寫適合向後兼容的代碼。
NetBeans團隊工作的擴展能力是當時所面臨的另外一個挑戰。1997年項目剛剛開始的時候,由我個人負責API的開發,其他工程師的工作則只是寫代碼,也就是說他們負責完成用戶界面並實現其他NetBeans IDE,這些工作都需要頻繁用到我所設計的API。毫無疑問,當時我就變成了整個項目的瓶頸。我開始認識到,實現NetBeansIDE功能的開發人員越來越多,我一個“架構師”不可能完成所有的API需求。隨著時間的流逝,急需做出改變。NetBeans的開發團隊中的大部分開發人員都應該能夠設計他們自己的API。而且不管是哪個開發人員設計的API,我們希望保持一致性。但一致性在這裡卻成為最大的一個問題,原因不是出在開發人員身上,他們其實也想保持一致性,只是因為我當時無法給他們解釋清楚我所指的一致性到底是什麼東西。相信很多人也有過與我類似的感覺:自己知道如何去做一件事,但就是無法清楚地解釋給別人聽。我當時就處於這種狀態:我覺得自己知道如何設計API,但卻花了好幾個月的時間才整理出想讓大家遵守的最重要的規範。
銘記於心的API講座
一直以來,我對設計和發布API都有著濃厚的興趣。在Sun公司內部以及為NetBeans合作夥伴我也做過多次關於該議題的講座。但直到2005年在舊金山舉行JavaOne會議時,我才第一次就這個議題進行了公開講座。當時我和我的朋友Tim Boudreau向大會提交了一份議題,名為:如何設計歷久彌新的API。也許是因為在議題摘要中沒有寫上Ajax和Web 2.0這種吸引眼球的字樣,我們的議題被安排在晚上10點30分。對於講座而言,這個時間點實在算不上理想,各種聚會、免費的啤酒宵夜,以及午夜的各種娛樂活動都會搶走我們的聽眾。我們深受打擊,只能期望講座期間會有一兩個朋友來露上一小臉,問上幾個問題。當晚我們抵達會場,看到隔壁將要進行一場JDBC 的講座時,我們的情緒更是一下子跌到了谷底。走廊里擠滿了人,我們以為這些人都是對資料庫感興趣,準備來參加隔壁的JDBC技術講座的。但讓我們吃驚的是,大部分聽眾其實是衝著我們的講座來的!舉辦講座的房間很快就水泄不通了,座無虛席,還有些聽眾乾脆坐在地板上或者倚牆而立,甚至門都不能關上,因為還有部分人只能站在外面的走廊聽。最終,我做了一場從未有過的激動人心的講座。
自那以後,我確信有很多人想要了解如何設計API,這種需求並非憑空臆造,而是真實存在的。因此在我撰寫此書的過程中,每當快要失去動力的時候,便會想起那場激情澎湃的講座,就又重燃熱情。這場講座時刻提醒著我,那些源於實踐並指導我們正確設計API的原則應當被記錄下來。這些原則雖然基於大量的設計圖書介紹的設計知識,但在本書中得到強調和擴展,因為API設計有其特殊性。
API設計的特殊所在
為什麼說市場上現有的設計圖書還不夠用呢?這是因為設計框架或者通用類庫是一件非常複雜的事情,其複雜度與自行設計內部系統不可同日而語。打個比方,在一台伺服器上基於一個小型資料庫來搭建一個Web套用之類的小系統,就和蓋一間房子差不多。當然,有些房子可能很小,也有些房子會很大,有時也許是一座摩天大樓。這些建築通常情況下都只有一個主人,只有他才會改造這個房子。如果需要的話,改一下屋頂,換上新窗戶,也許會砌面牆多隔出一間來,再拆舊牆打通兩個房間,等等。當然,有些改變算不上什麼麻煩事,如換個屋頂不會對地板造成什麼大的破壞,換個窗戶,只要大小規格不變,也不會影響別的部位。但是,如果想把窗戶從小變大,就不那么簡單了,而想換個兩倍大的新電梯更是不可能完成的任務。再如,罕有人會瘋狂到蓋個新的一樓,然後把原來的樓層都往上移一層。這樣做實在是問題多多,弊大於利。當然,從技術角度來看,上面這些變化仍然是可行的。只要這個房間的主人確有這個需要,鐵了心也能做到。
內部軟體系統也有些相似。通常也只有一個所有者,而且它還具有完全的控制權。如果現在需要對系統中的部分功能進行升級,那么儘管放手去做就是了!如果要改變一下資料庫的模式,也可以悉聽尊便。當然,有些改變會比較複雜。拿以下兩種情況作個比較:修改一行代碼來修復一個NullPointerException的bug和調整資料庫模式,顯然,後者的影響會大得多。但對於內部系統來說,一切改變都是可行的。因為其所有者有著絕對的控制權,只要系統確實需要大的升級,甚至可以暫時關閉這個套用系統,等完成升級以後再重新運行。此外,我們已經有了不少相關的設計原則幫助,我們更好地管控一個內部系統的變化。有設計模式的書籍幫助開發人員更好地組織代碼,還有不少關於設計系統和測試系統的方法論,還有大量的圖書介紹如何組織和領導員工進行團隊合作。可以說,維護一個內部系統的方方面面,都是非常清楚明了的,也有很多詳細文檔可供參考。
但是編寫API則有所不同。可以拿宇宙來打個比方,儘管宇宙不像先前說的房子那么直觀,但還算得上比較形象。先來回憶一下我們已知的宇宙,我說“已知”的宇宙,是因為沒有人能洞悉宇宙的一切,所有那些恆星、銀河,以及其他天體,還包括無形的內容,如所有物理規律。當然,人類現在對宇宙所了解的其實只算得上滄海一粟。我們的視野有多寬,就限定了宇宙在我們眼中的樣子,也就是說,我們所說的宇宙,只是在我們自己眼中的宇宙,是真實宇宙在我們眼中的一個縮影。它包含了無數的天體和狀態,然而,我們的經驗和想像告訴我們,在我們視野範圍以外,還有其他的星星和銀河,但我們對它們一無所知。千百年來,人類通過打造更先進的設備及不斷地認識和理解自然的規律,不停地擴展自己的視野,不斷地發現新事物或者新規律,人類關於宇宙的認識和經驗就是基於上述實踐而逐漸形成的。
宇宙並非亘古不變,而是時刻都在變化,但其變化卻有規律可循。這些規律告訴我們,行星、恆星以及其他天體之間是如何相互影響的。舉例來說,假設某個人通過自己的觀察,發現了一顆新的恆星,那么不管明天、後天還是大後天,都可以觀察到這顆恆星,這並不奇怪。雖然現有的自然規律告訴我們:恆星不僅會移動,而且會旋轉,甚至還會爆炸。但這些變化都會遵從自然規律。不會有人隔一兩周就發現一件宇宙大事,說有某個恆星出現了,消失了,或者是進行隨機性的移動。如果宇宙真有這么瘋狂,那么就完全顛覆了我們今天對於宇宙的認識。我們通常認為,一旦一顆恆星被發現,它就會長久存在,甚至相信即使無人觀察到它,它依然存在。這顆恆星可以被地球上的一個人觀察到,也可以被太陽系其他地方的另外一個人觀察到,甚至還有宇宙中其他智慧生物觀察到,當然也可能無人注意它。但恆星本身並不知道自己是否被他人所觀察,它只會默默遵照自然規律存在和運行。因此一旦被發現,它便一直陪伴著我們。
好的API也是如此。一旦某個通用的類庫在某個版本引入了一個新方法,就好像發現了一顆新的恆星。所有使用該類庫的人都可以看到並使用該方法。至於是否在自己的程式中使用,則要視乎程式設計師自己的需要了。有可能,多數API的使用者對你添加的新方法完全不在意。但你不能把希望寄托在這種並無根據的猜測之上。多年的開發經驗告訴我,API的用戶太有創意了。有時候,他們涉及的領域甚至超過了API設計者。換句話說,只要API有可能被誤用,就一定會有人去誤用這個API。隨之而來的結果就是,不管是方法本身還是其設計者,都不知道該方法是否被使用及其使用頻率有多大。也許會有很多用戶使用它,也許一個都沒有,但除非你想破壞優秀API的設計法則,即打破其向後兼容性,否則就必須假定有人在觀察,必須保證這些API得到良好的維護和保留。“API就如同恆星,一旦出現,便與我們永恆共存。”
宇宙與API的設計還有一個相似之處。我們對宇宙的認識不斷加深,正如我們的類庫在不斷演進。古希臘人已經可以識別和觀察遠至土星和木星的所有行星的運行路線,這就是他們眼中的宇宙的樣子。他們盡力去解釋行星運行背後的原因,但以今天的標準來看,他們顯然並不成功。他們還不能揭示出行星運行的規律。到了文藝復興時期,哥白尼提出了日心說,而克卜勒則用行星三大定律來詮釋行星相對於太陽的運行軌跡和速度。這些探索豐富了人類對宇宙的發現,從而能夠對“宇宙是什麼”進行精確的解釋。但沒有人知道“宇宙為什麼是這個樣子”。直到1687年,牛頓才對這個問題給出了詮釋,他引入了萬有引力的概念。萬有引力不僅可以用來解釋克卜勒定律,還極大地擴展了我們對宇宙的認識,因為對於已知宇宙中多個天體間發生的所有事情,基於牛頓定律的物理學 幾乎都可以給出合理的解釋。
一切看來都很完美,直到19世紀末。很多實驗都發現了一些無法用牛頓定律來解釋的現象,特別是對於一些高速運動的天體。這些現象促使愛因斯坦創立了相對論,幫助我們更深入地理解宇宙及其各種現象包括對高速運動的天體的理解。事實上,愛因斯坦的理論是牛頓理論的延伸,只要天體慢到一個合理的速度,那么使用這兩個理論可以得到相同的結果,此時可謂殊途同歸。
前文囉哩囉嗦地說了很多物理和歷史背景的內容,這些東西與設計API有半點關係嗎?接下來先讓我們來假設有一個上帝,萬能的他通過一個API庫與人類進行溝通。這個庫是人類通向這個已知的宇宙的橋樑。古希臘人使用這個庫的0.1版本,其功能很簡單,只能用來列舉不同的行星以及它們的名字。這個庫提供的API顯然不夠豐富,但對於那時候的人來說,已經足夠了。藉助於這個簡單的API庫,古希臘人已經可以分辨幾顆行星。在這個庫的使用過程中會有不少人經常提出新的需求,希望對這個API庫加以改進。當克卜勒需要進一步了解行星的運行規律時,就發現這個庫的功能已經不能滿足他的需要了。因此,這個萬能的上帝就給了他一個升級版本,姑且稱之為1.0版吧。這個版本的庫能夠為每個行星在某個時間點上提供空間坐標,以確定其位置。同時1.0版本很好地兼容了0.1版本,也就是說,原來那些古希臘人所使用的功能仍然可以正常使用。
只不過,用戶從來沒有知足的時候,物理學家們亦如是。為了幫助牛頓,那位萬能的上帝接著提供了宇宙2.0這個重要的版本。該版本不僅描述了太陽和其他行星之間存在的萬有引力,還提供了一堆公式用以計算空間物體的引力、加速度及速度,也不再只局限於行星了。不用說,這個新版本仍然兼容以前的版本,古希臘人和克卜勒使用的那些功能仍然可以在新版本中正常使用。
至此,所有的變化都很直接。一直以來,那位萬能的上帝只是為新版本添加了一些功能。在經典物理學成熟以後,物理學家聲稱,宇宙的所有規律都已經被物理學家發現了,在物理學的領域已經沒有什麼未解之謎了!這個聲明真是一個天大的諷刺,上帝拋出了麥可遜實驗 證明了這個臆斷的荒謬,進而導致愛因斯坦提出他的相對論。這時候,物理學家們發現,最新版本的宇宙API庫出現了無法簡單向後兼容的問題,因為新的理論指出,以前所有的物理學家,包括牛頓,都存在少許的錯誤!儘管出現了如此大的一個變動,但新的API庫仍然可以按向後兼容方式來處理。那是因為只有以特別高速運動的物體,根據原有方式來計算得到的結果才有可能不正確。而在牛頓及其先行者的那個時代,受限於技術等原因,是無法對這樣高的運動速度進行測量的。因此,儘管不兼容的問題早就存在,但用以前的測量技術卻發現不了,從而也無法證實宇宙API庫的功能發生了改變。
上述這個荒誕的故事旨在說明我們對宇宙的認識一直在不停地進步。我們編寫的API庫也同樣如此。或許樂觀的人並不認同,但我真的感覺人類永遠都無法了解整個宇宙的奧秘。當然,我認為我們對宇宙的了解會越來越多。雖然程式設計師的觀點各有不同,但我相信所有已經在使用的API庫都會永無止境,它們會繼續演化。對此,我們必須做好準備。我們必須準備好隨時改進我們的API庫,就像我們必須準備好隨時修正我們對宇宙的認識。
與建造一所房子或一個內部的軟體系統不同,編寫API庫需要開發人員放眼未來,看到今後潛在的需求。但在我看來,現在人們設計API的做法往往不是這樣。目前市面上的圖書也並沒有促使人們這樣思考。書中的設計模式大多只能用在特定版本,使用者也只是在特定上下文的環境中去考慮問題,他們極少參考老的版本,也不太考慮未來的需求,所舉的例子以及相關的上下文都具有很大的片面性。當然並非說這些書全無裨益,在編寫通用功能庫和框架時還是需要這些技巧。現在,我們必須停止學習如何來設計內部系統,而要開始學習如何來設計一套API庫。在學習中一定要堅持一個觀點:“API一旦發布,便與我們永恆共存。”
讀者對象
如果此時你正在書店面對這本書,在買與不買之間猶豫不決,那是因為你無法判斷這本書對你是否有用。老實說,這點我幫不了你,因為我不是你。但我可以告訴你我自己為什麼需要這本書,以及我寫作該書的緣由,這樣也許可以幫助你決定是否應該購買此書。我在設計NetBeans框架的API時,就像在一片迷茫中尋找光明,總是摸不清方向。最開始,我完全憑直覺,而且認為寫API是一種藝術。我知道對於藝術來講,需要創造力,但設計API,與藝術是不同的範疇。時間慢慢過去,我從已經完成的工作中汲取經驗,逐漸整理出一整套思路和度量標準,藉助於這些可量化的標準,可以將一個普通的API最佳化成一個優秀的API。
本書介紹了 NetBeans團隊中一直以何種標準來評價API的質量,並清楚地說明我們團隊為什麼一直堅持使用這個標準。事實上,這些標準都是我們經過多年的嘗試,並從錯誤中吸取教訓,才最終得到的。地球人都知道,重新發明輪子並不是一個好主意,這是在浪費時間和金錢,所以對於那些把API設計更多地看作是一種工程而非藝術的架構師們,我鄭重推薦此書。在NetBeans的初創階段,只有我一個人來設計API 。當時,我們有一個比較極端的觀點:“一群代碼開發人員是不可能設計出一個好的API的。”其實對於一個單兵作戰設計人員來說,即使沒有任何規則來約束,他所設計的API也具有一致性。但像NetBeans這樣的一個大團隊,是不能只有一個設計人員的。所以,我的首要任務就是要去尋找一種方式,讓更多的人能夠設計API,同時還能保持整體設計上的一致性。那個時候我已經開始撰寫本書,希望能告訴大家API設計方面的相關理論,以及我們編寫API的原因和目標等,同時還根據我們過去的經驗總結出一些規則,方便大家來量化一個API的設計質量。接下來,我將我的經驗與NetBeans團隊中的成員分享。從此,我開始放手讓他們也來編寫API,在開始和結束階段花些時間來評審和指導他們的設計。以我的標準來衡量,可以說這么做很棒。這10年來,他們一直在努力地學習和進步,現在看來,我們設計的API具有了相當高的一致性,並滿足了我們的大部分需求。如果你也處於相應的職位,需要評審或者指導他人來設計API,你將發現本書中的建議會對你有所裨益。
當我想為API給出一個定義時,卻發現這個定義的範圍非常廣泛。要知道,不是說只有一個框架或者是通用庫才算是API。即使只是寫了一個普通的類,只要有同事使用了這個類,那么也算是寫了一個API。為什麼這樣說呢?假設你刪除了這個類中的一些方法,或者是改變了這些方法的名稱,哪怕是改變了這個類的一些功能,這個類的使用者都會火冒三丈。為共享庫寫API也面臨同樣的問題。如果你所寫的類有多個人使用了,那么你對該類的修改也許會強制要求所有用戶都進行相應調整,這無異於一場噩夢。這種噩夢其實是可以避免的。如果在開始寫代碼時,就把一個類當成一個 API來認真對待,你會少許多麻煩。換個角度來說,其實這事也不難,只需要設計類的時候再小心一些,在調整的時候多多關注它的兼容性,再參考和借鑑一些好的經驗,其實都搞得定。如果根據前面所說的這個定義來分析一下,幾乎所有的開發人員都在編寫和設計API。
API的一個本質特性就是它的工作機制。API的測試是非常重要的,藉助於它,可以更加清楚地說明API的原理。沒有合適的測試,就不可能編寫一個好的API。本書有幾個章節會列出一些測試方式,告訴讀者如何來有效地測試一個庫的公開接口,而且即使是要處理該庫的多個版本,也無礙於測試的正常運行。我會詳細地說明測試時要注意的各項內容,包括簽名 、單元測試和兼容性工具。所以,對於那些需要檢查API兼容性的人來說,本書非常有價值。
最後要說的是,一個得到廣泛使用的類庫將會是該庫作者的財富。如果這個類庫能夠很好地滿足現有用戶的需求,就會吸引更多的用戶來使用,財富將會不停地增加。要知道,只有在類庫擁有了大量的用戶以後,才能在這個基礎上獲得經濟利益,從而繼續開發和維護這個類庫。本書會就這一點展開討論,那些喜歡從商業角度來審視軟體開發的人會對這一個話題很感興趣。
這本書只適用於Java
NetBeans是一個使用Java語言開發的IDE框架,我的大部分與API有關的知識都是從這個項目中學到的。如果由我來回答這個問題:“這本書是否有益於那些非Java的開發?”那么答案仍然是肯定的。對於如何評價API設計是否良好,本書給出了很多準則,這些準則同樣適用於其他的程式語言。本書中的一些議題,例如:開發API的原因,編寫具有良好結構的文檔的規則和動機,還有那些關於向後兼容的原則。這些內容都可以適用於多種程式語言,包括C、 FORTRAN、Perl、Python和Haskell 。
當然,涉及細節的時候,就不得不提到Java語言的具體特性。要知道,Java 首先是一種面向對象的語言。為面向對象的語言設計API的時候,像繼承、虛方法 和封裝這些特性都必須加以考慮。因此,本書給出的一些原則更適用於一些特定的面向對象語言,如C+ +、Python或Java,至於C或FORTRAN這些面向過程的語言,雖然不錯,但已經有點古老了,不再適用本書中的原則。
Java是一種非常新的面向對象語言,它引入了垃圾收集器。當前,業界已經普遍接受了Java,說明即使在產品的正式運行環境下,垃圾收集器也是可用的且有益的。但在 Java語言出現之前,業界更傾向那種自行管理記憶體的傳統方式,如C、C++都是採用這種方式來管理記憶體,開發人員需要明確地聲明如何去申請和釋放記憶體。當時也有一些語言,像Smalltalk或Ada,它們使用了垃圾收集器,但它們都被當作實驗品,沒有幾個軟體開發商敢冒著這樣大的風險去使用它們來開發軟體。Java卻從根本上扭轉了這個現象。目前,可以說,一個基於記憶體自動管理系統的語言可以用來編寫高性能的程式。大多數的軟體工程師都已經普遍接受了這個觀點,而不像以往只會取笑或者害怕使用這種基於記憶體自動管理的語言。一個能夠自動管理記憶體的語言,會對你寫的API有所要求。比如說,Java只能通過malloc一樣的構造函式來分配對象,而不是像C一樣,需要對應地釋放API。而且Java中,其記憶體的釋放無須程式設計師關注。所以本書中給出的一些意見或者做法更適用於帶有垃圾收集器的語言,就是那種與Java相似、支持記憶體自動管理的語言。
Java還推廣了虛擬機和動態編譯技術的使用。Java的靜態編譯技術會將原始碼編譯成多個類檔案。在代碼真正運行的時候才去部署和連線這些檔案。而這些編譯好的類檔案格式是不依賴於具體的處理器架構的,而應用程式最終運行於這個架構上。
以上所說的內容通過運行時環境實現,它不僅將分散的類檔案連線 起來,還將指令轉換成處理器可以識別的指令。從Java誕生之初,這一點就是Java與傳統語言的相異之處。大家都知道,高性能程式不能通過虛擬機的解釋來獲得,像Fortran語言就要在不同的作業系統上分別進行編譯,根據實際環境來生成相應的可執行彙編,這樣才能夠更好地利用硬體的各種特性來提高程式的運行性能。當時很多人,包括我自己在內,都認為只有使用C或C++才能編寫出高性能的程式,而Java則不可能做到這一點。
然而,時間證明,基於虛擬機的程式語言具有一定的優勢。例如跨平台,不管在什麼硬體平台上,所有的數字類型具有相同的長度,不需要程式設計師去了解這些基礎的硬體架構內容。此外,Java程式不會因為段錯誤而崩潰。虛擬機對記憶體的自動化管理,避免了C指針誤用而引起的記憶體泄漏崩潰的問題 ,而且能夠保證使用的變數始終都有正確的類型。儘管具有以上所說的多項優勢,但對於早期的Java虛擬器來說,性能仍然是一個長久困擾人們的問題。隨著時間的推移,解釋器越來越快,而且用來取代解釋器的即時編譯器 能夠生成更快的代碼。這些新的變化極具吸引力,其他的一些新語言也開始採用虛擬機。目前,虛擬機已經被業界廣泛接受,並大量使用。在一定程度上,本書談論了虛擬機多方面的內容,雖有大量的篇幅用來講述類檔案的格式,但類檔案格式正是虛擬機 的通用語言。
如果想全面掌握Java語言結構對於虛擬機的意義,那么必須清楚地理解類檔案的格式。在虛擬機的世界裡,Java語言及其格式是非常簡單的。其他程式語言,如C,也有自己對應的ABI(抽象二進制接口)模型,但Java的類檔案非常有特色,體現在兩個方面。首先,它天生就是面向對象的。其次,它使用動態編譯,這就意味著,它所包含的信息遠遠超過了簡單的C對象檔案。因此,學習虛擬機獲得的知識幾乎不能用於那些雖然優秀但已經很古老的非面向對象語言。但對於那些與Java一樣使用虛擬機的新程式語言,這些知識就會非常有用。
Java是第一個能夠將實際代碼和API文檔緊密結合在一起的程式語言。通過javadoc可以將代碼中的注釋變成公開的文檔,Java提倡開發人員使用這種代碼即文檔的方式,這樣可以保證文檔隨時都是最新的。儘管其他的程式語言也允許開發人員在代碼中加入注釋,但只有在Java語言中,才支持通過JavaDoc將相應的注釋轉成可供程式設計師使用的文檔,從而保持文檔和代碼的高度一致。另一方面,這也不再是Java特有的功能了。自從這種從代碼生成文檔的功能被證明了其實用性以後,幾乎每一種在Java語言之後創建的新程式語言都支持類似於JavaDoc的文檔生成功能。而且在Java語言出現之前就有的語言也提供了額外的工具用來通過代碼生成文檔。因此,本書雖然只分析了JavaDoc在幫助用戶理解API方面的有效性,以及文檔格式的利弊,但是,給出的結論幾乎可以適用於任何程式語言。
Java 5中開始支持泛型,為Java語言帶來多方面的改變。雖然本書並不想成為一本全面講述Java語言構造的圖書,但泛型這樣的一個重要特性是不可忽視的。泛型是API設計中的一個重要內容。其新穎之處體現在哪裡呢?要知道傳統的面向對象語言通過繼承來鼓勵重用,而其實組合也是一種常見的代碼重用形式。只不過大家往往都把注意力集中在繼承,而忽略了組合。造成這一問題的根本原因是:繼承是面向對象語言內置的特性,而組合則只能由程式設計師來手工編碼,並非常容易出現類型錯誤。同時,現代已經有大量的語言將組合作為首要的重用方式,其次才是繼承,尤其是在Hashell這種函式語言中。有些人認為這兩種方式(即繼承和組合)各有所長,不可偏頗,所以他們花了大量時間嘗試將面向對象語言與多態類型函式語言結合起來。
將繼承和組合進行有效結合,這正是 Java5 中引入泛型的原因。一些人認為泛型過於複雜,對其大肆批評,但我自己在1997年的研究經驗表明,幾乎無法找到比泛型更加合適的方式了。在這點上,我欣賞 Java語言的設計團隊,他們在繼承和組合這兩者之間儘量地保持相對均衡。這也是本書講述泛型的原因所在。這樣做使得本書的部分內容更加貼近於如 Haskell這種函式式語言。
本書之所以適合於其他語言,恰恰是因為使用了Java語言。它不是去發明一種特定的新程式語言來處理API問題。整本書都使用我們熟悉的Java語言。書中所有的原則和建議都使用Java固有的編碼風格,沒有引入任何新的關鍵字,也不會對前置和後置條件或者對常量的檢查進行一些特殊的支持。對於開發一個通用類庫的軟體工程項目來說,一旦確定了一種開發語言和實現目標,都會設定一個相應的編碼風格,約束開發人員使用合適而且統一的編碼風格。要知道,學習新API所需要的工作量,與新學習一門程式語言相比,可謂小巫見大巫。
由於具體項目要使用的程式語言是確定的,那么API的設計原則也必然使用該語言來描述。我們相信,如果能使用C語言來編寫一個好的API,那么也同樣可以使用Java語言來寫一個好的 API。所以本書中只使用Java語言就足夠了。總之,書中提供了可以套用到任何程式語言的通用內容。書中還有一部分內容會更多地講述面向對象的概念,在需要進行深入講述時,會使用Java語言給出合適的例子。
學習編寫API
毫無疑問,肯定有不少人用正確的方式開發了很多API,否則現在的市場上就不會有這么多非常有用的軟體產品。但設計原則、設計API的技巧及要點,通常都是在開發過程中下意識積累而得,而這一過程往往並不具有借鑑意義。很多設計師往往沒有知其所以然就做一些API設計上的決策。結果就是在不停地嘗試,犯錯,再嘗試再犯錯,周而復始才逐漸在其潛意識中形成了設計 API的相關知識,耽誤了很多時間。這一過程中會產生很多指導人們正常做事的技巧,雖然這種過程非常有用,但因為它自身存在的問題,導致其產生的大量技巧非常分散,不易收集和管理。首當其衝的問題就是,這些技巧都有其特定的背景和作用域。很多人都知道,有大量的技巧,對於某一個項目或者是特定的人群是非常有效的,但只要換個團隊或者換個項目,就完全不能用了。
其次,因為各人的思路都不相同,所以知識的傳遞就變得非常困難。在解決某個特定問題的時候,你覺得使用Java類要比使用java接口更為合適,但一旦換了一個問題,這種解決方案可能就完全不合適了。即使你嘗試去說服別人接受這種方案,但如果沒有充足的理由去解釋,你只能使用以往自己成功的案例來說服他人來採用該方案。肯定有人會贊同該方案,也有人會反對,但這都不是知識傳承的本意。
憑感覺的NetBeans API設計中
必須承認我們在開發NetBeans項目的過程中也經歷過這個階段。在設計API時,我們會覺得某種設計可行,而另一種設計不可行,這種判斷完全是憑感覺來做出的,而不是自底向上有堅實的基礎的。這就是說,進行設計時,我們沒有嚴格的推理和分析,只是憑著一種感覺,依靠我們的潛意識來設計API。想將知識傳授給其他人的時候,就會因為他們根本沒有類似於我們的經驗,也就很難說服他們接受我們的知識。這迫使我們深入思考這一現象,考慮建立可度量的標準,用來幫助大家設計優秀的API。這本書就是深入思考後的結晶。我們確信,我們所積累的經驗已經清楚地揭示了我們決策時的邏輯思路。現在我們把這些決策時的邏輯清晰地整理出來,傳授給每一個願意傾聽的人。
閱讀本書的人,首要關注的問題莫過於以下兩個:為什麼創造API?API到底是什麼東西?本書會就這些問題展開詳細的討論。
即使不讀、不理解甚至不同意本書中給出的建議,軟體產品從業人員只要理解書中的基本需求和思路就會從中受益。這有助於更好地認識和理解API設計及其複雜性。當開發團隊中的所有成員都可以自行設計API時,成員間的溝通就會非常簡單,決策也不再需要多餘的解釋,因為他們擁有相同的知識,並在此基礎上進行思考。這樣做的最大好處,是可以提高開發人員間的合作效率,以及開發團隊及其合作夥伴間的合作效率,從而保證了軟體產品的高質量。
這本書盡力想幫助每個人都解決一些問題。它盡力為每一位讀者解釋API設計的基本動機,並為開發人員提供了例子和很多技巧,它敘述了良好架構的方方面面,不管是誰來設計API,都可以使用書中給出的那些可度量的原則來評估API的質量。
如果你還在懷疑是否應該閱讀本書,那么給你一個最簡捷的回答:“你要讀這本書。”
這是一本備忘錄嗎
決定要以何種風格來撰寫本書無疑是一件非常困難的事情,我當時在兩種完全不同的寫作風格之間搖擺不定,無法定奪。一種寫作風格是:用非常科學化、公式化的方式來說明API設計時的動機、原因及步驟。使用這種方式來撰寫的話,書中給出的建議和規則具有通用性,可以套用於任何項目。當然,通用性是本書的一個目的,書中所說的內容必須是普遍適用的,而不是簡單地描述NetBeans項目的10年發展史。而另一方面,我堅信,如果只是在不停地說著一些建議,講述著這些原則性的內容,而不給出合適的詮釋,那么再好的建議也不能起到應有的作用。我不喜歡只說上一堆“是什麼”,而不去詳細地解釋“為什麼”。我一直想清楚地分析上下文,並以此來評估各種解決方案,然後再根據具體的環境來選擇一個最合適的方案。這就是為什麼我先把設計的背景和大家說明,只有這樣,才有利於大家接受我們的設計原則。那么最佳的方式,就是把NetBeans項目中不同階段面臨的所有問題一五一十地擺出來。因此,可以將本書看作一本 NetBeans項目的備忘錄。
本書日誌風格的寫法也是一點點形成的。本書的寫作,並不是一開始就列好提綱,打好草稿,書的議題是我在幾年中陸陸續續地添加的。每當我們需要解決一個具有普遍性問題的時候,就會先在書中增加一個新的議題,找到相應解決方案後,就會記錄下來。所以這種方式有效地記錄了我們當時解決問題的思路,以及相應的規則。以這種寫作方式來完成本書,使得本書讀起來就像是記錄實驗日誌一樣。但我們的實驗日誌不是像寫日記一樣,每天一份,而是針對每個問題進行記錄!
為了從這兩種寫作風格中獲得最佳的解決方案,本書對每個專題的分析都詳細說明了NetBeans項目中需要解決的問題的真實處境,然後從特有的問題抽象出一般性的建議或者解決方案,可以適用於任何框架或通用庫項目。這類似我們採用的如下思路:首先是面對一個問題,然後進行分析,並提出解決方案。按照這樣的思路來閱讀本書,讀者就可以一步步地驗證我們給出的建議、方案,並判斷我們推廣的通用規則是否正確。在任何情況下,讀者都可以靈活地調整書中所給出的方案、意見、建議等,從而更好地套用到自己的項目中。最後採用同樣的思路、步驟,來看是否能得到與我們一致的意見。
API設計的技術天地非常美妙,但到目前為止,都還處於探索階段,需要我們一步步地來積累這些知識。今天的軟體系統正在變得越發龐大,我們需要運用最好的工程實踐來正確地架構軟體系統,並提高它們的可靠性。API設計就是其中的一種實踐。在21世紀的軟體開發中,希望這本書可以對你的開發進行指導!讓我們的NetBeans API設計探索成為供你學習的案例,讓我們總結的經驗幫助你消除類似的錯誤。回顧1997年,我們踏上了崎嶇不平的探索之路,走過了崢嶸歲月,今天,希望讀者藉助於這本書,一帆風順地通過API設計的艱難險阻,而不必重複我們的曲折經歷。
目錄
《軟體框架設計的藝術》
第一部分 理論與理由
第1章 軟體開發的藝術 4
1.1 理性主義,經驗主義以及無緒 4
1.2 軟體的演變過程 6
1.3 大型軟體 8
1.4 漂亮,真理和優雅 9
1.5 更好的無緒 12
第2章 設計api的動力之源 14
2.1分散式開發14
2.2 模組化應用程式 16
2.3 交流互通才是一切 20
2.4 經驗主義編程方式 22
2.5 開發第一個版本通常比較容易 24
第3章 評價api好壞的標準 26
3.1 方法和欄位簽名 26
3.2 檔案及其內容 27
3.3 環境變數和命令行選項 29
3.4 文本信息也是api 30
3.5 協定 32
.3.6 行為 35
3.7 國際化支持和信息國際化 35
3.8 api的廣泛定義 37
3.9 如何檢查api的質量 37
3.9.1 可理解性 37
3.9.2 一致性 38
3.9.3 可見性 39
3.9.4 簡單的任務應該有簡單的方案 40
3.9.5 保護投資 40
第4章 不斷變化的目標 42
4.1 第一個版本遠非完美 42
4.2 向後兼容 43
4.2.1 原始碼兼容 43
4.2.2 二進制兼容 44
4.2.3 功能兼容——阿米巴變形蟲效應 50
4.3 面向用例的重要性 52
4.4 api設計評審 55
4.5 一個api的生命周期 56
4.6 逐步改善 60
第二部分 設計實戰
第5章 只公開你要公開的內容 67
5.1 方法優於欄位 68
5.2 工廠方法優於構造函式 70
5.3 讓所有內容都不可更改 71
5.4 避免濫用setter方法 72
5.5 儘可能通過友元的方式來公開功能 73
5.6 賦予對象創建者更多權利 77
5.7 避免暴露深層次繼承 82
第6章 面向接口而非實現進行編程 85
6.1 移除方法或者欄位 87
6.2 移除或者添加一個類或者接口 88
6.3 向現有的繼承體系中添加一個接口或者類 88
6.4 添加方法或者欄位 88
6.5 java中接口和類的區別 90
6.6 弱點背後的優點 91
6.7 添加方法的另一種方案 92
6.8 抽象類有沒有用呢 94
6.9 要為增加參數做好準備 95
6.10 接口vs.類 97
第7章 模組化架構 98
7.1 模組化設計的類型 100
7.2 組件定位和互動 103
7.3 編寫擴展點 116
7.4 循環依賴的必要性 117
7.5 滿城儘是lookup 121
7.6 lookup的濫用 126
第8章 設計api時要區分其目標用戶群 129
8.1 c和java語言中如何定義api和spi 129
8.2 api演進不同於spi演進 131
8.3 java.io.writer這個類從jdk 1.4到jdk 5的演進 131
8.4 合理分解api 143
第9章 牢記可測試性 147
9.1 api設計和測試 148
9.2 規範的光環正在褪去 151
9.3 好工具讓api設計更簡單 153
9.4 兼容性測試套件155
第10章 與其他api協作 158
10.1 謹慎使用第三方api 158
10.2 只暴露抽象內容 162
10.3 強化api的一致性 164
10.4 代理和組合 168
10.5 避免api的誤用 176
10.6 不要濫用javabeans那種監聽器機制 180
第11章 api具體運行時的一些內容 184
11.1 不要冒險 186
11.2 可靠性與無緒 189
11.3 同步和死鎖 191
11.3.1 描述執行緒模型 192
11.3.2 java monitors中的陷阱 193
11.3.3 觸發死鎖的條件 196
11.3.4 測試死鎖 201
11.3.5 對條件競爭進行測試 204
11.3.6 分析隨機故障206
11.3.7 日誌的高級用途 208
11.3.8 使用日誌記錄程式控制流程 210
11.4 循環調用的問題 215
11.5 記憶體管理 218
第12章 聲明式編程 223
12.1 讓對象不可變 225
12.2 不可變的行為 229
12.3 文檔兼容性 230
第三部分 日常生活
第13章 極端的意見有害無益 236
13.1 api必須是漂亮的 237
13.2 api必須是正確的 237
13.3 api應該儘量簡單 240
13.4 api必須是高性能的 242
13.5 api必須絕對兼容 242
13.6 api必須是對稱的 245
第14章 api設計中的矛盾之處 247
14.1 api設計中的自相矛盾 248
14.2 背後隱藏的工作 251
14.3 不要害怕發布一個穩定的api 252
14.4 降低維護費用 255
第15章 改進api 258
15.1 讓有問題的類庫重新煥發活力 259
15.2 自覺地升級與無意識地被迫升級 265
15.3 可選的行為 268
15.4 相似api的橋接和共存 274
第16章 團隊協作 286
16.1 在提交代碼時進行代碼評審286
16.2 說服開發人員為他們的api提供文檔 290
16.3 盡職盡責的監控者 292
16.4 接受api的補丁 297
第17章 利用競賽遊戲來提升api設計技巧 300
17.1 概述 300
17.2 第一天 301
17.2.1 非public類帶來的問題 304
17.2.2 不可變性帶來的問題 304
17.2.3 遺漏實現的問題 308
17.2.4 返回結果可能不正確的問題 309
17.2.5 第一天的解決方案 310
17.3 第二天 313
17.3.1 我想修正犯下的錯誤 316
17.3.2 第二天的解決方案 317
17.4 第三天:評判日 320
17.5 也來玩下這個遊戲吧 327
第18章 可擴展visitor模式的案例 328
18.1 抽象類 331
18.2 為改進做好準備 333
18.3 默認的遍歷 334
18.4 清楚地定義每個版本 337
18.5 單向改進 339
18.6 使用接口時的數據結構 340
18.7 針對用戶和開發商的visitor模式 341
18.8 三重調度 343
18.9 visitor模式的圓滿結局 345
18.10 語法小技巧 346
第19章 消亡的過程 348
19.1 明確版本的重要性 349
19.2 模組依賴的重要性 349
19.3 被移除的部分需要永久保留嗎 352
19.4 分解龐大的api 352
第20章 未來 356
20.1 原則性內容 357
20.2 無緒長存 358
20.3 api設計方法論 360
20.4 程式語言的演變 361
20.5 教育的作用 363
20.6 共享 365
參考書目 366