1. 程式人生 > >Java API設計指南[轉載]

Java API設計指南[轉載]

前言:
市場上關於如何設計和編寫優秀Java程式碼的書如此之多,可能要用汗牛充櫝來形容,但是想找到一本如何設計API的書,卻是難之又難。這裡我將把自己一些關於API設計的經驗與大家分享。

分享這些經驗是源於最近我參加了JavaPolis上的一個討論,這個討論是由Elliotte Rusty Harold發起的,是關於設計XOM時的一些原則性問題,討論中的思想交流如此精采,令我受益頗多。雖然這次討論主題是與XOM有關,但是大部分的時間我們都在討論設計XOM API時的一些原則性問題,而這些內容對於API設計而言,則是通用的。這幾年,Java的應用日益廣泛,開源專案也是蒸蒸日上。一本能夠指導開發人員設計和編寫API的好書,可以幫助開發人員設計和編寫更好的API。

在過去的五年中,我一直參與JMX API的修訂及調整,在此過程中,同樣受益頗多。特別在這次討論會,我對Elliotte提出的一些觀點高舉雙手贊同。
接下來的內容是討論會上的一些主要觀點的總結,其中包括一些個人或者是來自他人的經驗,也參考一些相關的文件,希望對大家設計API有所裨益。

下面是個人推薦的一些閱讀和參考資料。
下面給出的網址是Netbeans網站上的一篇關於API設計的優秀文件,
http://openide.netbeans.org/tutorial/api-design.html
Josh Bloch's 的Effective Java作為Java設計的聖經之一,從來都不會被漏下。

設計需要進化
API的價值就在於能夠幫助你完成許多功能,但請不要忘記要持續的改善它,否則它的價值就會逐漸減少。而且要根據使用者的反饋資訊,來改善API,或者說是對API進行演化,另外改進API時要注意的就是在不同版本間要保持相容性,這點至關重要,它也是一個API是否成功的重要標識之一。
如果一個功能以API的方式公佈出來,那麼在釋出以後,它的對外介面就已經固定,無論什麼情況,都不能取消,而且一定要能夠按照原有的約定功能正確執行。如果API不能在版本間保持相容性(譯註:這裡應該指的是向下相容),使用者將會難以接受這樣一個千變萬化的API,最終的結果只能是讓使用者放棄使用你的API。如果你的API被大量的軟體或者模組所使用,又或者被大型軟體使用,這個相容問題也就愈加嚴重。
以一個Java應用程式為例,如果Module1使用了Banana1.0的API,而Module2則使用了Banana2.0的API,現在要同時部署這兩個模組到一個Web Application上,如果Banana2.0對Banana1.0保持相容的話,整個部署就會非常簡單,直接使用Banana2.0就可以了。如果Banana2.0和Banana1.0不相容的話,就很難同時在一個程式中同時使用Module1和Module2(可能要自定義ClassLoader,或者是其它方式,這對於使用者未免有些為難了)。最終的結果可能就是你失去一個使用者,這時的API就不能為他人提供任何價值(譯註:看來它也不能帶來經濟價值了)。
分析Java SE平臺所提供的API,有著嚴格的相容性控制。其根本目標就在於保證使用者在版本升級時,不會導致低版本的程式碼無法執行。這也再次說明,當一個API釋出以後,任何API公開的方法,常量等元素都不能被移出API,否則會嚴重降低API的價值。(譯註:所以個人一向比較懷疑deprecated的價值,因為既然所有的方法都不會被刪除,即使那些被 deprecasted的方法和變數也會被仍然保留,其功能也不應該會被改變,因此從這個角度來說,其方法和變數的使用仍然是安全的。)
說到相容性,必然要談到二進位制相容性,這是指對於已經編譯完成的程式碼,不會因為版本的變更,而致使編譯後的程式碼不能正常執行。比如說你將一個public方法從API中移走,就會無法正常執行,會丟擲NoSuchMethodException這類錯誤。
但原始碼的相容性也不可忽視,在某些特殊情況下,修改API時,會產生原始碼相容問題,導致原始碼無法編譯通過。例如新增一個過載的同名方法,其引數不同,如getName(User)和getName(String),當用戶使用getName(null)時,會因為存在二義性,而產生編譯錯誤,使用者必須給出getName((String)null),來明確標識要呼叫的方法。因此必須尋找一種方式,來保持原始碼的相容性。
通常情況下,如果原始碼不相容,程式碼編譯就無法通過,使用者必須修改原始碼才能保證程式正常執行,但這並不是一個好的解決方案。以J2SE6中的javax.management.StandardMBean為例,這個類的建構函式已經採用泛型了。這樣會使得一些類在建立這個Bean的時候,會出現編譯不能通過的問題,但是解決方案有時卻是非常簡單,往往只需要新增一個cast進行強型別轉換就可以解決這樣的一個編譯問題了。但是更多的時候,需要更復雜的方案才能解決這些編譯問題,例如在一個方法中呼叫第三方的API方法,如果有一天這個API方法的引數被修改了,在修改原始碼時候,可能會發現你的方法中不含有API方法需要的引數,這時就需要修改方法引數。以下是這種情況的示範程式碼。
public void callApi(Parameter1 p1,Parameter2 p2)
{
//do some operations
new ThirdClass().doCallThirdApi(p1);
}
現在第三方的API現在變成了doCallThirdApi(Parameter1 p1,Parameter3 p3)
這時可能需要將方法改成:
public void callApi(Parameter1 p1,Parameter2 p2,Parameter3 p3)
{
//do some operations
new ThirdClass().doCallThirdApi(p1,p3);
}
這樣一個API的改變,很可能引發一個多米諾骨牌事件。
通常情況下,你不知道使用者如何使用API來完成工作。除非能夠確認API的修改對使用者的程式碼不會造成破壞,才可以考慮修改API。,反之如果API的改變會影響使用者現在的程式碼,就必須有一個足夠的理由。只有意識到對API的修改,會嚴重的破壞使用者現有的程式碼,在修改API時才會謹慎地,儘量地保證API相容性。修改API的時候要儘量避免以下幾種情況,以免對使用者的程式碼產生破壞:
降低方法的可見性,如將public變成package或者proteced,又或者將protected變成private。
修改方法的引數,如刪除一個引數、新增一個引數、或者是改變引數的型別,尤以後兩者更為嚴重。

