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
































