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时进行更改。