就象業界遊行的經驗之談--“不要使用3.0版本以下的軟體”(譯註:所以我總想把自己的軟體直接釋出為9.9。),同樣的情況對API也是一樣的,前幾個版本的API往往包含有大量的錯誤,這些錯誤不應該被隱藏起來,因為縱然是冰山的底部,也終有浮出水面的一天。因此在正式釋出API 1.0以前,請不要忘記提供若干個0.x版本。對於使用0.x版本的使用者,會有一個比較明確的說明,使用者會清楚的知道當前版本所公佈的API還不穩定,有可能在正式釋出的時候有所更改,0.x版本也不保證相容性。(譯註:以Visual Studio2000 beta2為例,與正式版的差別就非常大,所以0.x版本通常只是用來學習,或者進行技術預言,而不能在產品中使用)。但是一旦1.0版本正式釋出,請記住,就是對API的相容性就做出一個正式的承諾。象JCP組織在對某一個規範推出正式版本以前,通常都會發布若干個草稿版本(如意向草稿,公共預覽草稿,最終建議版本等)。如果方便的話,對於規範性的內容,在釋出API時,提供一個API的實現可能會更有效的推行規範(譯註:因為規範性的內容更多的是以Interface的方式來發布API,大家可以參考一下Interface和Abstract Class,所以提供一個實現往往更好,象sun釋出J2EE規範時,就提供了一個預設的實現。)。
當API的設計和開發到了一定階段以後,可能會發現以前的版本已經出現了一些問題,又或者需要新增新的功能,此時設計人員完全可以重新建立新的API,並放到新的包中,這樣就可以保證那些使用老版本的使用者可以很輕鬆的移植到新版本上,而不會產生問題。請牢記一點:新增新的功能,請不要修改原有的內容。

API的設計目標
設計一個API要達到哪位目標呢?除了相容性以外,也從Elliotte的討論中提出一些目標。

