Maison  >  Article  >  Java  >  Pratiques de conception d'API pour Java

Pratiques de conception d'API pour Java

WBOY
WBOYoriginal
2024-08-30 06:02:02329parcourir

API design practices for Java

Par : BJ Hargrave

Comprenez certaines des pratiques de conception d'API qui doivent être appliquées lors de la conception de l'API Java. Ces pratiques sont utiles, en général, et garantissent que l'API peut être utilisée correctement dans un environnement modulaire, tel que OSGi et Java Platform Module System (JPMS). Certaines pratiques sont prescriptives et d’autres sont proscriptives. Et bien sûr, d’autres bonnes pratiques de conception d’API s’appliquent également.

L'environnement OSGi fournit un environnement d'exécution modulaire utilisant le concept de chargeur de classe Java pour appliquer l'encapsulation de type visibilité. Chaque module aura son propre chargeur de classes qui sera câblé aux chargeurs de classes des autres modules pour partager les packages exportés et consommer les packages importés.

JPMS, introduit dans Java 9, fournit une plate-forme modulaire utilisant le concept de contrôle d'accès de la spécification du langage Java pour appliquer l'encapsulation de type accessibilité. Chaque module définit quels packages sont exportés et donc accessibles par d'autres modules. Par défaut, les modules d'une couche JMPS résident tous dans le même chargeur de classe.

Un package peut contenir une API. Il existe deux rôles de clients pour ces packages API : les consommateurs d'API et les fournisseurs d'API. Les consommateurs d'API utilisent l'API implémentée par un fournisseur d'API.

Dans les pratiques de conception suivantes, nous discutons des parties publiques d'un package. Les membres et les types d'un package, qui ne sont ni publics ni protégés (c'est-à-dire privés ou accessibles par défaut), ne sont pas accessibles en dehors du package et sont donc des détails d'implémentation du package.

Les packages Java doivent être une unité cohérente et stable

Un package Java doit être conçu pour garantir qu'il s'agit d'une unité cohésive et stable. En Java modulaire, le package est l'entité partagée entre les modules. Un module peut exporter un package afin que d'autres modules puissent utiliser le package. Étant donné que le package est l'unité de partage entre les modules, un package doit être cohérent dans la mesure où tous les types du package doivent être liés à l'objectif spécifique du package. Les packages grab bag comme java.util sont déconseillés car les types d'un tel package n'ont souvent aucun rapport les uns avec les autres. De tels packages non cohérents peuvent entraîner de nombreuses dépendances, car les parties non liées du package font référence à d'autres packages non liés et les modifications apportées à un aspect du package affectent tous les modules qui dépendent du package, même si un module peut ne pas utiliser réellement la partie du package. package qui a été modifié.

Étant donné que le package est partagé par l'unité, son contenu doit être bien connu et l'API contenue est uniquement susceptible d'être modifiée de manière compatible à mesure que le package évolue dans les versions futures. Cela signifie qu'un package ne doit pas prendre en charge les surensembles ou sous-ensembles d'API ; par exemple, voyez javax.transaction comme un package dont le contenu est instable. L'utilisateur d'un package doit être en mesure de savoir quels types sont disponibles dans le package. Cela signifie également que les colis doivent être livrés par une seule entité (par exemple, un pot
fichier) et non réparti sur plusieurs entités puisque l'utilisateur du package doit savoir que l'intégralité du package est présente.

De plus, le package doit évoluer de manière compatible au fil des futures versions. Un package doit donc être versionné et son numéro de version doit évoluer selon les règles du versioning sémantique. Il existe également un livre blanc OSGi sur la gestion des versions sémantiques.

Cependant, les recommandations de version sémantique pour les changements de version majeurs des packages sont problématiques. L'évolution du package doit être une accumulation de fonctions. Dans le versionnage sémantique, cela augmente la version mineure. Lorsque vous supprimez une fonction, cela apporte une modification incompatible au package, au lieu d'augmenter le majeur
version, vous devez passer à un nouveau nom de package en laissant le package d'origine toujours compatible. Pour comprendre pourquoi cela est important et nécessaire, consultez cet article sur la gestion des versions d'importation sémantique pour Go et cette excellente présentation principale de Rich Hickey à Clojure/conj 2016. Ces deux éléments plaident en faveur du passage à un nouveau nom de package au lieu de changer le nom principal. version lorsque vous apportez des modifications incompatibles à un package.

Minimiser le couplage des packages

