Tuesday, October 15, 2013

Too Many Parameters in Java Methods, Part 4: Overloading

One of the problems with expecting too many parameters to be passed to a Java method is that it is more difficult for the client of that method to be determine that they are passing the appropriate values in the appropriate order. In previous posts, I have described how custom types, parameters objects, and builders can be used to address this issue. Another way to address this issue, and the subject of this post, is to provide overloaded versions of the same method for the clients to use the one that best fits their needs. As with my earlier posts on the subject of too many method parameters, I will end this post with a brief discussion of the advantages and disadvantages of this approach.

Java supports method overloading, the ability to have different version of the same method differentiated by their method signatures. Note that a different return type as sole difference between two methods is generally not sufficient for overloading.

Method overloading might be applied for a number of reasons. One objective for overloading methods might be to support the same functionality on different types (especially if generics cannot be used to allow the method to support different types or if the methods were written before generics were available). Examples of method overloading with this intent in mind include String.valueOf(boolean), String.valueOf(char), String.valueOf(double), String.valueOf(long), String.valueOf(Object), and a few more versions of String.valueOf overloaded on a few additional types.

Another reason one might choose to overload methods is so that a client can call the appropriate version of the method for supplying just the necessary parameters. This can be done, for example, to remove the need for the client to pass in one or more nulls for parameters that don't apply or are optional. Examples of overloaded methods written to achieve this objective include the Date class constructors such as Date(int, int, int), Date(int, int, int, int, int), and Date(int, int, int, int, int, int, int).

This approach of having many overloaded versions constructors, each accepting a different number of parameters, is known as telescoping constructors and has been labeled an anti-pattern by some. In fact, the downsides of this telescoping constructors approach is one of the drivers for Josh Bloch's focus on the Builder pattern in Item #2 of the Second Edition of Effective Java. Incidentally, the Date class also provides some overloaded constructors intended to accomplish the previously mentioned objective as well, allowing Date to be constructed from a String, for example.

The idea of supplying multiple overloaded methods and constructors to accept a reduced set of required or minimally applicable parameters can be applied to our own classes. The next code listing provides the original method with far too many parameters and then shows some potential overloaded versions of that method that accept a reduced set of parameters. For purposes of this discussion, we are assuming that any parameter not provided in one of the overridden method signatures is optional or not applicable for that particular method call. The comments on the code explain how each method makes certain assumptions to reduce its parameter count.

Example of Parameter with Too Many Methods and Overloaded Versions
   /**
    * Generates and provides an instance of the Person class. This method
    * expects all characteristics of the populated Person instance and so any
    * optional or not applicable characteristics must be passed in as null.
    * 
    * @param lastName
    * @param firstName
    * @param middleName
    * @param salutation
    * @param suffix
    * @param streetAddress
    * @param city
    * @param state
    * @param isFemale
    * @param isEmployed
    * @param isHomeOwner
    * @return A Person object.
    */
   public Person createPerson(
      final String lastName,
      final String firstName,
      final String middleName,
      final String salutation,
      final String suffix,
      final String streetAddress,
      final String city,
      final String state,
      final boolean isFemale,
      final boolean isEmployed,
      final boolean isHomeOwner)
   {
      // implementation goes here...
   }

   /**
    * Generate and provide an instance of the Person class that has only a first
    * and last name and address information. This method does not make any
    * assumptions about other characteristics of the instantiated Person, but
    * simply leaves those attributes undefined.
    * 
    * @param lastName
    * @param firstName
    * @param streetAddress
    * @param city
    * @param state
    * @return Instance of Person class with no middle name and without specified
    *    gender, employment status, or home ownership status.
    */
   public Person createPerson(
      final String lastName,
      final String firstName,
      final String streetAddress,
      final String city,
      final String state)
   {
      // implementation goes here...
   }

   /**
    * Generate and provide instance of Person class with no middle name and
    * with specified home ownership status. All instances of Person returned
    * from this method are assumed to be Female and to be Employed, but have no
    * address information.
    * 
    * @param lastName
    * @param firstName
    * @param homeOwnerStatus
    * @return Instance of Person with provided first name, provided last name,
    *    and provided home ownership status, and assumed to be an employed
    *    female.
    */
   public Person createPerson(
      final String lastName,
      final String firstName,
      final boolean homeOwnerStatus)
   {
      // implementation goes here...
   }

