Conception d’une bibliothèque Java conviviale

Conception d'une bibliothèque Java conviviale

1. Vue d'ensemble

Java est l'un des piliers du monde des logiciels libres. Presque tous les projets Java utilisent d’autres projets Open Source, car personne ne veut réinventer la roue. Cependant, il arrive souvent que nous ayons besoin d’une bibliothèque pour ses fonctionnalités, mais nous ne savons pas comment les utiliser. Nous rencontrons des choses comme:

  • De quoi s'agit-il avec toutes ces classes «* Service»?

  • Comment instancier cela, cela prend trop de dépendances. Qu'est-ce qu'un «latch»?

  • Oh, je l'ai mis ensemble, mais maintenant il commence à lancerIllegalStateException. Qu'est-ce que je fais mal?

Le problème est que tous les concepteurs de bibliothèques ne pensent pas à leurs utilisateurs. La plupart ne pensent qu'à la fonctionnalité et aux fonctionnalités, mais peu considèrent comment l'API va être utilisée dans la pratique, et à quoi le code des utilisateurs ressemblera et sera testé.

Cet article contient quelques conseils sur la façon de sauver nos utilisateurs de certaines de ces difficultés - et non, ce n'est pas en écrivant de la documentation. Bien sûr, un livre entier pourrait être écrit sur ce sujet (et quelques-uns l'ont été); Ce sont quelques-uns des points clés que j'ai appris en travaillant moi-même sur plusieurs bibliothèques.

Je vais illustrer les idées ici en utilisant deux bibliothèques:charles etjcabi-github

2. Limites

Cela devrait être évident, mais ce n’est souvent pas le cas. Avant de commencer à écrire une ligne de code, nous devons avoir une réponse claire à certaines questions: quelles entrées sont nécessaires? Quelle est la première classe que mon utilisateur verra? Avons-nous besoin d'implémentations de la part de l'utilisateur? quelle est la sortie? Une fois que ces questions ont été clairement résolues, tout devient plus facile puisque la bibliothèque a déjà une doublure, une forme.

2.1. Contribution

C'est peut-être le sujet le plus important. Nous devons nous assurer que ce que l’utilisateur doit fournir à la bibliothèque est clair pour qu’elle puisse faire son travail. Dans certains cas, il s'agit d'une question très triviale: il peut s'agir simplement d'une chaîne représentant le jeton d'authentification d'une API, mais il peut également s'agir d'une implémentation d'une interface ou d'une classe abstraite.

Une très bonne pratique consiste à prendre toutes les dépendances à travers les constructeurs et à les garder courtes, avec quelques paramètres. Si nous avons besoin d'un constructeur avec plus de trois ou quatre paramètres, le code doit clairement être refactoré. Et si des méthodes sont utilisées pour injecter des dépendances obligatoires, les utilisateurs se retrouveront probablement avec la troisième frustration décrite dans la vue d'ensemble.

En outre, nous devrions toujours proposer plus d'un constructeur, donner aux utilisateurs des alternatives. Laissez-les travailler à la fois avecString etInteger ou ne les limitez pas à unFileInputStream, travaillez avec unInputStream, afin qu'ils puissent soumettre peut-êtreByteArrayInputStream quand tests unitaires, etc.

Par exemple, voici quelques façons d'instancier un point d'entrée d'API Github à l'aide de jcabi-github:

Github noauth = new RtGithub();
Github basicauth = new RtGithub("username", "password");
Github oauth = new RtGithub("token");

Simple, pas de hâte, pas d'objets de configuration ombragés à initialiser. Et il est logique d’avoir ces trois constructeurs, car vous pouvez utiliser le site Web de Github en étant déconnecté, connecté ou une application peut s’authentifier en votre nom. Naturellement, certaines fonctionnalités ne fonctionneront pas si vous n'êtes pas authentifié, mais vous le savez dès le départ.

Comme deuxième exemple, voici comment nous pourrions travailler avec charles, une bibliothèque d’analyse Web:

WebDriver driver = new FirefoxDriver();
Repository repo = new InMemoryRepository();
String indexPage = "http://www.amihaiemil.com/index.html";
WebCrawl graph = new GraphCrawl(
  indexPage, driver, new IgnoredPatterns(), repo
);
graph.crawl();

