作成者: BJ Hargrave
Java API を設計するときに適用する必要がある API 設計慣行のいくつかを理解します。これらのプラクティスは一般に便利で、OSGi や Java Platform Module System (JPMS) などのモジュラー環境で API を適切に使用できるようになります。実践の中には規範的なものもあれば、規範的なものもある。そしてもちろん、他の優れた API 設計手法も適用されます。
OSGi 環境は、Java クラス ローダーの概念を使用して型 可視性 カプセル化を強制するモジュラー ランタイムを提供します。各モジュールには独自のクラス ローダーがあり、エクスポートされたパッケージを共有し、インポートされたパッケージを消費するために、他のモジュールのクラス ローダーに接続されます。
Java 9 で導入されたJPMS は、Java 言語仕様のアクセス制御概念を使用して、型 アクセシビリティ カプセル化を強制するモジュラー プラットフォームを提供します。各モジュールは、どのパッケージをエクスポートし、他のモジュールからアクセスできるかを定義します。デフォルトでは、JMPS レイヤー内のモジュールはすべて同じクラス ローダーに存在します。
パッケージには API を含めることができます。これらの API パッケージには、API コンシューマー と API プロバイダー という 2 つのクライアントのロールがあります。 API コンシューマーは、API プロバイダーによって実装された API を使用します。
以下の設計実践では、パッケージの公開部分について説明します。パブリックまたは保護されていない (つまり、プライベートまたはデフォルトでアクセス可能な) パッケージのメンバーとタイプは、パッケージの外部からアクセスできないため、パッケージの実装の詳細となります。
Java パッケージは、凝集性と安定性のユニットとなるように設計する必要があります。モジュール型 Java では、パッケージはモジュール間の共有エンティティです。 1 つのモジュールがパッケージをエクスポートして、他のモジュールがそのパッケージを使用できるようにすることができます。パッケージはモジュール間の共有の単位であるため、パッケージ内のすべてのタイプがパッケージの特定の目的に関連している必要があるという点で、パッケージは一貫性を持っている必要があります。 java.util のような Grab Bag パッケージは、そのようなパッケージ内の型が互いに無関係であることが多いため、推奨されません。このような非結合パッケージは、パッケージの無関係な部分が他の無関係なパッケージを参照するため、多くの依存関係を引き起こす可能性があり、モジュールが実際にその部分を使用していない場合でも、パッケージの 1 つの側面への変更がそのパッケージに依存するすべてのモジュールに影響を与えるためです。変更されたパッケージ。
パッケージは共有するユニットであるため、その内容はよく知られている必要があり、パッケージが将来のバージョンで進化するにつれて、含まれる API は互換性のある方法でのみ変更される可能性があります。これは、パッケージが API スーパーセットまたはサブセットをサポートしてはいけないことを意味します。たとえば、javax.transaction は、内容が不安定なパッケージとして参照してください。パッケージのユーザーは、そのパッケージでどのようなタイプが利用可能であるかを知ることができなければなりません。これは、パッケージが単一のエンティティ (たとえば、jar
) によって配信される必要があることも意味します。
ファイル) であり、パッケージのユーザーはパッケージ全体が存在することを知っている必要があるため、複数のエンティティに分割されません。
さらに、パッケージは将来のバージョンと互換性のある方法で進化する必要があります。したがって、パッケージはバージョン管理する必要があり、そのバージョン番号はセマンティック バージョン管理のルールに従って進化する必要があります。セマンティック バージョニングに関する OSGi ホワイトペーパーもあります。
しかし、パッケージのメジャーバージョン変更に対するセマンティックバージョニングの推奨事項には問題があります。パッケージの進化は機能の付加でなければなりません。セマンティック バージョニングでは、これによりマイナー バージョンが増加します。関数を削除すると、メジャー
を増やすのではなく、パッケージに互換性のない変更が加えられます。
元のパッケージとの互換性を維持したまま、新しいパッケージ名に移動する必要があります。これがなぜ重要であり必要なのかを理解するには、Go のセマンティック インポート バージョニングに関するこの論文と、Clojure/conj 2016 での Rich Hickey による優れた基調講演を参照してください。どちらも、メジャー パッケージを変更するのではなく、新しいパッケージ名に移行する必要があることを示しています。パッケージに互換性のない変更を加えるときのバージョン。
パッケージ内の型は、他のパッケージ内の型を参照できます。たとえば、メソッドのパラメータの型と戻り値の型、フィールドの型などです。このパッケージ間の結合により、パッケージにいわゆるuse制約が作成されます。これは、API コンシューマーが API プロバイダーと両方が参照される型を理解できるようにするために、同じ参照されるパッケージを使用する必要があることを意味します。
一般に、パッケージの使用制約を最小限に抑えるために、このパッケージの結合を最小限に抑えたいと考えています。これにより、OSGi 環境での配線解決が簡素化され、依存関係のファンアウトが最小限に抑えられ、展開が簡素化されます。
API の場合、クラスよりもインターフェイスが優先されます。これはかなり一般的な API 設計手法であり、モジュラー Java にとっても重要です。インターフェイスを使用すると、実装の自由が可能になるだけでなく、複数の実装も可能になります。インターフェイスは、API コンシューマを API プロバイダから分離するために重要です。これにより、API インターフェースを含むパッケージを、インターフェースを実装する API プロバイダーとインターフェース上のメソッドを呼び出す API コンシューマーの両方が使用できるようになります。このように、API コンシューマーは API プロバイダーに直接依存しません。どちらも API パッケージにのみ依存します。
インターフェースの代わりに抽象クラスが設計上の選択肢として有効な場合もありますが、特にデフォルトのメソッドをインターフェースに追加できるため、通常はインターフェースが最初の選択肢となります。
最後に、API にはイベント タイプや例外タイプなど、多数の小規模な具体的なクラスが必要になることがよくあります。これは問題ありませんが、型は一般に不変である必要があり、API コンシューマによるサブクラス化を意図したものではありません。
API では静的動作を回避する必要があります。型には静的メンバーがあってはなりません。静的ファクトリーは避けるべきです。インスタンスの作成は API から切り離す必要があります。たとえば、API コンシューマは、依存関係の注入、または OSGi サービス レジストリや JPMS の java.util.ServiceLoader などのオブジェクト レジストリを通じて、API タイプのオブジェクト インスタンスを受け取る必要があります。
静的は簡単にモックできないため、静的を回避することもテスト可能な API を作成するための良い習慣です。
API 設計にはシングルトン オブジェクトが存在する場合があります。ただし、シングルトン オブジェクトへのアクセスは、静的な getInstance メソッドや静的フィールドなどの静的メソッドを介して行わないでください。シングルトン オブジェクトが必要な場合、そのオブジェクトは API によってシングルトンとして定義され、前述のように依存関係注入またはオブジェクト レジストリを通じて API コンシューマーに提供される必要があります。
API には、API プロバイダがロードする必要があるクラスの名前を API コンシューマが提供できる拡張メカニズムが備わっていることがよくあります。 API プロバイダーは、Class.forName を使用して (おそらくスレッド コンテキスト クラス ローダーを使用して) クラスをロードする必要があります。この種のメカニズムは、API プロバイダー (またはスレッド コンテキスト クラス ローダー) から API コンシューマーまでのクラスの可視性を前提としています。 API 設計では、クラス ローダーの想定を避ける必要があります。モジュール性の主要なポイントの 1 つは型のカプセル化です。 1 つのモジュール (API プロバイダーなど) は、別のモジュール (API コンシューマーなど) の実装の詳細を表示/アクセスできてはなりません。
API 設計では、API コンシューマーと API プロバイダーの間でクラス名を渡すことを避け、クラス ローダーの階層と型の可視性/アクセシビリティに関する仮定を避ける必要があります。拡張性モデルを提供するには、API 設計で API コンシューマーがクラス オブジェクト、またはさらに良いのはインスタンス オブジェクトを API プロバイダーに渡すようにする必要があります。これは、API のメソッドを通じて、または OSGi サービス レジストリなどのオブジェクト レジストリを通じて行うことができます。ホワイトボードのパターンを参照してください。
java.util.ServiceLoader クラスは、JPMS モジュールで使用されていない場合、すべてのプロバイダがスレッド コンテキスト クラス ローダーまたは提供されたクラス ローダーから認識できると想定しているという点で、クラス ローダーの前提条件の影響を受けます。 JPMS では、モジュールが
を提供または使用することを宣言するモジュール宣言を許可していますが、この仮定は一般にモジュール環境では当てはまりません。
ServiceLoader マネージド サービス。
多くの API 設計は、オブジェクトがインスタンス化されて API に追加される構築フェーズのみを想定しており、動的システムで発生する可能性のある破棄フェーズは無視されます。 API 設計では、オブジェクトが来たり去ったりする可能性があることを考慮する必要があります。たとえば、ほとんどのリスナー API では、リスナーの追加と削除が可能です。しかし、多くの API 設計は、オブジェクトが追加されるだけで、削除されないことを前提としています。たとえば、多くの依存関係注入システムには、注入されたオブジェクトを取り消す手段がありません。
OSGi 環境ではモジュールを追加したり削除したりできるため、そのようなダイナミクスに対応できる API 設計が重要です。 OSGi 宣言サービス仕様
注入されたオブジェクトの取り消しを含むこれらのダイナミクスをサポートする OSGi の依存関係注入モデルを定義します。
冒頭で述べたように、API パッケージのクライアントには API コンシューマーと API プロバイダーという 2 つの役割があります。 API コンシューマーは API を使用し、API プロバイダーは API を実装します。 API のインターフェイス (および抽象クラス) 型については、API プロバイダーのみが実装する型と API コンシューマーが実装できる型を API 設計で明確に文書化することが重要です。たとえば、リスナー インターフェイスは通常、API コンシューマーによって実装されます
そしてインスタンスは API プロバイダーに渡されます。
API プロバイダーは、API コンシューマーと API プロバイダーの両方によって実装された型の変更に敏感です。プロバイダーは、API プロバイダー タイプの新しい変更を実装する必要があり、API コンシューマー タイプの新しい変更を理解し、おそらくそれを呼び出す必要があります。 API コンシューマーは、通常、新しい関数を呼び出すために変更する必要がない限り、API プロバイダー タイプの (互換性のある) 変更を無視できます。ただし、API コンシューマは API コンシューマ タイプの変更に敏感であり、新しい関数を実装するには変更が必要になる可能性があります。たとえば、javax.servlet パッケージでは、ServletContext タイプはサーブレット コンテナなどの API プロバイダによって実装されます。新しいメソッドを ServletContext に追加するには、新しいメソッドを実装するためにすべての API プロバイダーを更新する必要がありますが、API コンシューマーは、新しいメソッドを呼び出したい場合を除き、変更する必要はありません。ただし、サーブレット タイプは API コンシューマによって実装され、サーブレットに新しいメソッドを追加するには、新しいメソッドを実装するようにすべての API コンシューマを変更する必要があり、新しいメソッドを利用するためにすべての API プロバイダを変更する必要もあります。したがって、ServletContext タイプには API プロバイダーの役割があり、Servlet タイプには API コンシューマーの役割があります。
一般に API コンシューマーは多く、API プロバイダーは少ないため、API の進化では、API コンシューマー タイプの変更を検討する際には細心の注意を払う必要がありますが、API プロバイダー タイプの変更についてはより緩和されます。これは、更新された API をサポートするには少数の API プロバイダーを変更する必要がありますが、API が更新されたときに多くの既存の API コンシューマーを変更する必要はないためです。 API コンシューマは、新しい API を利用したい場合にのみ変更する必要があります。
OSGi Alliance は、API パッケージ内の型の役割をマークするために、ドキュメント アノテーション、ProviderType、および ConsumerType を定義します。これらのアノテーションは、API で使用するために osgi.annotation jar で利用できます。
次に API を設計するときは、次の API 設計プラクティスを考慮してください。これにより、API はモジュール式 Java 環境と非モジュール式 Java 環境の両方で使用できるようになります。
以上がJava の API 設計プラクティスの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。