瞭解在設計Java API時應該應用的一些API設計實踐。一般,這些實踐頗有用,並確保API能夠在模塊化環境中正確使用,例如OSGi和Java平臺模塊系統(JPMS)。有些作法是規定性的,有些則是禁止性的。固然,其餘良好的API設計實踐也適用。html
OSGi環境使用Java類加載器概念提供模塊化運行時強制類型可見性(visibility)的封裝。每一個模塊都有本身的類加載器,它會被鏈接到其餘模塊的類加載器,以此來共享導出的包並使用導入的包。java
Java 9引入了JPMS,它是一個模塊化平臺,使用了Java語言規範中的access control概念來強制執行類型的可達性(accessibility)的封裝。每一個模塊定義導出哪些包,所以可由其餘模塊訪問。默認狀況下,JMPS層中的模塊都駐留在同一個類加載器中。api
包能夠包含API。API包有兩種角色:API consumers and API providers。oracle
在如下設計實踐中,咱們將討論包的公共部分。程序包中非public或非protected的成員和類型,在程序包以外是不可訪問的,所以它們是程序包的實現細節。ide
必須設計Java包以確保它是一個內聚、穩定的單元。在模塊化Java中,包是模塊之間的共享實體。一個模塊能夠導出包,以便其餘模塊可使用該包。因爲包是模塊之間共享的單元,所以包必須具備內聚性,由於包中的全部類型都必須與包的特定用途相關。像java.util這樣的包是不鼓勵的,由於這種包中的類型一般彼此沒有關係。這樣的非內聚的包可能致使許多依賴性問題,由於包的不相關部分引用其餘不相關的包,而且修改包的一個部分會影響依賴這個包的全部模塊,即便模塊實際上可能不使用被修改的這部分。模塊化
因爲包是單元共享,所以其內容必須是衆所周知的,而且包含的API僅在兼容方式中隨着包在將來版本的發展而變化。這意味着包不能支持API超集或子集;例如,javax.transaction就是一個內容不穩定的包。包的用戶必須可以知道包中哪些類型是可用的。這也意味着包應該由單個實體(例如,jar文件)提供,而不是跨多個實體分開,由於包的用戶必須知道整個包的存在。函數
此外,包必須以一種兼容的方式發展。所以,應該對包進行版本控制,而且其版本號必須根據semantic versioning規則進行演變。測試
但最近我意識到包的主要版本更改的語義版本控制建議是錯誤的。包演變必須是功能的增長。在語義版本控制中,這增長了次要版本。當您刪除功能時,即對包進行不兼容的更改,您必須移動到新的包名稱,使原始包仍然兼容。要了解爲何這很重要且必要,請參閱本文Semantic Import Versioning for Go。這兩種狀況都適用於在對包進行不兼容的更改時轉移到新包名而不是更改主要版本的狀況。spa
包中的類型能夠引用其餘包中的類型。例如,方法的參數類型和返回類型以及字段的類型均可能引用其餘包的類型。這種包間耦合創造了所謂的包與包之間的uses關係。這意味着API consumer必須使用與API provider相同的引用包,以便他們理解引用的類型。線程
一般,咱們但願最小化包間耦合以最小化對包的使用約束。這簡化了OSGi環境中的佈線分辨率,並最大限度地減小了依賴扇出,簡化了部署(This simplifies wiring resolution in the OSGi environment and minimizes dependency fan-out simplifying deployment)。
對於API,接口比類更受歡迎。這是一種至關常見的API設計實踐,對模塊化Java也很重要。對接口的實現很自由,一個接口能夠有多個實現。接口對於將API consumer與API provider分離是很重要的。它使得一個包含API的包,既能夠被API consumer使用,也能夠被API provider使用。經過這種方式,API consumer與API provider沒有直接的依賴關係。它們都只依賴於API包。
抽象類有時是一種有效的設計選擇,但一般接口是首選,特別是考慮到最近接口添加了default methods這一改進.
最後,API一般須要許多小的具體類,例如事件類型和異常類型。這很好,但類型一般應該是不可變的,不適合API使用者進行子類化。
應該在API中避免使用靜態。類型不該該有靜態成員。應避免使用靜態工廠。應該將實例建立與API分離。例如,API consumer應該經過依賴注入或對象註冊表(如OSGi服務註冊表或者JPMS的java.util.ServiceLoader)來接收API類型的對象實例.
避免靜態也是製做可測試API的好方法,由於靜態不容易被模擬。
有時在API設計中有單例對象。可是,對單例對象的訪問不該該像靜態同樣經過靜態getInstance方法或靜態字段來訪問。當須要單個對象時,該對象應該由API定義爲單例,並經過依賴注入或如上所述的對象註冊表提供給API consumer。
API一般具備可擴展性機制,API consumer能夠提供API provider必須加載的類的名稱。API provider而後必須使用Class.forName(可能使用的是線程上下文類加載器)來加載類。這種機制保證了從API provider(或線程上下文類加載器)到API consumer的類可見性。 API設計必須避免類加載器假設。模塊化的一個要點是類型封裝。一個模塊(例如,API provider)必須不具備對另外一個模塊(例如,API consumer)的實現細節的可見性/可訪問性。
API設計必須避免在API consumer和API provider之間傳遞類名,而且必須避免關於類加載器層次結構和類型可見性/可訪問性的假設。爲了提供可擴展性模型,API設計應該讓API consumer將類對象或更好的實例對象傳遞給API provider。這能夠經過API中的方法或經過對象註冊表(例如OSGi服務註冊表)來完成。見whiteboard pattern.
java.util.ServiceLoader類,當在JPMS模塊中沒有使用時,也會受到類加載器假設的影響,由於它假定全部提供者均可以從線程上下文類加載器或提供的類加載器中看到。雖然JPMS容許模塊聲明聲明模塊提供或使用ServiceLoader managed service,但在模塊化環境中一般不會出現這種假設 .
許多API設計只假設一個構造階段,其中對象被實例化並添加到API中,但忽略了在動態系統中可能發生的破壞階段。 API設計應該考慮對象能夠來,他們能夠去。例如,大多數listener API容許添加和刪除listener。可是許多API設計只假設添加了對象而且從未刪除過。例如,許多依賴注入系統沒法撤回注入的對象。
在OSGi環境中,能夠添加和刪除模塊,所以能夠適應這種動態的API設計很是重要。該OSGi Declarative Services specification定義了OSGi的依賴注入模型,它支持這些動態,包括注入對象的撤銷。
如簡介中所述,API包的客戶端有兩個角色:API consumer和API provider。 API consumer使用API,API provider實現API。對於API中的接口(和抽象類)類型,重要的是API設計清楚地記錄哪些類型僅由API provider實現,而API consumer不能夠實現。爲了方便記憶,咱們把API provider須要實現的部分記爲P,把API consumer須要實現的部分記爲C。例如,偵聽器接口一般由API consumer實現,而且實例傳遞給API provider。
API provider對API 中P部分和C部分更改都很敏感。API provider必須實現API中P部分的類型的任何新更改,而且必須瞭解C部分的任何新更改。 API consumer一般能夠忽略API中P部分的更改,除非它想要更改以調用新函數。但API consumer對API中C部分的更改很敏感,可能須要修改才能實現新功能。例如,在javax.servlet package, ServletContext由API provider(如servlet容器)實現。爲ServletContext添加新方法將要求更新全部API provider以實現新方法,但API consumer沒必要更改,除非他們但願調用新方法。然而Servlet由API consumer實現,爲Servlet添加新方法將要求修改全部API consumer以實現新方法,而且還須要修改全部API provider以使用新方法。就這樣ServletContext相似於API的P部分,Servlet相似於API中C部分。
因爲一般有許多API consumer和不多的API provider,所以在考慮更改API 中C部分時,API演變必須很是當心。這是由於,您須要更改少數API provider以支持更新的API,但您不但願在更新API時更改許多現有API consumer。 API consumer只須要在API consumer想要利用新API時進行更改。
下次設計API時,請考慮這些API設計實踐。而後,您的API將可用於模塊化Java和非模塊化Java環境。
英文原文:https://developer.ibm.com/articles/api-design-practices-for-java
更多文章歡迎訪問 http://www.apexyun.com/
聯繫郵箱:public@space-explore.com
(未經贊成,請勿轉載)