C’est aussi assez explicite, je crois. Cependant, en écrivant ceci, je me rends compte dans la version actuelle qu'il y a une erreur: tous les constructeurs demandent à l'utilisateur de fournir une instance deIgnoredPatterns. Par défaut, aucun modèle ne devrait être ignoré, mais l'utilisateur ne devrait pas avoir à le spécifier. J'ai décidé de le laisser comme ça ici, donc vous voyez un contre exemple. Je suppose que vous essayez d'instancier un WebCrawl et que vous vous demandez "Qu'est-ce que c'est avec cesIgnoredPatterns ?!"

La variable index Page est l'URL à partir de laquelle l'analyse doit commencer, le pilote est le navigateur à utiliser (vous ne pouvez utiliser aucun paramètre par défaut car nous ne savons pas quel navigateur est installé sur la machine en cours d'exécution). La variable repo sera expliquée ci-dessous dans la section suivante.

Ainsi, comme vous le voyez dans les exemples, essayez de rester simple, intuitif et explicite. Encapsulez la logique et les dépendances de manière à ce que l'utilisateur ne se gratte pas la tête lorsqu'il regarde vos constructeurs.

Si vous avez encore des doutes, essayez de faire des requêtes HTTP à AWS en utilisantaws-sdk-java: vous devrez gérer un soi-disant AmazonHttpClient, qui utilise un ClientConfiguration quelque part, puis doit prendre un ExecutionContext quelque part entre les deux. Enfin, vous pouvez éventuellement exécuter votre demande et obtenir une réponse sans avoir la moindre idée de ce qu'est un contexte d'exécution, par exemple.

2.2. Sortie

Ceci concerne principalement les bibliothèques qui communiquent avec le monde extérieur. Ici, nous devrions répondre à la question "comment la sortie sera-t-elle gérée?". Encore une fois, une question plutôt amusante, mais il est facile de se tromper.

Regardez à nouveau le code ci-dessus. Pourquoi devons-nous fournir une implémentation de référentiel? Pourquoi la méthode WebCrawl.crawl () ne renvoie-t-elle pas simplement une liste d'éléments WebPage? Ce n’est clairement pas le travail de la bibliothèque de gérer les pages explorées. Comment devrait-il même savoir ce que nous aimerions faire avec eux? Quelque chose comme ça:

WebCrawl graph = new GraphCrawl(...);
List pages = graph.crawl();

Rien ne pourrait être pire. Une exception OutOfMemory peut se produire de nulle part si le site exploré contient, par exemple, 1000 pages - la bibliothèque les charge toutes en mémoire. Il y a deux solutions à cela:

  • Continuez à retourner les pages, mais implémentez un mécanisme de pagination dans lequel l'utilisateur devra fournir les numéros de début et de fin. Or

  • Demander à l'utilisateur d'implémenter une interface avec une méthode appelée export (List ), que l'algorithme appelle chaque fois qu'un nombre maximal de pages est atteint

La deuxième option est de loin la meilleure; cela simplifie les choses des deux côtés et est plus testable. Pensez à la logique qui devrait être mise en œuvre du côté de l'utilisateur si nous options pour le premier. Comme cela, un référentiel pour les pages est spécifié (pour les envoyer dans une base de données ou pour les écrire sur le disque, par exemple) et rien d'autre ne doit être fait après l'appel de la méthode crawl ().

Soit dit en passant, le code de la section Input ci-dessus contient tout ce que nous devons écrire pour récupérer le contenu du site Web (toujours en mémoire, comme le dit l'implémentation du repo, mais c'est notre choix - nous l'avons fourni nous prenons le risque).

Pour résumer cette section: nous ne devons jamais séparer complètement notre travail du travail du client. Nous devrions toujours penser à ce qui se passe avec le résultat que nous créons. Tout comme un chauffeur de camion devrait aider à déballer les marchandises plutôt que de simplement les jeter à leur arrivée à destination.

3. Des interfaces

Toujours utiliser des interfaces. L'utilisateur ne doit interagir avec notre code que par le biais de contrats stricts.

Par exemple, dans la bibliothèquejcabi-github, la classe RtGithub est la seule que l'utilisateur voit réellement:

Repo repo = new RtGithub("oauth_token").repos().get(
  new Coordinates.Simple("eugenp/tutorials"));
Issue issue = repo.issues()
  .create("Example issue", "Created with jcabi-github");

L'extrait de code ci-dessus crée un ticket dans leseugenp/tutorials repo. Des instances de Repo et Issue sont utilisées, mais les types réels ne sont jamais révélés. Nous ne pouvons pas faire quelque chose comme ça:

Repo repo = new RtRepo(...)

Ce qui précède n’est pas possible pour une raison logique: nous ne pouvons pas créer directement un problème dans un dépôt Github, pouvons-nous? Tout d’abord, nous devons nous connecter, puis rechercher dans le dépôt et nous pourrons alors créer un problème. Bien sûr, le scénario ci-dessus pourrait être autorisé, mais le code de l'utilisateur deviendrait alors pollué avec beaucoup de code standard: ceRtRepo devrait probablement prendre une sorte d'objet d'autorisation via son constructeur, autoriser le client et obtenir au bon repo, etc.

Les interfaces offrent également une facilité d'extensibilité et une compatibilité ascendante. En tant que développeurs, nous sommes tenus, d’une part, de respecter les contrats déjà conclus et, d’autre part, l’utilisateur peut étendre les interfaces que nous proposons - il peut les décorer ou écrire des implémentations alternatives.

En d'autres termes, abstraite et encapsule autant que possible. En utilisant des interfaces, nous pouvons le faire de manière élégante et non restrictive. Nous appliquons des règles architecturales tout en laissant au programmeur la liberté d'améliorer ou de modifier le comportement exposé.

Pour terminer cette section, il suffit de garder à l’esprit: notre bibliothèque, nos règles. Nous devons savoir exactement à quoi va ressembler le code du client et comment il va le tester unitaire. Si nous ne le savons pas, personne ne le fera et notre bibliothèque contribuera simplement à la création d'un code difficile à comprendre et à maintenir.

4. Les tiers

Gardez à l'esprit qu'une bonne bibliothèque est une bibliothèque légère. Votre code peut résoudre un problème et être fonctionnel, mais si le fichier jar ajoute 10 Mo à ma version, il est clair que vous avez perdu les plans de votre projet il y a longtemps. Si vous avez besoin de nombreuses dépendances, vous essayez probablement de couvrir trop de fonctionnalités et devriez diviser le projet en plusieurs projets plus petits.

Soyez aussi transparent que possible, ne liez pas, dans la mesure du possible, les mises en œuvre réelles. Le meilleur exemple qui me vienne à l’esprit est: utilisez SLF4J, qui n’est qu’une API pour la journalisation - n’utilisez pas log4j directement, l’utilisateur souhaitera peut-être utiliser d’autres enregistreurs.

Les bibliothèques de documents qui traversent votre projet de manière transitoire et vous garantissent de ne pas inclure de dépendances dangereuses telles quexalan ouxml-apis (pourquoi elles sont dangereuses, ce n’est pas à cet article d’expliquer).

En résumé, gardez votre structure légère, transparente et sachez toujours avec quoi vous travaillez. Cela pourrait permettre à vos utilisateurs d'économiser plus de temps que vous ne pouvez l'imaginer.

5. Conclusion

L'article présente quelques idées simples qui peuvent aider un projet à rester en ligne en ce qui concerne la convivialité. Une bibliothèque, en tant que composant qui devrait trouver sa place dans un contexte plus large, devrait avoir des fonctionnalités puissantes tout en offrant une interface fluide et bien conçue.

C'est une étape facile sur la ligne et fait des dégâts de la conception. Les contributeurs sauront toujours comment l'utiliser, mais ce n'est peut-être pas le cas de tout nouveau qui l'examinera d'abord. La productivité est le plus important de tous et, suivant ce principe, les utilisateurs devraient pouvoir commencer à utiliser une bibliothèque en quelques minutes.