Java的API設(shè)計實踐
Introduction
了解在設(shè)計Java API時應(yīng)該應(yīng)用的一些API設(shè)計實踐。通常,這些實踐很有用,并確保API可以在模塊化環(huán)境中正確使用,例如OSGi和Java平臺模塊系統(tǒng)(JPMS)。有些做法是規(guī)定性的,有些則是禁止性的。當(dāng)然,其他良好的API設(shè)計實踐也適用。
OSGi環(huán)境使用Java類加載器概念提供模塊化運行時強制類型可見性( visibility )的封裝。每個模塊都有自己的類加載器,它會被連接到其他模塊的類加載器,以此來共享導(dǎo)出的包并使用導(dǎo)入的包。
Java 9引入了JPMS,它是一個模塊化平臺,使用了Java語言規(guī)范中的 access control 概念來強制執(zhí)行類型的可達(dá)性( accessibility )的 封裝。每個模塊定義導(dǎo)出哪些包,因此可由其他模塊訪問。默認(rèn)情況下,JMPS層中的模塊都駐留在同一個類加載器中。
包可以包含API。API包有兩種角色: API consumers and API providers 。
在以下設(shè)計實踐中,我們將討論包的公共部分。程序包中非public或非protected的成員和類型,在程序包之外是不可訪問的,因此它們是程序包的實現(xiàn)細(xì)節(jié)。
Java包必須是一個內(nèi)聚,穩(wěn)定的單元
必須設(shè)計Java包以確保它是一個內(nèi)聚、穩(wěn)定的單元。在模塊化Java中,包是模塊之間的共享實體。一個模塊可以導(dǎo)出包,以便其他模塊可以使用該包。由于包是模塊之間共享的單元,因此包必須具有內(nèi)聚性,因為包中的所有類型都必須與包的特定用途相關(guān)。像java.util這樣的包是不鼓勵的,因為這種包中的類型通常彼此沒有關(guān)系。這樣的非內(nèi)聚的包可能導(dǎo)致許多依賴性問題,因為包的不相關(guān)部分引用其他不相關(guān)的包,并且修改包的一個部分會影響依賴這個包的所有模塊,即使模塊實際上可能不使用被修改的這部分。
由于包是單元共享,因此其內(nèi)容必須是眾所周知的,并且包含的API僅在兼容方式中隨著包在未來版本的發(fā)展而變化。這意味著包不能支持API超集或子集;例如,javax.transaction就是一個內(nèi)容不穩(wěn)定的包。包的用戶必須能夠知道包中哪些類型是可用的。這也意味著包應(yīng)該由單個實體(例如,jar文件)提供,而不是跨多個實體分開,因為包的用戶必須知道整個包的存在。
此外,包必須以一種兼容的方式發(fā)展。因此,應(yīng)該對包進(jìn)行版本控制,并且其版本號必須根據(jù)semantic versioning 規(guī)則進(jìn)行演變。
但最近我意識到包的主要版本更改的語義版本控制建議是錯誤的。包演變必須是功能的增加。在語義版本控制中,這增加了次要版本。當(dāng)您刪除功能時,即對包進(jìn)行不兼容的更改,您必須移動到新的包名稱,使原始包仍然兼容。要了解為什么這很重要且必要,請參閱本文 Semantic Import Versioning for Go 。這兩種情況都適用于在對包進(jìn)行不兼容的更改時轉(zhuǎn)移到新包名而不是更改主要版本的情況。
包間耦合最小化
包中的類型可以引用其他包中的類型。例如,方法的參數(shù)類型和返回類型以及字段的類型都可能引用其他包的類型。這種包間耦合創(chuàng)造了所謂的包與包之間的 uses關(guān)系 。這意味著API consumer必須使用與API provider相同的引用包,以便他們理解引用的類型。
通常,我們希望最小化包間耦合以最小化對包的使用約束。這簡化了OSGi環(huán)境中的布線分辨率,并***限度地減少了依賴扇出,簡化了部署(This simplifies wiring resolution in the OSGi environment and minimizes dependency fan-out simplifying deployment)。
接口比類更受歡迎
對于API,接口比類更受歡迎。這是一種相當(dāng)常見的API設(shè)計實踐,對模塊化Java也很重要。對接口的實現(xiàn)很自由,一個接口可以有多個實現(xiàn)。接口對于將API consumer與API provider分離是很重要的。它使得一個包含API的包,既可以被API consumer使用,也可以被API provider使用。通過這種方式,API consumer與API provider沒有直接的依賴關(guān)系。它們都只依賴于API包。
抽象類有時是一種有效的設(shè)計選擇,但通常接口是***,特別是考慮到最近接口添加了default methods這一改進(jìn).
***,API通常需要許多小的具體類,例如事件類型和異常類型。這很好,但類型通常應(yīng)該是不可變的,不適合API使用者進(jìn)行子類化。
避免 statics
應(yīng)該在API中避免使用靜態(tài)。類型不應(yīng)該有靜態(tài)成員。應(yīng)避免使用靜態(tài)工廠。應(yīng)該將實例創(chuàng)建與API分離。例如,API consumer應(yīng)該通過依賴注入或?qū)ο笞员恚ㄈ鏞SGi服務(wù)注冊表或者JPMS的java.util.ServiceLoader)來接收API類型的對象實例.
避免靜態(tài)也是制作可測試API的好方法,因為靜態(tài)不容易被模擬。
Singletons
有時在API設(shè)計中有單例對象。但是,對單例對象的訪問不應(yīng)該像靜態(tài)一樣通過靜態(tài)getInstance方法或靜態(tài)字段來訪問。當(dāng)需要單個對象時,該對象應(yīng)該由API定義為單例,并通過依賴注入或如上所述的對象注冊表提供給API consumer。
避免類加載器假設(shè)
API通常具有可擴展性機制,API consumer可以提供API provider必須加載的類的名稱。API provider然后必須使用Class.forName(可能使用的是線程上下文類加載器)來加載類。這種機制保證了從API provider(或線程上下文類加載器)到API consumer的類可見性。 API設(shè)計必須避免類加載器假設(shè)。模塊化的一個要點是類型封裝。一個模塊(例如,API provider)必須不具有對另一個模塊(例如,API consumer)的實現(xiàn)細(xì)節(jié)的可見性/可訪問性。
API設(shè)計必須避免在API consumer和API provider之間傳遞類名,并且必須避免關(guān)于類加載器層次結(jié)構(gòu)和類型可見性/可訪問性的假設(shè)。為了提供可擴展性模型,API設(shè)計應(yīng)該讓API consumer將類對象或更好的實例對象傳遞給API provider。這可以通過API中的方法或通過對象注冊表(例如OSGi服務(wù)注冊表)來完成。見 whiteboard pattern .
java.util.ServiceLoader類,當(dāng)在JPMS模塊中沒有使用時,也會受到類加載器假設(shè)的影響,因為它假定所有提供者都可以從線程上下文類加載器或提供的類加載器中看到。雖然JPMS允許模塊聲明聲明模塊提供或使用ServiceLoader managed service,但在模塊化環(huán)境中通常不會出現(xiàn)這種假設(shè) .
不要假設(shè)***性
許多API設(shè)計只假設(shè)一個構(gòu)造階段,其中對象被實例化并添加到API中,但忽略了在動態(tài)系統(tǒng)中可能發(fā)生的破壞階段。 API設(shè)計應(yīng)該考慮對象可以來,他們可以去。例如,大多數(shù)listener API允許添加和刪除listener。但是許多API設(shè)計只假設(shè)添加了對象并且從未刪除過。例如,許多依賴注入系統(tǒng)無法撤回注入的對象。
在OSGi環(huán)境中,可以添加和刪除模塊,因此可以適應(yīng)這種動態(tài)的API設(shè)計非常重要。該 OSGi Declarative Services specification 定義了OSGi的依賴注入模型,它支持這些動態(tài),包括注入對象的撤銷。
針對provider和consumer劃分API
如簡介中所述,API包的客戶端有兩個角色:API consumer和API provider。 API consumer使用API,API provider實現(xiàn)API。對于API中的接口(和抽象類)類型,重要的是API設(shè)計清楚地記錄哪些類型僅由API provider實現(xiàn),而API consumer不可以實現(xiàn)。為了方便記憶,我們把API provider需要實現(xiàn)的部分記為P,把API consumer需要實現(xiàn)的部分記為C。例如,偵聽器接口通常由API consumer實現(xiàn),并且實例傳遞給API provider。
API provider對API 中P部分和C部分更改都很敏感。API provider必須實現(xiàn)API中P部分的類型的任何新更改,并且必須了解C部分的任何新更改。 API consumer通??梢院雎訟PI中P部分的更改,除非它想要更改以調(diào)用新函數(shù)。但API consumer對API中C部分的更改很敏感,可能需要修改才能實現(xiàn)新功能。例如,在javax.servlet package, ServletContext由API provider(如servlet容器)實現(xiàn)。為ServletContext添加新方法將要求更新所有API provider以實現(xiàn)新方法,但API consumer不必更改,除非他們希望調(diào)用新方法。然而Servlet由API consumer實現(xiàn),為Servlet添加新方法將要求修改所有API consumer以實現(xiàn)新方法,并且還需要修改所有API provider以使用新方法。就這樣ServletContext類似于API的P部分,Servlet類似于API中C部分。
由于通常有許多API consumer和很少的API provider,因此在考慮更改API 中C部分時,API演變必須非常小心。這是因為,您需要更改少數(shù)API provider以支持更新的API,但您不希望在更新API時更改許多現(xiàn)有API consumer。 API consumer只需要在API consumer想要利用新API時進(jìn)行更改。
Conclusion
下次設(shè)計API時,請考慮這些API設(shè)計實踐。然后,您的API將可用于模塊化Java和非模塊化Java環(huán)境。
英文原文: https://developer.ibm.com/articles/api-design-practices-for-java