了解在设计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
(未经赞成,请勿转载)