API的正確性必須保證:
以XOM為例,無論使用者如何呼叫API,都不應該產生錯誤的XML文件。再如JMX,不管是註冊一個錯誤的MBean還是併發執行一些操作,又或者MBeans使用了一些特殊的名稱,MBean Server都必須保持狀態的一致性,不能在某個錯誤的MBeans進行了操作以後,整個系統就無法提供服務。
API的易用性:
API必須易於使用。通常易用性一向難以評價。但是有一個辦法可以有效的提高易用性,就是編寫大量範例程式碼,並將其很好的組織在一起,從而為使用者提供API參考。(譯註:個人認為一個好的FAQ可以提供各種API使用的範例。)
另外下列原則也可以用來判斷API的易用性:
是不是總是經常出現一組操作程式碼?(譯註:這裡是指如果有多行程式碼重複被呼叫,說明它們應該被放到一個方法中,避免使用者重複編寫一組程式碼。)
在使用API時,是否需要經常參考JavaDoc或者是原始碼,才能知道應該呼叫哪個方法呢?(譯註:比較理想的情況就是,大部分操作只需要通過類名和方法的名稱就可以明白)。
根據名稱呼叫一個方法,但是該方法所做的事並不是使用者所想要的。(譯註:例如呼叫一個command方法,以為是執行一個操作,但是結果這個方法是做備份用。)
API必須易學:
很大程度上,API的易學和易用性是相似的,一般來說,易用也就易學。如果要使API易學,下列基本原則要遵循的:
API越小就容易學習;
文件應該有範例;
如果方便的話,儘可能將API與一些常用的API保持一致。例如如果要做一個資源訪問的API,儘可能與J2SE中的IO使用一致,自然很容易學習。
(譯註:如果你要關閉一個資源,就象Java的File,Connection一樣,使用close,而不是destroy。)
API的執行速度必須夠快:
Elliotte也是考慮了很久,才給出這一條。但是要在保證API簡單而且正確的前提下,再來考慮API的效能問題。在設計API時,你可能會先使用一種能夠快速實現但是效能不好的方式來實現API,然後再根據實際情況再修改API的實現,以調整效能。至於如何調整效能,絕對不要通過直覺來判斷何種方式能獲得高效能。只能通過正確,嚴格的測試以後,再對效能瓶頸進行優化從而提高效能。(譯註:過早的優化是所有的錯誤根源,這已經是一個普遍認同的觀點,特別是對於Java程式,因為它的JVM越來越快,越來越聰明。)

API必須足夠的小:
這裡所說的小不僅是指編譯後代碼的檔案比較小,而且更重要的是執行時佔用的記憶體要小。之所以提出最小化的概念,還有一個原因:就是因為很容易為API新增新的內容,但是要將一個內容從API中移出就很困難,所以不要隨便向API中新增內容,如果不確定一項內容,就不要將它加入到API中。通過這樣一個建議或者說是限制,可以提醒一個API設計人員更加關注API中最重要的功能,而非一些枝節的問題。
(譯註:許多時候這個最小化原則是很難遵守的,如不變類通常比可變類更好一些,但是它會佔用更多的記憶體,而可變類佔用的記憶體會少些,但要處理執行緒,併發等問題,所以更多時候是一個權衡,大固然不好,小也必就好)。

有一種設計API的方法很常見,但是結果卻令人頭痛,這種方法就是在設計API前,會詳細的考慮每一個使用者的需求,並設計出相應的方法,可能在實現中還要設計一堆的Protected方法,這樣使得使用者可以通過繼承來調整預設實現。為什麼這種方法不好呢?
因為考慮的過於詳細,功能邊界也就越大,所面對的需求也就越多,因此要提供的功能和可供使用者調整的功能也就更加龐大,也就是說這種方法會使得API包含很多的功能,最終就是API膨脹性的增長。
事實上API中包含的功能越多,也就更加難以學習和使用,而且其學習難度往往是以幾何級數進行增長,而不是線性增長。想像一下,理解並學習使用10個類,100個方法的API對於一個程式設計師並不困難,大概一天就可以完成,但是對於一個100個類,1000個方法的API,即使對於一個非常優秀的程式設計師,估計10天的時間是不足以完全理解。
另外在一個龐大的API中,如何才能儘快的找到最重要的內容,如何找到完成所需功能的方案,一直都是API中設計中的一個難題。
JavaDoc工具給使用者帶來了許多的方便,但一直以來它都沒有解決如何學習和使用一個龐大API庫的方法。JavaDoc將指定包中的所有類都放置在一起,並且將一個類中的所有方法放置在一起(譯註:這裡指的是allclasses-frame.html,index-all.html),縱然是天才,看到成千上萬的方法和類也只能抱頭而泣了。現在看來,只能寄希望於JSR260標準,希望它能夠有效地增強JavaDoc工具,從而可以獲得API的完整檢視,能夠更加巨集觀地表示API,如果這樣,即使是很龐大的API包,也不會顯得擁擠,從而也就更加容易理解和使用。