Les types d'un package peuvent faire référence aux types d'autres packages. Par exemple, les types de paramètres et le type de retour d'une méthode et le type d'un champ. Ce couplage inter-package crée ce que l'on appelle des contraintes d'usages sur le package. Cela signifie qu'un consommateur d'API doit utiliser les mêmes packages référencés que le fournisseur d'API afin qu'ils comprennent tous les deux les types référencés.

En général, nous souhaitons minimiser ce couplage de package pour minimiser les contraintes d'utilisation sur un package. Cela simplifie la résolution du câblage dans l'environnement OSGi et minimise la diffusion des dépendances, simplifiant ainsi le déploiement.

Interfaces préférées aux classes

Pour une API, les interfaces sont préférées aux classes. Il s'agit d'une pratique de conception d'API assez courante qui est également importante pour Java modulaire. L'utilisation d'interfaces permet une liberté d'implémentation ainsi que des implémentations multiples. Les interfaces sont importantes pour dissocier le consommateur d'API du fournisseur d'API. Il permet à un package contenant les interfaces API d'être utilisé à la fois par le fournisseur d'API qui implémente les interfaces et par le consommateur d'API qui appelle des méthodes sur les interfaces. De cette manière, les consommateurs d’API n’ont aucune dépendance directe à l’égard d’un fournisseur d’API. Ils dépendent tous deux uniquement du package API.

Les classes abstraites sont parfois un choix de conception valable à la place des interfaces, mais généralement les interfaces sont le premier choix, d'autant plus que des méthodes par défaut peuvent être ajoutées à une interface.

Enfin, une API aura souvent besoin d'un certain nombre de petites classes concrètes telles que des types d'événements et des types d'exceptions. C'est bien, mais les types doivent généralement être immuables et ne sont pas destinés au sous-classement par les consommateurs d'API.

Évitez la statique

Les statiques doivent être évitées dans une API. Les types ne doivent pas avoir de membres statiques. Les usines statiques doivent être évitées. La création d'instance doit être découplée de l'API. Par exemple, les consommateurs d'API doivent recevoir des instances d'objet de types d'API via une injection de dépendances ou un registre d'objets tel que le registre de services OSGi ou java.util.ServiceLoader dans JPMS.

Éviter les statiques est également une bonne pratique pour créer une API testable, car les statiques ne peuvent pas être facilement moquées.

Célibataires

Parfois, il y a des objets singleton dans une conception d'API. Cependant, l'accès à l'objet singleton ne doit pas se faire via des statistiques telles qu'une méthode getInstance statique ou un champ statique. Lorsqu'un objet singleton est nécessaire, l'objet doit être défini par l'API comme un singleton et fourni aux consommateurs de l'API via une injection de dépendances ou un registre d'objets comme mentionné ci-dessus.

Évitez les hypothèses du chargeur de classe

Les API disposent souvent de mécanismes d'extensibilité dans lesquels le consommateur de l'API peut fournir le nom d'une classe que le fournisseur d'API doit charger. Le fournisseur d'API doit ensuite utiliser Class.forName (éventuellement en utilisant le chargeur de classe de contexte de thread) pour charger la classe. Ce type de mécanisme suppose une visibilité de classe depuis le fournisseur d'API (ou le chargeur de classe de contexte de thread) jusqu'au consommateur d'API. Les conceptions d’API doivent éviter les hypothèses du chargeur de classe. L'un des principaux points de la modularité est l'encapsulation de type. Un module (par exemple, le fournisseur d'API) ne doit pas avoir de visibilité/accessibilité aux détails d'implémentation d'un autre module (par exemple, le consommateur d'API).

Les conceptions d'API doivent éviter de transmettre les noms de classe entre le consommateur d'API et le fournisseur d'API et doivent éviter les hypothèses concernant la hiérarchie du chargeur de classe et la visibilité/accessibilité du type. Pour fournir un modèle d'extensibilité, une conception d'API doit permettre au consommateur d'API de transmettre des objets de classe, ou mieux encore, des objets d'instance au fournisseur d'API. Cela peut être effectué via une méthode dans l'API ou via un registre d'objets tel que le registre de services OSGi. Voir le modèle du tableau blanc.

La classe java.util.ServiceLoader, lorsqu'elle n'est pas utilisée dans les modules JPMS, souffre également des hypothèses du chargeur de classe dans la mesure où elle suppose que tous les fournisseurs sont visibles depuis le chargeur de classe de contexte de thread ou le chargeur de classe fourni. Cette hypothèse n'est généralement pas vraie dans un environnement modulaire bien que JPMS permette à la déclaration de module de déclarer que les modules fournissent ou utilisent un
Service géré ServiceLoader.

