Demasiados parámetros en los métodos Java, Parte 6: Devolución del método

En la serie actual de publicaciones que estoy escribiendo sobre la reducción de la cantidad de parámetros necesarios para llamar a los métodos y constructores de Java, me he centrado hasta ahora en enfoques que afectan directamente a los parámetros en sí mismos (tipos personalizados, objetos de parámetros, patrón de constructor, sobrecarga de métodos y nombre del método). Dado esto, podría parecerme sorprendente dedicar una publicación de esta serie a cómo los métodos Java proporcionan valores de retorno. Sin embargo, los valores de retorno de los métodos pueden afectar los parámetros que los métodos aceptan cuando los desarrolladores eligen proporcionar valores de "retorno" estableciendo o cambiando los parámetros proporcionados en lugar de o además de los mecanismos de retorno de métodos más tradicionales.

Las "formas tradicionales" en las que un método no constructor devuelve un valor pueden especificarse en la firma del método. El enfoque más comúnmente reconocido para devolver un valor de un método Java es a través de su tipo de retorno declarado. Esto a menudo funciona bien, pero una de las frustraciones que ocurren con mayor frecuencia es que se le permita devolver solo un valor de un método Java.

El mecanismo de manejo de excepciones de Java es también otro enfoque para retener un "resultado" de un método para los llamadores. Las excepciones marcadas, en particular, se anuncian a la persona que llama a través de la cláusula throws. De hecho, Jim Waldo, en su libro Java: The Good Parts, afirma que es más fácil comprender las excepciones de Java cuando uno piensa en las excepciones de Java como otro tipo de retorno de método limitado a ser un tipo Throwable.

Aunque el tipo de retorno del método y las excepciones lanzadas están pensados ​​como enfoques principales para que los métodos devuelvan información a los llamadores, a veces es tentador devolver datos o estados a través de los parámetros pasados ​​al método. Cuando un método necesita devolver más de una pieza de información, los retornos de valor único de los métodos Java pueden parecer limitantes. Aunque las excepciones proporcionan otra forma de comunicarse con la persona que llama, parece casi universalmente aceptado que las excepciones solo deben usarse para informar situaciones excepcionales y no para informar datos "normales" o usarse en el flujo de control. Dado que solo se puede devolver un objeto o primitiva de un método y que las excepciones solo permiten la devolución de unThrowable y solo debe usarse para informar situaciones excepcionales, se vuelve cada vez más atractivo para el desarrollador de Java secuestrar parámetros como una ruta alternativa para devolver datos a la persona que llama.

La técnica que un desarrollador puede usar para aplicar parámetros de método como portadores de datos de retorno es aceptar parámetros que son mutables y mutar el estado de los objetos pasados. El método puede modificar el contenido de estos objetos mutables y luego la persona que llama puede acceder al objeto que proporcionó para determinar la nueva configuración de estado que ha aplicado el método llamado. Aunque esto se puede hacer con cualquier objeto mutable, las colecciones parecen particularmente atractivas para el desarrollador que intenta pasar valores al llamador a través de parámetros.

Hay algunas desventajas de devolver el estado a la llamada a través de los parámetros proporcionados. Este enfoque a menudo viola el principio del mínimo asombro, ya que la mayoría de los desarrolladores de Java probablemente esperan que los parámetros sean INcoming en lugar de OUTgoing (y Java no proporciona ningún soporte de código para especificar la diferencia). Bob Martin lo expresa de esta manera en su libro Código limpio, "En general, se deben evitar los argumentos de salida". Otra desventaja de usar argumentos como un medio para que un método proporcione un estado o salida al llamador es que esto se suma al desorden de argumentos pasados ​​a un método. Con esto en mente, el resto de esta publicación se enfoca en alternativas para devolver múltiples valores a través de parámetros pasados.

Aunque los métodos Java solo pueden devolver un único objeto o primitivo, esto realmente no es una gran limitación cuando se considera que un objeto puede ser casi cualquier cosa que queramos que sea. Hay varios enfoques que he visto pero que no recomiendo. Uno de estos es devolver una matriz o colección de instancias de Object con cadaObjectsiendo una "cosa" dispar, distinta y, a menudo, no relacionada. Por ejemplo, el método puede devolver tres valores como tres elementos de una matriz o colección. Una variación de este enfoque es usar una tupla de par o una tupla de tamaño n para devolver múltiples valores asociados. Otra variación de este enfoque es devolver un mapa de Java que asigna claves arbitrarias a su valor asociado. Al igual que con las otras soluciones, este enfoque impone una carga indebida al cliente para saber cuáles son esas claves y acceder a los valores del mapa a través de esas claves.