另外API越大,出現的錯誤可能性也就越多,與前面使用的難度一樣,錯誤的數量也是呈幾何級數增長而不是線性增長。對於小的API,投入相同的人力進行編碼和測試時,可以獲得更好的產出。

如果設計的API過於龐大,必然會包含了許多不必要的方法,至少有許多public的類和方法,對於大部分使用者是用不到,也會佔用更多的記憶體,並降低執行效率。這違反了通常的一個設計原則:“不要讓使用者為他使用不到的功能付出代價”。

正確的解決方案是在現在的例子上來設計API(譯註:更象原型演化的方式)。先來想像一下:一個使用者要使用API來解決何種問題呢,併為解決這些問題新增足夠的類和方法。然後將與之無關的內容全部移除,這樣可以自己來檢查這些API的有用性,這種方法還會帶來一個有用的附加功能,它可以更加有效測試程式碼。同時也可以將這些例子與同伴分享。(譯註,這個概念與測試先行是非常相似的)。

介面的功能被誇大了:
在Java的世界,有一些API的設計原則是很通用的,如儘量使用介面的方式來表達所有的API(不要使用類來描述API)。介面自有它的價值,但是將所有的API都通過介面來表示並不見得總是一個好的設計方案。在使用一個介面來描述API時,必須有一個足夠的理由。下面給出了一些理由:

1、介面可以被任何人所實現。假設String是一個介面而非類,永遠都無法確認使用者提供的String實現能夠遵循你希望的規則:字串類是一個不變數;它的hashCode是按照一定的演算法規則來返回數字;而length永遠都不會是一個負數等。如果真的由使用者來提供一個String的實現,可以想象程式碼中要加入多少異常處理程式碼和相關的判斷語句才能保證程式的健壯性。
實踐告訴我們,如果API完全是由介面來定義,使用者在使用這些API時會發現不得不進行大量的強制轉型(譯註:個人認為,強制轉型並不是因為API是通過介面來定義引起的,而是不好的API定義引起的,而且強制轉型從程式的設計角度幾乎是無法避免的,除非所有的子類都不新增任何新的功能,而這一點與前面的抽象類演化又是矛盾的)。

2、介面不可能擁有建構函式或者是static方法。如果需要介面的例項,不可能直接例項化介面,只能通過某種方式,可能是new也可能是通過引數傳遞的方式來獲得一個介面的具體實現物件。當然,這個物件可能是由你來實現的,也可能是由第三方供應商開發的。如果Integer是一個介面而非一個類,則無法通過new Integer(int)來構造一個Integer物件,可能會通過一個IntegerFactory.newInteger(int)來獲得一個Integer物件例項,天啊,API變得更加複雜和難以理解了。
(譯註:個人認為原文作者有些過於極端化了,因為Java不是一個純粹的API,它同時是一種語言,一個平臺,所以提供的String和Integer,都是作為基礎型別來提供。雖然同意作者的觀點,但是作者使用的上述例子,個人認為不是很有說服力。)

3、介面無法進行演化。假設在2.0版本的API中為一個介面新增一個方法,多米諾骨牌倒了,大量直接實現了這個介面的類,根本就無法通過編譯,直到實現了這個方法為止。 當然可以在呼叫這個新方法的時候,通過捕捉AbstractMethodError這個異常來保證二進位制的相容性,但是如此笨重的方法,實在不是智者所為。除非告訴使用者說千萬不要直接實現這個介面,請先繼承所提供的那個抽象類,這樣做就不會有問題了,不過使用者會尖銳的責問:那為什麼要提供這樣一個介面,不如直接提供一個抽象類算了。