Ne présumez pas la permanence

De nombreuses conceptions d'API supposent uniquement une phase de construction au cours de laquelle les objets sont instanciés et ajoutés à l'API, mais ignorent la phase de destruction qui peut se produire dans un système dynamique. Les conceptions d’API doivent considérer que les objets peuvent aller et venir. Par exemple, la plupart des API d'écoute permettent d'ajouter et de supprimer des écouteurs. Mais de nombreuses conceptions d’API supposent uniquement que les objets sont ajoutés et jamais supprimés. Par exemple, de nombreux systèmes d'injection de dépendances n'ont aucun moyen de retirer un objet injecté.

Dans un environnement OSGi, des modules peuvent être ajoutés et supprimés, il est donc important d'avoir une conception d'API capable de s'adapter à une telle dynamique. La spécification des services déclaratifs OSGi
définit un modèle d'injection de dépendances pour OSGi qui prend en charge ces dynamiques, y compris le retrait des objets injectés.

Documenter clairement les rôles de type pour les consommateurs d'API et les fournisseurs d'API

Comme mentionné dans l'introduction, il existe deux rôles pour les clients d'un package API : les consommateurs d'API et les fournisseurs d'API. Les consommateurs d'API utilisent l'API et les fournisseurs d'API implémentent l'API. Pour les types d'interface (et de classe abstraite) dans une API, il est important que la conception de l'API documente clairement lesquels de ces types doivent être implémentés uniquement par les fournisseurs d'API par rapport à ceux qui peuvent être implémentés par les consommateurs d'API. Par exemple, les interfaces d'écoute sont généralement implémentées par les consommateurs d'API
et les instances transmises aux fournisseurs d'API.

Les fournisseurs d'API sont sensibles aux changements de types mis en œuvre à la fois par les consommateurs d'API et les fournisseurs d'API. Le fournisseur doit mettre en œuvre toute nouvelle modification dans les types de fournisseur d'API et doit comprendre et probablement invoquer toute nouvelle modification dans les types de consommateur d'API. Un consommateur d'API peut généralement ignorer les modifications (compatibles) du type de fournisseur d'API, à moins qu'il ne souhaite modifier pour appeler la nouvelle fonction. Mais un consommateur d'API est sensible aux changements dans les types de consommateurs d'API et aura probablement besoin de modifications pour implémenter la nouvelle fonction. Par exemple, dans le package javax.servlet, le type ServletContext est implémenté par des fournisseurs d'API tels qu'un conteneur de servlet. L'ajout d'une nouvelle méthode à ServletContext nécessitera que tous les fournisseurs d'API soient mis à jour pour implémenter la nouvelle méthode, mais les consommateurs d'API n'auront pas à changer à moins qu'ils ne souhaitent appeler la nouvelle méthode. Cependant, le type Servlet est implémenté par les consommateurs d'API et l'ajout d'une nouvelle méthode à Servlet nécessitera que tous les consommateurs d'API soient modifiés pour implémenter la nouvelle méthode et nécessitera également que tous les fournisseurs d'API soient modifiés pour utiliser la nouvelle méthode. Ainsi, le type ServletContext a un rôle de fournisseur d'API et le type Servlet a un rôle de consommateur d'API.

Comme il existe généralement de nombreux consommateurs d'API et peu de fournisseurs d'API, l'évolution des API doit être très prudente lors de l'examen des modifications apportées aux types de consommateurs d'API, tout en étant plus souple quant aux changements de types de fournisseurs d'API. En effet, vous devrez modifier les quelques fournisseurs d'API pour prendre en charge une API mise à jour, mais vous ne souhaitez pas exiger que les nombreux consommateurs d'API existants changent lorsqu'une API est mise à jour. Les consommateurs d'API ne devraient avoir besoin de changer que lorsqu'ils souhaitent profiter de la nouvelle API.

L'OSGi Alliance définit les annotations documentaires, ProviderType et ConsumerType pour marquer les rôles des types dans un package API. Ces annotations sont disponibles dans le pot osgi.annotation pour une utilisation dans votre API.

Conclusion

Lors de la prochaine conception d'une API, veuillez tenir compte de ces pratiques de conception d'API. Votre API sera alors utilisable aussi bien dans des environnements Java modulaires que Java non modulaires.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn