Trop de paramètres dans les méthodes Java, partie 6: retours de méthode

Dans la série actuelle d'articles que j'écris sur la réduction du nombre de paramètres requis pour appeler des méthodes et des constructeurs Java, je me suis jusqu'à présent concentré sur les approches qui affectent directement les paramètres eux-mêmes (types personnalisés, objets paramètres, modèle de générateur, surcharge de méthode, et nom de méthode). Compte tenu de cela, il peut me sembler surprenant de consacrer un article de cette série à la manière dont les méthodes Java fournissent des valeurs de retour. Cependant, les valeurs de retour des méthodes peuvent avoir un impact sur les paramètres que les méthodes acceptent lorsque les développeurs choisissent de fournir des valeurs de «retour» en définissant ou en modifiant les paramètres fournis plutôt que ou en plus de mécanismes de retour de méthode plus traditionnels.

Les «méthodes traditionnelles» par lesquelles une méthode non-constructeur renvoie une valeur peuvent toutes deux être spécifiées dans la signature de la méthode. L'approche la plus couramment reconnue pour renvoyer une valeur à partir d'une méthode Java consiste à utiliser son type de retour déclaré. Cela fonctionne souvent bien, mais l'une des frustrations les plus fréquentes est de ne pouvoir renvoyer qu'une seule valeur à partir d'une méthode Java.

Le mécanisme de gestion des exceptions de Java est également une autre approche pour conserver un «résultat» d'une méthode aux appelants. Les exceptions vérifiées, en particulier, sont annoncées à l'appelant via la clause throws. En fait, Jim Waldo, dans son livre Java: The Good Parts, déclare qu'il est plus facile de comprendre les exceptions Java quand on pense aux exceptions Java comme un autre type de retour de méthode limité au type Throwable.

Bien que le type de retour de la méthode et les exceptions levées soient conçus comme les principales approches des méthodes pour renvoyer des informations aux appelants, il est parfois tentant de renvoyer des données ou un état via les paramètres passés dans la méthode. Lorsqu'une méthode doit renvoyer plus d'une information, les retours à valeur unique des méthodes Java peuvent sembler limitatifs. Bien que les exceptions fournissent un autre moyen de communiquer avec l'appelant, il semble presque universellement convenu que les exceptions ne devraient être utilisées que pour signaler des situations exceptionnelles et non pour signaler des données «normales» ou utilisées dans le flux de contrôle. Étant donné qu'un seul objet ou primitive peut être retourné à partir d'une méthode et que les exceptions ne permettent que le retour d'unThrowable et ne doit être utilisé que pour signaler des situations exceptionnelles, il devient de plus en plus attrayant pour le développeur Java de détourner des paramètres comme une autre voie pour renvoyer des données à l'appelant.

La technique qu'un développeur peut utiliser pour appliquer des paramètres de méthode en tant que supports pour les données de retour consiste à accepter des paramètres qui sont mutables et à muter l'état des objets transmis. Ces objets mutables peuvent voir leur contenu modifié par la méthode, puis l'appelant peut accéder à l'objet qu'il a fourni pour déterminer ses nouveaux paramètres d'état qui ont été appliqués par la méthode appelée. Bien que cela puisse être fait avec n'importe quel objet mutable, les collections semblent particulièrement attrayantes pour le développeur qui tente de renvoyer des valeurs à l'appelant via des paramètres.

Il y a quelques inconvénients à renvoyer l'état à l'appelé via les paramètres fournis. Cette approche viole souvent le principe du moindre étonnement car la plupart des développeurs Java s'attendent probablement à ce que les paramètres soient INcoming plutôt que OUTgoing (et Java ne fournit aucun support de code pour spécifier la différence). Bob Martin l'exprime ainsi dans son livre Clean Code: «En général, les arguments de sortie doivent être évités». Un autre inconvénient de l'utilisation d'arguments comme moyen pour une méthode de fournir un état ou une sortie à l'appelant est que cela ajoute à l'encombrement des arguments passés à une méthode. Dans cet esprit, le reste de cet article se concentre sur les alternatives au renvoi de plusieurs valeurs via des paramètres transmis.

