Java的API設計實踐
Introduction
了解在設計Java API時應該應用的一些API設計實踐。通常,這些實踐很有用,并確保API可以在模塊化環境中正確使用,例如OSGi和Java平臺模塊系統(JPMS)。有些做法是規定性的,有些則是禁止性的。當然,其他良好的API設計實踐也適用。
OSGi環境使用Java類加載器概念提供模塊化運行時強制類型可見性( visibility )的封裝。每個模塊都有自己的類加載器,它會被連接到其他模塊的類加載器,以此來共享導出的包并使用導入的包。
Java 9引入了JPMS,它是一個模塊化平臺,使用了Java語言規范中的 access control 概念來強制執行類型的可達性( accessibility )的 封裝。每個模塊定義導出哪些包,因此可由其他模塊訪問。默認情況下,JMPS層中的模塊都駐留在同一個類加載器中。
包可以包含API。API包有兩種角色: API consumers and API providers 。
在以下設計實踐中,我們將討論包的公共部分。程序包中非public或非protected的成員和類型,在程序包之外是不可訪問的,因此它們是程序包的實現細節。
Java包必須是一個內聚,穩定的單元
必須設計Java包以確保它是一個內聚、穩定的單元。在模塊化Java中,包是模塊之間的共享實體。一個模塊可以導出包,以便其他模塊可以使用該包。由于包是模塊之間共享的單元,因此包必須具有內聚性,因為包中的所有類型都必須與包的特定用途相關。像java.util這樣的包是不鼓勵的,因為這種包中的類型通常彼此沒有關系。這樣的非內聚的包可能導致許多依賴性問題,因為包的不相關部分引用其他不相關的包,并且修改包的一個部分會影響依賴這個包的所有模塊,即使模塊實際上可能不使用被修改的這部分。
由于包是單元共享,因此其內容必須是眾所周知的,并且包含的API僅在兼容方式中隨著包在未來版本的發展而變化。這意味著包不能支持API超集或子集;例如,javax.transaction就是一個內容不穩定的包。包的用戶必須能夠知道包中哪些類型是可用的。這也意味著包應該由單個實體(例如,jar文件)提供,而不是跨多個實體分開,因為包的用戶必須知道整個包的存在。
此外,包必須以一種兼容的方式發展。因此,應該對包進行版本控制,并且其版本號必須根據semantic versioning 規則進行演變。
但最近我意識到包的主要版本更改的語義版本控制建議是錯誤的。包演變必須是功能的增加。在語義版本控制中,這增加了次要版本。當您刪除功能時,即對包進行不兼容的更改,您必須移動到新的包名稱,使原始包仍然兼容。要了解為什么這很重要且必要,請參閱本文 Semantic Import Versioning for Go 。這兩種情況都適用于在對包進行不兼容的更改時轉移到新包名而不是更改主要版本的情況。
包間耦合最小化
包中的類型可以引用其他包中的類型。例如,方法的參數類型和返回類型以及字段的類型都可能引用其他包的類型。這種包間耦合創造了所謂的包與包之間的 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使用者進行子類化。
避免 statics
應該在API中避免使用靜態。類型不應該有靜態成員。應避免使用靜態工廠。應該將實例創建與API分離。例如,API consumer應該通過依賴注入或對象注冊表(如OSGi服務注冊表或者JPMS的java.util.ServiceLoader)來接收API類型的對象實例.
避免靜態也是制作可測試API的好方法,因為靜態不容易被模擬。
Singletons
有時在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的依賴注入模型,它支持這些動態,包括注入對象的撤銷。
針對provider和consumer劃分API
如簡介中所述,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時進行更改。
Conclusion
下次設計API時,請考慮這些API設計實踐。然后,您的API將可用于模塊化Java和非模塊化Java環境。
英文原文: https://developer.ibm.com/articles/api-design-practices-for-java