Introduction
了解在设计 Java API 时应该应用的一些 API 设计实践. 通常, 这些实践很有用, 并确保 API 可以在模块化环境中正确使用, 例如 OSGi 和 Java 平台模块系统(JPMS). 有些做法是规定性的, 有些则是禁止性的. 当然, 其他良好的 API 设计实践也适用.
OSGi 环境使用 Java 类加载器概念提供模块化运行时强制类型可见性 ( visibility) 的封装. 每个模块都有自己的类加载器, 它会被连接到其他模块的类加载器, 以此来共享导出的包并使用导入的包.
Java 9 引入了 JPMS, 它是一个模块化平台, 使用了 Java 语言规范中的 access control 概念来强制执行类型的可达性 ( accessibility ) 的封装. 每个模块定义导出哪些包, 因此可由其他模块访问. 默认情况下, JMPS 层中的模块都驻留在同一个类加载器中.
包可以包含 API.API 包有两种角色: API consumersand 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 环境.
来源: http://news.51cto.com/art/201901/590882.htm