Bien que les méthodes Java ne puissent renvoyer qu'un seul objet ou une primitive, ce n'est vraiment pas une grande limitation quand on considère qu'un objet peut être à peu près tout ce que nous voulons qu'il soit. Il existe plusieurs approches que j'ai vues mais que je ne recommande pas. L'une d'elles consiste à renvoyer un tableau ou une collection d'instances Object avec chaqueObjectétant une «chose» disparate et distincte et souvent sans rapport. Par exemple, la méthode peut renvoyer trois valeurs en tant que trois éléments d'un tableau ou d'une collection. Une variante de cette approche consiste à utiliser un tuple de paire ou un tuple de taille n pour renvoyer plusieurs valeurs associées. Une autre variante de cette approche consiste à renvoyer une carte Java qui mappe des clés arbitraires à leur valeur associée. Comme avec les autres solutions, cette approche impose au client une charge excessive de savoir quelles sont ces clés et d'accéder aux valeurs de la carte via ces clés.

La liste de codes suivante contient plusieurs de ces approches moins attrayantes pour renvoyer plusieurs valeurs sans détourner les paramètres de la méthode pour renvoyer plusieurs valeurs.

Renvoi de plusieurs valeurs via des structures de données génériques

 // =============================================================== // NOTE: These examples are intended solely to illustrate a point // and are NOT recommended for production code. // =============================================================== /** * Provide movie information. * * @return Movie information in form of an array where details are mapped to * elements with the following indexes in the array: * 0 : Movie Title * 1 : Year Released * 2 : Director * 3 : Rating */ public Object[] getMovieInformation() { final Object[] movieDetails = {"World War Z", 2013, "Marc Forster", "PG-13"}; return movieDetails; } /** * Provide movie information. * * @return Movie information in form of a List where details are provided * in this order: Movie Title, Year Released, Director, Rating. */ public List getMovieDetails() { return Arrays.asList("Ender's Game", 2013, "Gavin Hood", "PG-13"); } /** * Provide movie information. * * @return Movie information in Map form. Characteristics of the movie can * be acquired by looking in the map for these key elements: "Title", "Year", * "Director", and "Rating"./ */ public Map getMovieDetailsMap() { final HashMap map = new HashMap(); map.put("Title", "Despicable Me 2"); map.put("Year", 2013); map.put("Director", "Pierre Coffin and Chris Renaud"); map.put("Rating", "PG"); return map; } 

Les approches présentées ci-dessus répondent à l'intention de ne pas renvoyer les données à l'appelant via les paramètres des méthodes invoquées, mais il y a toujours une charge inutile placée sur l'appelant pour connaître les détails intimes de la structure de données renvoyée. C'est bien de réduire le nombre de paramètres à la méthode et de ne pas violer le principe de la moindre surprise, mais ce n'est pas si agréable d'exiger du client qu'il connaisse les subtilités d'une structure de données complexe.

Je préfère écrire des objets personnalisés pour mes retours lorsque j'ai besoin de renvoyer plus d'une valeur. C'est un peu plus de travail que d'utiliser une structure de tableau, de collection ou de tuple, mais la très petite quantité de travail supplémentaire (généralement quelques minutes avec les IDE Java modernes) porte ses fruits avec une lisibilité et une fluidité qui ne sont pas disponibles avec ces approches plus génériques. Plutôt que d'avoir à expliquer avec Javadoc ou d'exiger que les utilisateurs de mon code lisent attentivement mon code pour savoir quels paramètres sont fournis dans quel ordre dans le tableau ou la collection ou quelle valeur est laquelle dans le tuple, mes objets de retour personnalisés peuvent avoir des méthodes définies sur ceux qui disent exactement au client ce qu'ils fournissent.

Les extraits de code qui suivent illustrent une Movieclasse simple largement générée par NetBeans qui peut être utilisée comme type de retour avec le code qui pourrait renvoyer une instance de cette classe plutôt qu'une structure de données plus générique et moins lisible.

Movie.java