4、介面是不可以被序列化的,雖然Java的序列化存在許多問題,但是仍然不可避免的要用到它。象JMX的API就嚴重依賴於序列化介面。序列化是針對序列化介面的子類來處理的,當一個可序列化的物件被反序列化時,就有會一個相同的新物件被重新創建出來。如果這個子類沒有提供一個public建構函式,那麼可能很難在程式中使用這個功能,因為只能反序列化而不能進行序列化。而且在反序列化時,只能使用介面進行強制轉型。如果要序列化的內容是一個類,那就不需要提供這樣的序列化介面。

當然,對於下列情況,介面還是非常有用的:

1、回撥:如果功能完全由使用者來實現,在這種情況下,介面顯然比抽象類更加合適。例如Runnable介面。特別是那些通常只含有一個方法的,往往是介面,而非類(譯註:最常用的就是各種Listener)。如果一個介面中包含有大量的抽象方法,使用者在實現這個介面的時候,就不得不必須實現一些空方法。所以對於有多個方法的介面,建議提供一個抽象類,這樣在介面中新增新的方法,而不需要強迫使用者實現新的方法。(譯註:看來作者很推薦使用介面+基類的方式來編寫API,不過Java SE本身就是這樣做的,例如MouseAdapter,KeyAdapter等。個人認為,如果是規範,當然最好是介面,象J2EE規範;如果是框架,或者是功能包,還是建議使用介面+基類的方式。所謂的回撥其實是SPI(Service Provider Interface)的一種)。

2、多重繼承:在一個繼承體系比較深的結構裡,可以通過介面來實現多重繼承。Comparable是一個最好的例子,比如Integer實現了Comparable介面,因為Integer的父類是Number,所以通過介面的方式實現了多重繼承。但是在Java的核心類庫中,這樣的經典例子並不多。通常一個類實現了多個介面並不一定是一個好的設計,因為這往往將許多責任強加在一個類上,有違基本的設計原則,而且很容易產生重複程式碼。如果真的需要這樣一個功能,使用一個匿名類或者是一個內部類來實現這些介面,或者使用一個抽象類作為基類也是不錯的方案。

3、動態代理:價值不可估量的動態代理類java.lang.reflect.Proxy class 可以在執行的時候根據介面生成實現的內容。它將對一個介面的呼叫轉換成對某一個物件具體方法的呼叫,非常的靈活,可以有效的減少程式碼重複。但是對於一個抽象類,就不可能動態生成一個代理物件了。如果喜歡使用動態代理技術,那麼使用介面對軟體開發是非常有效的。(CGLIB有時可以有效地對抽象類實現動態代理,但是有許多限制,而且其文件也較少。)

