In the current series of posts I am writing on reducing the number of parameters required to call Java methods and constructors, I have focused so far on approaches that directly affect the parameters themselves (custom types, parameters objects, builder pattern, method overloading, and method naming). Given this, it might seem surprising for me to devote a post in this series to how Java methods provide return values. However, methods' return values can impact the parameters the methods accept when developers choose to provide "return" values by setting or changing provided parameters rather than or in addition to more traditional method return mechanisms.
The "traditional ways" that a non-constructor method returns a value can both be specified in the method signature. The most commonly recognized approach for returning a value from a Java method is via its declared return type. This often works well, but one of frustrations that most commonly occurs is being allowed to return only one value from a Java method..
Java's exception handling mechanism is also another approach for retaining a "result" of a method to callers. Checked exceptions, in particular, are advertised to the caller via the throws clause. In fact, Jim Waldo, in his book Java: The Good Parts, states that it is easier to understand Java exceptions when one thinks of Java exceptions as another type of method return limited to being a Throwable type.
Although the method's return type and thrown exceptions are intended as the primary approaches for methods to return information to callers, it is sometimes tempting to return data or state via the parameters passed into the method. When a method needs to return more than one piece of information, the single-value returns of Java methods can seem limiting. Although exceptions provide another way to communicate back to the caller, it seems almost universally agreed that exceptions should only be used for reporting exceptional situations and not for reporting "normal" data or used in control flow. Given that only one object or primitive can be returned from a method and that exceptions only allow returning of a Throwable
and should only be used to report exceptional situations, it becomes increasingly attractive for the Java developer to hijack parameters as an alternate route for returning data to the caller.
The technique that a developer can use to apply method parameters as carriers for return data is to accept parameters that are mutable and to mutate the passed-in objects' state. These mutable objects can have their contents changed by the method and then the caller can access the object it provided to determine its new state settings that have been applied by the called method. Although this can be done with any mutable object, collections seem particularly attractive to the developer trying to pass values back to the caller via parameters.
There are some disadvantages to passing state back to the called via the provided parameters. This approach often violates the principle of least astonishment as most Java developers probably expect parameters to be INcoming rather than OUTgoing (and Java doesn't provide any code support to specify the difference). Bob Martin puts it this way in his book Clean Code, "In general, output arguments should be avoided." Another disadvantage of using arguments as a means for a method to provide state or output to the caller is that this adds to the clutter of arguments passed to a method. With this in mind, the remainder of this post focuses on alternatives to returning multiple values via passed-in parameters.
Although Java methods can only return a single object or primitive, this is really not much of a limitation when one considers that an object can be just about anything we want it to be. There are several approaches that I've seen but don't recommend. One of these is returning an array or collection of Object instances with each Object
being a disparate and distinct and often unrelated "thing." For example, the method might return three values as three elements of an array or collection. A variation of this approach is to use a pair tuple or n-sized tuple to return multiple associated values. One other variation on this approach is to return a Java Map that maps arbitrary keys to their associated value. As with the other solutions, this approach places undue burden on the client to know what those keys are and to access the map values through those keys.
The next code listing contains several of these less attractive approaches for returning multiple values without hijacking the method parameters to return multiple values.
Returning Multiple Values via Generic Data Structures// =============================================================== // 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<Object> getMovieDetails() { return Arrays.<Object>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<String, Object> getMovieDetailsMap() { final HashMap<String, Object> 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; }
The approaches shown above do meet the intent of not passing data back to the caller via the invoked methods' parameters, but there is still unnecessary burden placed on the caller to know intimate details of the returned data structure. It's nice to reduce the number of parameters to the method and not violate the principle of least surprise, but it's not so nice to require the client to know the intricacies of a complex data structure.
I prefer to write custom objects for my returns when I need to return more than one value. It's a bit more work than using an array, collection, or tuple structure, but the very small amount of extra work (typically a few minutes with modern Java IDEs) pays off with readability and fluency that is not available with these more generic approaches. Rather than having to explain with Javadoc or require users of my code to read my code carefully to know which parameters are provided in which order in the array or collection or which value is which in the tuple, my custom return objects can have methods defined on them that tell the client exactly what they are providing.
The code snippets that follow illustrate a simple Movie
class largely generated by NetBeans that can be used as the return type along with the code that could return an instance of that class rather than a more generic and less readable data structure.
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 + '}'; } }Returning Multiple Details in Single Object
/** * Provide movie information. * * @return Movie information. */ public Movie getMovieInfo() { return new Movie("Oblivion", 2013, "Joseph Kosinski", "PG-13"); }
The simple writing of the Movie
class took me about 5 minutes. I used the NetBeans class creation wizard to select the class name and package and then I typed in the four attributes of the class. From there, I simply used NetBeans's "Insert Code" mechanism to insert "get" accessor methods along with overridden toString(), hashCode(), and equals(Object) methods. If I didn't think I needed some of that, I could keep the class simpler, but it really is easy to create as is. Now, I have a much more usable return type and this is reflected by the code that uses the class. It doesn't need nearly as much Javadoc comments on the return type because that type speaks for itself and advertises its content with its "get" methods. I feel that the small amount of additional effort to create these simple classes for returning multiple values pays off with huge dividends when compared to alternatives such as returning state via method parameters or using more generic and harder to use return data structures.
It is not too surprising that a custom type to hold the multiple values to be returned to a caller is an attractive solution. After all, this is conceptually very similar to the concepts I blogged about previously related to using custom types and parameters objects for passing in multiple related parameters rather than passing them all in individually. Java is an object-oriented language and so it surprises me when I don't see objects used more often in Java code for organizing parameters AND return values in a nice package.
Benefits and AdvantagesThe advantages of using custom parameter objects to represent and encapsulate multiple return values are obvious. Parameters to the method can remain "input" parameters because all output information (except for error information communicated via the exception mechanism) can be provided in the custom object returned by the method. This is a cleaner approach than using generic arrays, collections, maps, tuples, or other generic data structures because all of those alternative approaches shift development effort onto all potential clients.
Costs and DisadvantagesI see very little downside to writing custom types with multiple values to be used as return types from Java methods. Perhaps the most often claimed cost is the price of writing and testing these classes, but that cost is pretty small because these classes tend to be simple and because modern IDEs do most of the work for us. Because the IDEs do it automatically, the code is typically correct. The classes are so simple that they are easily readable by code reviewers and they are easy to test.
Stretching to find other costs and disadvantages, one might argue that these classes can bloat code bases and packages, but I don't see that as a strong argument. Although there may be a very small risk of a badly implemented custom class, I think the chance of client code messing up interpretation of a more generic return type is more likely to occur. One other small risk is that developers might throw a lot of unrelated stuff into the same class with the only relationship between those items being the fact that the same method needs to return them. Even with this, the only better alternative I can see would be to modify the code to not need to return multiple values. Returning otherwise unrelated items in a custom object still seems better than returning this set of unrelated data in a generic data structure. In fact, those generic data structures become more unwieldy and difficult to use as the values which they hold become less related.
ConclusionCustom types and parameters objects help us directly address the problem of too many parameters to a Java method. Fortunately, these custom types and return values objects can also help us indirectly reduce our number of required parameters by allowing us to return multiple values through the custom types and return values objects rather than needing to add additional parameters meant only to convey output information back to the caller.
No comments:
Post a Comment