package dustin.examples; import java.util.Objects; /** * Simple Movie class to demonstrate how easy it is to provide multiple values * in a single Java method return and provide readability to the client. * * @author Dustin */ public class Movie { private final String movieTitle; private final int yearReleased; private final String movieDirectorName; private final String movieRating; public Movie(String movieTitle, int yearReleased, String movieDirectorName, String movieRating) { this.movieTitle = movieTitle; this.yearReleased = yearReleased; this.movieDirectorName = movieDirectorName; this.movieRating = movieRating; } public String getMovieTitle() { return movieTitle; } public int getYearReleased() { return yearReleased; } public String getMovieDirectorName() { return movieDirectorName; } public String getMovieRating() { return movieRating; } @Override public int hashCode() { int hash = 3; hash = 89 * hash + Objects.hashCode(this.movieTitle); hash = 89 * hash + this.yearReleased; hash = 89 * hash + Objects.hashCode(this.movieDirectorName); hash = 89 * hash + Objects.hashCode(this.movieRating); return hash; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final Movie other = (Movie) obj; if (!Objects.equals(this.movieTitle, other.movieTitle)) { return false; } if (this.yearReleased != other.yearReleased) { return false; } if (!Objects.equals(this.movieDirectorName, other.movieDirectorName)) { return false; } if (!Objects.equals(this.movieRating, other.movieRating)) { return false; } return true; } @Override public String toString() { return "Movie{" + "movieTitle=" + movieTitle + ", yearReleased=" + yearReleased + ", movieDirectorName=" + movieDirectorName + ", movieRating=" + movieRating + '}'; } } 

Renvoyer plusieurs détails dans un seul objet

 /** * Provide movie information. * * @return Movie information. */ public Movie getMovieInfo() { return new Movie("Oblivion", 2013, "Joseph Kosinski", "PG-13"); } 

La simple écriture du Moviela classe m'a pris environ 5 minutes. J'ai utilisé l'assistant de création de classe NetBeans pour sélectionner le nom de la classe et le package, puis j'ai tapé les quatre attributs de la classe. À partir de là, j'ai simplement utilisé le mécanisme "Insert Code" de NetBeans pour insérer des méthodes d'accesseur "get" avec les méthodes toString (), hashCode () et equals (Object) remplacées. Si je ne pensais pas avoir besoin de cela, je pourrais garder la classe plus simple, mais c'est vraiment facile à créer telle quelle. Maintenant, j'ai un type de retour beaucoup plus utilisable et cela se reflète dans le code qui utilise la classe. Il n'a pas besoin d'autant de commentaires Javadoc sur le type de retour car ce type parle de lui-même et annonce son contenu avec ses méthodes "get".Je pense que le petit effort supplémentaire pour créer ces classes simples pour renvoyer plusieurs valeurs porte ses fruits avec d'énormes dividendes par rapport à des alternatives telles que le retour de l'état via des paramètres de méthode ou l'utilisation de structures de données de retour plus génériques et plus difficiles à utiliser.

Il n'est pas trop surprenant qu'un type personnalisé contenant les multiples valeurs à renvoyer à un appelant soit une solution intéressante. Après tout, c'est conceptuellement très similaire aux concepts sur lesquels j'ai blogué précédemment liés à l'utilisation d'objets de types et de paramètres personnalisés pour transmettre plusieurs paramètres liés plutôt que de les transmettre tous individuellement. Java est un langage orienté objet et cela me surprend donc de ne pas voir les objets utilisés plus souvent dans le code Java pour organiser les paramètres ET retourner les valeurs dans un joli package.

Avantages et avantages

Les avantages de l'utilisation d'objets de paramètres personnalisés pour représenter et encapsuler plusieurs valeurs de retour sont évidents. Les paramètres de la méthode peuvent rester des paramètres «d'entrée» car toutes les informations de sortie (à l'exception des informations d'erreur communiquées via le mécanisme d'exception) peuvent être fournies dans l'objet personnalisé renvoyé par la méthode. Il s'agit d'une approche plus propre que l'utilisation de tableaux génériques, de collections, de cartes, de tuples ou d'autres structures de données génériques car toutes ces approches alternatives déplacent l'effort de développement sur tous les clients potentiels.

Coûts et inconvénients

Je vois très peu d'inconvénient à l'écriture de types personnalisés avec plusieurs valeurs à utiliser comme types de retour à partir des méthodes Java. Le coût le plus souvent invoqué est peut-être le prix d'écriture et de test de ces classes, mais ce coût est assez faible car ces classes ont tendance à être simples et parce que les IDE modernes font la plupart du travail pour nous. Étant donné que les IDE le font automatiquement, le code est généralement correct. Les classes sont si simples qu'elles sont facilement lisibles par les réviseurs de code et elles sont faciles à tester.