謹慎的分包:
Java在控制類和方法的可見性上,所支援的方式實在乏善可陳,除了public,proteced,private以外,就只能通過pakcage來控制。如果一個類或者方法想讓外部的包可見,則所有的類和方法都可以訪問它了,不能指定外部哪些類可以訪問自身。這就意味著如果將API分成若干個包進行釋出,則必須對這些包詳細設計,避免減少API的公開性。 最簡單的方法當然是把所有的API放在一個包中,這樣很容易通過package來降低訪問性。如果API不超過30個類,這個方案簡直是完美。
但事事往往不盡如人事,經常API非常大,不適合放在一個包中。這時候可能要不得不進行私有分包了(這裡的私有與private不一樣的,只是一種偽私有),私有隻是不在JavaDoc中輸出這些類的文件資訊。如果檢視JDK,會發現許多以sun.*或者com.sun.*打頭的包相關的文件資訊並沒有包含在JDK的JavaDoc中。如果開發人員主要通過JavaDoc來使用API,那可能根本不會注意到這些包的存在,只有檢視原始碼或者分析API的人才能看到這些API內容。即使發現了這些沒有通過文件公開的類,也不建議使用它們,因為不通過文件公開的API,往往也意味著它可能會隨著時間的改變進行演化,也有可能在演化的過程中不能保持相容性。(譯註:象C#支援assembly的訪問機制,個人就感覺很好,象Osgi支援Bundle,允許定製輸出類也是不錯誤的解決方案,不過前者是語言級,而後者是框架級。)

還有將包隱藏起來的一個方式就是在包的名稱中包含internal。所以Banana的API可能會有公開的包com.example.banana, com.example.banana.peel,也可能還有s com.example.banana.internal 和com.example.banana.internal.peel。

別忘記所謂的私包同樣是可以訪問的,更多時候這樣的私包只是出於安全的考慮,建議使用者不要隨便訪問,並沒有任何語言級的約束。還有一些技術可以解決這個問題,例如NetBeans的API教程中就給出了一種解決方案。在JMX的API中,則使用了另外一種方式。象javax.management.JMX這個類,就只提供了static方法而沒有提供public建構函式。這也就意味著你不能例項化這樣一個類。(譯註:不明白這個例子的意義。)

下面在設計JMX時的一些技巧
不變類是一個很好的設計,如果一個類可以設計成不變類,就不要用可變類!如果詳細瞭解這樣設計的原因,請參見《Effective Java》的第十三條。如果沒有讀過這本書,很難設計出好的API。
另外欄位資訊應該是private的,只有static和final修飾的欄位資訊才能變成public,允許外部訪問。這一條是一個非常基礎的原則,這裡提到這個原則,只是因為在早期的API設計時,有些API違反了這個原則,這裡不再給出一個例子了。

避免奇怪的設計。對於Java程式碼,已經有了許多約定俗成的方法了,如get/set方法,標準的異常類。即使覺得有了更好的方法,也儘量避免使用這些方法。如果使用了一些奇怪的方法名稱,這樣使用API的使用者必須學習新的內容,不能按照原有的習慣來理解程式碼,會增加學習成本,也會增加誤用的可能。

再舉個例子,象java.nio以及java.lang.ProcessBuilder就是一個不好的設計,它不使用getThing()和setThing()方法這種方式,而使用了thing()和thing(T)這兩個方法。許多人認為這是一個不錯的設計方法,但是這樣違反了常用的方法設計原則,強迫使用者來學習這種API。(譯註:java.nio和java.lang.ProcessBuilder是指JDK6中的包,害得我在JDK1.4中找了半天,參見http://download.java.net/jdk6/doc/api/java/lang/ProcessBuilder.html,這裡所謂的thing和Thing也不是真有這個方法和類,而是ProcessBuilder中的command和command(List)等多個方法。)

不要實現Cloneable, 即使想某一個類支援物件的複製,這個介面也沒有太多的價值,如果真想支援複製功能,提供一個複製建構函式或者是一個static方法來複制物件,又或者提供一個static的工廠方法來建立物件,也會更加有效。例如想讓Banana這個類擁有clone的能力,可以使用程式碼如下:
public Banana(Banana b) { // copy constructor
this(b.colour, b.length);
}
// ...or...
public static Banana newInstance(Banana b) {
return new Banana(b.colour, b.length);
}
建構函式的優點就在於子類可以呼叫父類的建構函式。static函式則是可以返回具體類的子類實現。
《Effective Java》書中第十條則給出了clone()帶來的痛苦。
(譯註:個人不同意這個觀點,我覺得clone非常有用,特別是在多執行緒的處理中,我會再撰寫關於clone方面的文章,而且前面提到的缺點也都是可以通過一些設計上的技巧來改正。)

異常應該儘可能的是unchecked型別的,《Effective Java》書中第41條則給出了詳細的說明。如果當前API只能丟擲異常,而且開發人員可以對異常進行處理,如釋放資源,就可以使用Checked異常。因此所謂的Checked異常就是API內部與外部開發人員進行問題互動的一種方式。如網路異常,檔案異常或者是UI異常等資訊。如果是輸入引數不合法,或者是一個物件的狀態不正確,就應該使用Unchecked異常。

一個類如果不是抽象類,就應當是final類不可被繼承。《Effective Java》第15章給出了足夠的理由,同時也建議每個方法在預設情況下都應該是final(目前Java正好相反)(譯註:這點我也贊成,覺得方法預設為final更好,但是目前Java發展到當前情況下,已經不可能大規模的更改了,不能不說是Java語言的一個遺憾之處,C#這一點處理的更好,預設為final,後面的留言也提到這個了)。如果一個方法可以被覆蓋,一定要在文件中清楚的描述這個方法被覆蓋後帶來的後果,最好還能提供一些例子程式進行演示以避免開發人員誤用。

總結:
設計需要演化,否則會降低它的價值。
先保證API的正確性,在此基礎上再追求簡單和高效
介面並不如想像中的那麼有用。
謹慎分包可以帶來更多的價值。
不要忘記閱讀《Effective Java》(譯註:難道作者和Josh Bloch's有分贓協議不成。)

以下是當前文章一些討論的意見(因為比較多,所以我沒有全部翻譯,但是國外技術論壇上的一些討論,其價值往往比文章的價值更大,建議可以自行閱讀一下。):

Gregor Zeitlinger寫道:
如果使用作者給出的API標準,C#很多方面是不是做的更好呢。
C#的方法在預設情況是final的,不可被過載。(譯註:個人意見,在這一點上C#比Java更好,而且,引數預設就應該是final,因為java引數是傳值的,所以也不應該改變)。
C#只有unchecked exception(新的規範中又重新提出了checked exception)。(譯註:個人認為checked exception的價值還是很大的,不過在Java中,有些被誤用了)。

Eamonn McManus 寫道:
我個人不認為讓大部分方法都成為final是一個好的設計方案。根據我個人的經驗,這種處理方式將會嚴重的降低程式碼的複用度。
如果更極端的說一下:也許不應該有private方法,所以的方法都應該是protected甚至是public,這樣可以有益於複用度。當然這樣處理帶來的一個明顯缺點就是沒有人知道哪個方法可以被安全的複寫(overrid)。
(譯註:這也太極端了,如果這樣,一個API的規模恐怕會是原來的10倍以上,不要說複用,恐怕怎麼用我都不知道了,想想一下一個1000個類,10000個public方法的API包吧)。

Gregor Zeitlinger寫道:
在什麼情況下我們要同時提供介面和抽象類呢。舉個例子,Java的集合框架中提供了List這個介面和AbstractList這個抽象類。
介面的好處在於
多繼承
其實現子類可以有最大的靈活性
能夠將API的描述資訊與其實現徹底分離

類的好處在於:
提供通用的方法,避免重複程式碼
能夠支援介面的演化

Eamonn McManus 寫道:
Bloch在《Effective Java》中強烈建議在提供一個介面的同時,儘量提供一個實現了該介面的抽象基類。這話在Java集合框架的設計體現的淋漓盡至,他老兄就是Collection框架的主設計師。這個設計的模式在許多場景中都是非常有用的,不過也不要把它當作金科玉律,一言而敝之:不要為模式而模式。
即使你使用了介面+基類的方式,也不能保證你API的演化,除非你只在基類中新增方法,而不在介面中新增方法,這種情況帶來的壞處就是混亂,如果一個類想呼叫這個新新增的方法,因為介面中沒有新增這個方法,所以通過介面是無法呼叫的,那麼只能將它強行轉型,然後再呼叫,但有時候又很難確認你的強行轉型是正確,糟糕的ClassCastException又出現了。除非你能保證所有的子類都繼承這個基類,不過這種情況和中彩票的機會相差不多吧。

現在來談一下unchecked exceptions和C#的問題,許多人都覺得在Java中Checked exceptions並不是一個缺陷,或者說它不是一個嚴重的問題。但我不這樣認為:象IOException這種異常,應該是Checked Exception,以便由編譯器來提醒程式設計師時要正確處理資源問題,這是一件好事,但是在Java中,有大量不必要的異常成為Checked Exception,這些Checked Exception卻給程式設計師帶來了許多麻煩。

Robert Cooper 寫道:
> 即使你使用了介面+基類的方式,也不能保證你API的演化。
我認為,如果可能的話,在為基類新增新方法的同時,也應該在介面中新增新的方法。我向來如此,也沒有出現過什麼問題。
很清楚的一點就是,如果這樣做了,介面的定義改了,如果一個類是直接實現這個介面,就需要實現所有的方法。
Michael Feathers 寫道:
>在什麼環境下我們要同時提供介面和抽象類呢。
>介面+基類並不是可以用於所有的場景!
大部分情況下,我都會使用介面+基類這種方式,不過這種方式也會帶有幾個缺點。
如果你的API比較複雜,很難找到一個準確的入口點來使用你的API。比如說我需要一個IX,但是我要一步步的向下查詢AbstractX,以及相關的實現,這種介面+基類的方式加深了繼承的層次,增加了API的複雜度。