La siguiente lista de códigos contiene varios de estos enfoques menos atractivos para devolver múltiples valores sin secuestrar los parámetros del método para devolver múltiples valores.

Devolver varios valores a través de estructuras de datos genéricas

 // =============================================================== // 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; } 

Los enfoques que se muestran arriba cumplen con la intención de no pasar datos a la persona que llama a través de los parámetros de los métodos invocados, pero todavía hay una carga innecesaria sobre la persona que llama para conocer detalles íntimos de la estructura de datos devuelta. Es bueno reducir la cantidad de parámetros del método y no violar el principio de la menor sorpresa, pero no es tan bueno exigir al cliente que conozca las complejidades de una estructura de datos compleja.

Prefiero escribir objetos personalizados para mis devoluciones cuando necesito devolver más de un valor. Es un poco más trabajo que usar una matriz, colección o estructura de tupla, pero la pequeña cantidad de trabajo adicional (por lo general, unos minutos con los IDE de Java modernos) se compensa con la legibilidad y la fluidez que no está disponible con estos enfoques más genéricos. En lugar de tener que explicar con Javadoc o exigir a los usuarios de mi código que lean mi código cuidadosamente para saber qué parámetros se proporcionan en qué orden en la matriz o colección o qué valor es cuál en la tupla, mis objetos de retorno personalizados pueden tener métodos definidos en ellos que le dicen al cliente exactamente lo que están proporcionando.

Los fragmentos de código que siguen ilustran una Movieclase simple generada en gran parte por NetBeans que puede usarse como el tipo de retorno junto con el código que podría devolver una instancia de esa clase en lugar de una estructura de datos más genérica y menos legible.

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 + '}'; } } 

Devolver varios detalles en un solo objeto

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

La simple escritura del Moviela clase me tomó unos 5 minutos. Usé el asistente de creación de clases de NetBeans para seleccionar el nombre de la clase y el paquete y luego escribí los cuatro atributos de la clase. A partir de ahí, simplemente utilicé el mecanismo "Insertar código" de NetBeans para insertar métodos de acceso "get" junto con los métodos toString (), hashCode () y equals (Object) anulados. Si no creyera que necesito algo de eso, podría simplificar la clase, pero realmente es fácil de crear tal como está. Ahora, tengo un tipo de retorno mucho más utilizable y esto se refleja en el código que usa la clase. No necesita tantos comentarios de Javadoc sobre el tipo de retorno porque ese tipo habla por sí mismo y anuncia su contenido con sus métodos "get".Siento que la pequeña cantidad de esfuerzo adicional para crear estas clases simples para devolver múltiples valores vale la pena con enormes dividendos en comparación con alternativas como devolver el estado a través de parámetros de método o usar estructuras de datos de retorno más genéricas y más difíciles de usar.

No es demasiado sorprendente que un tipo personalizado para contener los múltiples valores que se devolverán a una persona que llama sea una solución atractiva. Después de todo, esto es conceptualmente muy similar a los conceptos sobre los que publiqué anteriormente relacionados con el uso de tipos personalizados y objetos de parámetros para pasar múltiples parámetros relacionados en lugar de pasarlos todos individualmente. Java es un lenguaje orientado a objetos, por lo que me sorprende cuando no veo que los objetos se utilicen con más frecuencia en el código Java para organizar parámetros Y valores de retorno en un paquete agradable.

Beneficios y ventajas

Las ventajas de usar objetos de parámetros personalizados para representar y encapsular múltiples valores de retorno son obvias. Los parámetros del método pueden seguir siendo parámetros de "entrada" porque toda la información de salida (excepto la información de error comunicada a través del mecanismo de excepción) se puede proporcionar en el objeto personalizado devuelto por el método. Este es un enfoque más limpio que el uso de matrices genéricas, colecciones, mapas, tuplas u otras estructuras de datos genéricas porque todos esos enfoques alternativos transfieren el esfuerzo de desarrollo a todos los clientes potenciales.

Costos y desventajas

Veo muy pocas desventajas en escribir tipos personalizados con múltiples valores para usarlos como tipos de retorno de métodos Java. Quizás el costo más demandado es el precio de escribir y probar estas clases, pero ese costo es bastante pequeño porque estas clases tienden a ser simples y porque los IDE modernos hacen la mayor parte del trabajo por nosotros. Debido a que los IDE lo hacen automáticamente, el código suele ser correcto. Las clases son tan simples que los revisores de código las pueden leer fácilmente y son fáciles de probar.