The overloaded methods' Javadoc descriptions tell a little about their differing approach. The first method expected all characteristics of the Person instance to be provided and null would need to be provided for parameters that are not applicable (such as if a person does not have a middle name or that middle name is not important for the use in this case). The second overloaded version did not expect all the parameters to be provided and assumed that the parameters it did not expect would remain undefined in the returned Person instance.

The third overloaded method version mostly made assumptions about the characteristics for which it did not provide an explicit parameter. For example, it assumed that the instantiated Person is both female and employed. There is no way with that third approach to instantiate a person who is either male or unemployed. This illustrates a weakness of dealing with too many parameters with simple method overloading (overloading methods with same name based only on number and types of parameters).

I have not shown any constructors of my own in this post, but the same issues and approaches apply as shown for the non-constructor methods above. Likewise, overloaded constructors share the same advantages and disadvantages as overloaded non-constructor methods.

Benefits and Advantages

Method overloading in Java seems easy to understand and is common in several languages including C/C++ and C#. Method overloading is particularly effective when parameters are optional. For example, method overloading that removed the expectation of a middle name being passed in was far more effective in my examples than the method overloading making assumptions about a specific instantiation being an employed female. If the characteristics of middle name, gender, and employment status are all truly optional, then not assuming a value for any of them at all seems better than assuming a specific value for them.

Costs and Disadvantages

Judicious method overloading can be useful, but method overloading must be used carefully. The Defining Methods section of the Classes and Objects lesson of the Learning the Java Language trail warns: "Overloaded methods should be used sparingly, as they can make code much less readable."

Even my simple three examples showed how overloading can quickly become difficult to read. In my examples, the reader or user of this code would either need to read the Javadoc carefully and trust it to be current and accurate or would need to delve into the implementation to see what each overloaded version of the method did. In an IDE, especially if there are numerous overloaded versions of the same method, it can be difficult to see which one applies in a given situation.

My example showed that comments must be used to explain assumptions made by the overloaded methods. As just mentioned, these could be out of date or inaccurate or not even available if the developer did not bother to write them. It would obviously be better to be able to name the methods differently so that the name of the method could give clues about its assumptions rather than relying solely on Javadoc. Using named methods in this way will be the subject of a later post, but using different names for the methods by definition makes them no longer overloaded methods.

My examples showed a particular limitation of using overloaded (same named) methods with multiple parameters of the same type. The third example accepts a single boolean, but only the Javadoc and the name of that parameter could tell me that it applied to home ownership and not to gender or employment status. I cannot provide similar overloaded methods to take the same name information and a boolean indicating something different (such as gender or employment status) because that method would have the same signature as the method where the boolean indicated home ownership status. This again could be remedied through the use of differently named methods that indicated for which boolean condition they applied.

Another way to address this last mentioned limitation would be to use custom types and/or parameters objects and provide various versions of overloaded methods accepting different combinations of those custom types. This is shown with custom types in the next code listing which shows how the method accepting two name Strings can be overloaded by a single third parameter for all three cases when those three cases don't each need to share the same type.

Custom Types Enable Improved Method/Constructor Overloading
   public Person createPerson(
      final String lastName,
      final String firstName,
      final HomeownerStatus homeOwnership)
   {
      // implementation goes here...
   }

   public Person createPerson(
      final String lastName,
      final String firstName,
      final Gender gender)
   {
      // implementation goes here...
   }

   public Person createPerson(
      final String lastName,
      final String firstName,
      final EmploymentStatus employmentStatus)
   {
      // implementation goes here...
   }

One final disadvantage I want to mention related to using method overloading to address the problems associated with too many parameters to a method or constructor is that such an approach can lead to significant maintenance work in the future. Any time an attribute to that class (constructors) or a parameter to a method is added or removed or even changed, multiple constructors and/or methods may need to be individually reviewed and potentially changed.

Conclusion

Overloaded methods do have their place and can be a convenient way to provide more understandable and readable methods and constructors for clients. However, I find this approach to be the "best" approach less often than some of the other approaches already covered (custom types, parameters objects, builders) and even less often than some of the approaches I intend to cover (such as differently and explicitly named versions of the same methods and constructors). Some of the limitations and disadvantages of the method overloading approach can be reduced through the use the method overloading in conjunction with some of these other approaches. For example, the use of custom types and parameters objects can significantly improve one's ability to more narrowly tailor the various versions of an overloaded method or constructor to what is desired.

No comments: