`
sunxboy
  • 浏览: 2876693 次
  • 性别: Icon_minigender_1
  • 来自: 武汉
社区版块
存档分类
最新评论

Use Builder pattern to avoid method has too many parameters

 
阅读更多

 

sometimes, we have a class need to have a constructor which have too many parameters, like this:

    public InverterInfoView(String description, String clientId, boolean isActive, 
                    int panel1Power, int panel2Power, int gridFrequence,
                    int outputPower, int lineRAC, int lineSAC, int lineTAC) {
       
        this.description=description;
        this.inverterId=clientId;
        this.inverterStatus=isActive;
        // ... ...
}

 

this is not a good practise, so we need refactor.

 

 I looked at reducing the number of parameters required for a constructor or method invocation via custom types and parameter objects. In this post, I look at use of thebuilder pattern to reduce the number of parameters required for a constructor with some discussion on how this pattern can even help with non-constructor methods that take too many parameters.

In the Second Edition of Effective JavaJosh Bloch introduces use of the builder pattern in Item #2 for dealing with constructors that require too many parameters. Bloch not only demonstrates how to use the Builder, but explains it advantages over constructors accepting a large number of parameters. I will get to those advantages at the end of this post, but think it's important to point out that Bloch has devoted an entire item in his book to this practice.

To illustrate the advantages of this approach, I'll use the following example Person class. It doesn't have all the methods I would typically add to such a class because I want to focus on its construction.

package dustin.examples;

/**
 * Person class used as part of too many parameters demonstration.
 * 
 * @author Dustin
 */
public class Person
{
   private final String lastName;
   private final String firstName;
   private final String middleName;
   private final String salutation;
   private final String suffix;
   private final String streetAddress;
   private final String city;
   private final String state;
   private final boolean isFemale;
   private final boolean isEmployed;
   private final boolean isHomewOwner;

   public Person(
      final String newLastName,
      final String newFirstName,
      final String newMiddleName,
      final String newSalutation,
      final String newSuffix,
      final String newStreetAddress,
      final String newCity,
      final String newState,
      final boolean newIsFemale,
      final boolean newIsEmployed,
      final boolean newIsHomeOwner)
   {
      this.lastName = newLastName;
      this.firstName = newFirstName;
      this.middleName = newMiddleName;
      this.salutation = newSalutation;
      this.suffix = newSuffix;
      this.streetAddress = newStreetAddress;
      this.city = newCity;
      this.state = newState;
      this.isFemale = newIsFemale;
      this.isEmployed = newIsEmployed;
      this.isHomewOwner = newIsHomeOwner;
   }
}

 This class's constructor works, but it is difficult for client code to use properly. The Builder pattern can be used to make the constructor easier to use. NetBeans will refactor this for me as I have written about previously. An example of the refactored code is shown next (NetBeans does this by creating all new Builder class).

package dustin.examples;


public class PersonBuilder
{
   private String newLastName;
   private String newFirstName;
   private String newMiddleName;
   private String newSalutation;
   private String newSuffix;
   private String newStreetAddress;
   private String newCity;
   private String newState;
   private boolean newIsFemale;
   private boolean newIsEmployed;
   private boolean newIsHomeOwner;

   public PersonBuilder()
   {
   }

   public PersonBuilder setNewLastName(String newLastName) {
      this.newLastName = newLastName;
      return this;
   }

   public PersonBuilder setNewFirstName(String newFirstName) {
      this.newFirstName = newFirstName;
      return this;
   }

   public PersonBuilder setNewMiddleName(String newMiddleName) {
      this.newMiddleName = newMiddleName;
      return this;
   }

   public PersonBuilder setNewSalutation(String newSalutation) {
      this.newSalutation = newSalutation;
      return this;
   }

   public PersonBuilder setNewSuffix(String newSuffix) {
      this.newSuffix = newSuffix;
      return this;
   }

   public PersonBuilder setNewStreetAddress(String newStreetAddress) {
      this.newStreetAddress = newStreetAddress;
      return this;
   }

   public PersonBuilder setNewCity(String newCity) {
      this.newCity = newCity;
      return this;
   }

   public PersonBuilder setNewState(String newState) {
      this.newState = newState;
      return this;
   }

   public PersonBuilder setNewIsFemale(boolean newIsFemale) {
      this.newIsFemale = newIsFemale;
      return this;
   }

   public PersonBuilder setNewIsEmployed(boolean newIsEmployed) {
      this.newIsEmployed = newIsEmployed;
      return this;
   }

   public PersonBuilder setNewIsHomeOwner(boolean newIsHomeOwner) {
      this.newIsHomeOwner = newIsHomeOwner;
      return this;
   }

   public Person createPerson() {
      return new Person(newLastName, newFirstName, newMiddleName, newSalutation, newSuffix, newStreetAddress, newCity, newState, newIsFemale, newIsEmployed, newIsHomeOwner);
   }
   
}

 I prefer to have my Builder as a nested class inside the class whose object it builds, but the NetBeans automatic generation of a standalone Builder is very easy to use. Another difference between the NetBeans-generated Builder and the Builders I like to write is that my preferred Builder implementations have required fields provided in the Builder's constructor rather than provide a no-arguments constructor. The next code listing shows my Person class from above with a Builder added into it as a nested class.

 

package dustin.examples;

/**
 * Person class used as part of too many parameters demonstration.
 * 
 * @author Dustin
 */
public class Person
{
   private final String lastName;
   private final String firstName;
   private final String middleName;
   private final String salutation;
   private final String suffix;
   private final String streetAddress;
   private final String city;
   private final String state;
   private final boolean isFemale;
   private final boolean isEmployed;
   private final boolean isHomewOwner;

   public Person(
      final String newLastName,
      final String newFirstName,
      final String newMiddleName,
      final String newSalutation,
      final String newSuffix,
      final String newStreetAddress,
      final String newCity,
      final String newState,
      final boolean newIsFemale,
      final boolean newIsEmployed,
      final boolean newIsHomeOwner)
   {
      this.lastName = newLastName;
      this.firstName = newFirstName;
      this.middleName = newMiddleName;
      this.salutation = newSalutation;
      this.suffix = newSuffix;
      this.streetAddress = newStreetAddress;
      this.city = newCity;
      this.state = newState;
      this.isFemale = newIsFemale;
      this.isEmployed = newIsEmployed;
      this.isHomewOwner = newIsHomeOwner;
   }

   public static class PersonBuilder
   {
      private String nestedLastName;
      private String nestedFirstName;
      private String nestedMiddleName;
      private String nestedSalutation;
      private String nestedSuffix;
      private String nestedStreetAddress;
      private String nestedCity;
      private String nestedState;
      private boolean nestedIsFemale;
      private boolean nestedIsEmployed;
      private boolean nestedIsHomeOwner;

      public PersonBuilder(
         final String newFirstName,
         final String newCity,
         final String newState) 
      {
         this.nestedFirstName = newFirstName;
         this.nestedCity = newCity;
         this.nestedState = newState;
      }

      public PersonBuilder lastName(String newLastName)
      {
         this.nestedLastName = newLastName;
         return this;
      }

      public PersonBuilder firstName(String newFirstName)
      {
         this.nestedFirstName = newFirstName;
         return this;
      }

      public PersonBuilder middleName(String newMiddleName)
      {
         this.nestedMiddleName = newMiddleName;
         return this;
      }

      public PersonBuilder salutation(String newSalutation)
      {
         this.nestedSalutation = newSalutation;
         return this;
      }

      public PersonBuilder suffix(String newSuffix)
      {
         this.nestedSuffix = newSuffix;
         return this;
      }

      public PersonBuilder streetAddress(String newStreetAddress)
      {
         this.nestedStreetAddress = newStreetAddress;
         return this;
      }

      public PersonBuilder city(String newCity)
      {
         this.nestedCity = newCity;
         return this;
      }

      public PersonBuilder state(String newState)
      {
         this.nestedState = newState;
         return this;
      }

      public PersonBuilder isFemale(boolean newIsFemale)
      {
         this.nestedIsFemale = newIsFemale;
         return this;
      }

      public PersonBuilder isEmployed(boolean newIsEmployed)
      {
         this.nestedIsEmployed = newIsEmployed;
         return this;
      }

      public PersonBuilder isHomeOwner(boolean newIsHomeOwner)
      {
         this.nestedIsHomeOwner = newIsHomeOwner;
         return this;
      }

      public Person createPerson()
      {
         return new Person(
            nestedLastName, nestedFirstName, nestedMiddleName,
            nestedSalutation, nestedSuffix,
            nestedStreetAddress, nestedCity, nestedState,
            nestedIsFemale, nestedIsEmployed, nestedIsHomeOwner);
      }
   }
}

 The Builder can be even nicer when enhanced through use of custom types and parameters objects as outlined in my first two posts on the "too many parameters" problem. This is shown in the next code listing.

package dustin.examples;

/**
 * Person class used as part of too many parameters demonstration.
 * 
 * @author Dustin
 */
public class Person
{
   private final FullName name;
   private final Address address;
   private final Gender gender;
   private final EmploymentStatus employment;
   private final HomeownerStatus homeOwnerStatus;

   /**
    * Parameterized constructor can be private because only my internal builder
    * needs to call me to provide an instance to clients.
    * 
    * @param newName Name of this person.
    * @param newAddress Address of this person.
    * @param newGender Gender of this person.
    * @param newEmployment Employment status of this person.
    * @param newHomeOwner Home ownership status of this person.
    */
   private Person(
      final FullName newName, final Address newAddress,
      final Gender newGender, final EmploymentStatus newEmployment,
      final HomeownerStatus newHomeOwner)
   {
      this.name = newName;
      this.address = newAddress;
      this.gender = newGender;
      this.employment = newEmployment;
      this.homeOwnerStatus = newHomeOwner;
   }

   public FullName getName()
   {
      return this.name;
   }

   public Address getAddress()
   {
      return this.address;
   }

   public Gender getGender()
   {
      return this.gender;
   }

   public EmploymentStatus getEmployment()
   {
      return this.employment;
   }

   public HomeownerStatus getHomeOwnerStatus()
   {
      return this.homeOwnerStatus;
   }

   /**
    * Builder class as outlined in the Second Edition of Joshua Bloch's
    * <em>Effective Java</em> that is used to build a {@link Person} instance.
    */
   public static class PersonBuilder
   {
      private FullName nestedName;
      private Address nestedAddress;
      private Gender nestedGender;
      private EmploymentStatus nestedEmploymentStatus;
      private HomeownerStatus nestedHomeOwnerStatus;

      public PersonBuilder(
         final FullName newFullName,
         final Address newAddress) 
      {
         this.nestedName = newFullName;
         this.nestedAddress = newAddress;
      }

      public PersonBuilder name(final FullName newName)
      {
         this.nestedName = newName;
         return this;
      }

      public PersonBuilder address(final Address newAddress)
      {
         this.nestedAddress = newAddress;
         return this;
      }

      public PersonBuilder gender(final Gender newGender)
      {
         this.nestedGender = newGender;
         return this;
      }

      public PersonBuilder employment(final EmploymentStatus newEmploymentStatus)
      {
         this.nestedEmploymentStatus = newEmploymentStatus;
         return this;
      }

      public PersonBuilder homeOwner(final HomeownerStatus newHomeOwnerStatus)
      {
         this.nestedHomeOwnerStatus = newHomeOwnerStatus;
         return this;
      }

      public Person createPerson()
      {
         return new Person(
            nestedName, nestedAddress, nestedGender,
            nestedEmploymentStatus, nestedHomeOwnerStatus);
      }
   }
}

 The last couple of code listings show how a Builder is typically used - to construct an object. Indeed, the item on the builder (Item #2) in Joshua Bloch's Second Edition of Effective Java is in the chapter on creating (and destroying) object. However, the builder can help indirectly with non-constructor methods by allowing an easier way to build parameters objects that are passed to methods.

For example, in the last code listing, the methods have some parameters objects (FullName and Address) passed to them. It can be tedious for clients to have to construct these parameters objects and the builder can be used to make that process less tedious. So, although the builder is used for construction in each case, it indirectly benefits non-constructor methods by allowing for easier use of the parameters objects that reduce a method's argument count.

The new definitions of the FullName and Address classes to be used as parameters objects and using the Builder themselves are shown next.

package dustin.examples;

/**
 * Full name of a person.
 * 
 * @author Dustin
 */
public final class FullName
{
   private final Name lastName;
   private final Name firstName;
   private final Name middleName;
   private final Salutation salutation;
   private final Suffix suffix;

   private FullName(
      final Name newLastName,
      final Name newFirstName,
      final Name newMiddleName,
      final Salutation newSalutation,
      final Suffix newSuffix)
   {
      this.lastName = newLastName;
      this.firstName = newFirstName;
      this.middleName = newMiddleName;
      this.salutation = newSalutation;
      this.suffix = newSuffix;
   }

   public Name getLastName()
   {
      return this.lastName;
   }

   public Name getFirstName()
   {
      return this.firstName;
   }

   public Name getMiddleName()
   {
      return this.middleName;
   }

   public Salutation getSalutation()
   {
      return this.salutation;
   }

   public Suffix getSuffix()
   {
      return this.suffix;
   }

   @Override
   public String toString()
   {
      return  this.salutation + " " + this.firstName + " " + this.middleName
            + this.lastName + ", " + this.suffix;
   }

   public static class FullNameBuilder
   {
      private final Name nestedLastName;
      private final Name nestedFirstName;
      private Name nestedMiddleName;
      private Salutation nestedSalutation;
      private Suffix nestedSuffix;

      public FullNameBuilder(
         final Name newLastName, final Name newFirstName)
      {
         this.nestedLastName = newLastName;
         this.nestedFirstName = newFirstName;
      }

      public FullNameBuilder middleName(final Name newMiddleName)
      {
         this.nestedMiddleName = newMiddleName;
         return this;
      }

      public FullNameBuilder salutation(final Salutation newSalutation)
      {
         this.nestedSalutation = newSalutation;
         return this;
      }

      public FullNameBuilder suffix(final Suffix newSuffix)
      {
         this.nestedSuffix = newSuffix;
         return this;
      }

      public FullName createFullName()
      {
         return new FullName(
            nestedLastName, nestedFirstName, nestedMiddleName,
            nestedSalutation, nestedSuffix);
      }
   }
}

 

package dustin.examples;

/**
 * Representation of a United States address.
 * 
 * @author Dustin
 */
public final class Address
{
   private final StreetAddress streetAddress;
   private final City city;
   private final State state;

   private Address(final StreetAddress newStreetAddress, final City newCity, final State newState)
   {
      this.streetAddress = newStreetAddress;
      this.city = newCity;
      this.state = newState;
   }

   public StreetAddress getStreetAddress()
   {
      return this.streetAddress;
   }

   public City getCity()
   {
      return this.city;
   }

   public State getState()
   {
      return this.state;
   }

   @Override
   public String toString()
   {
      return this.streetAddress + ", " + this.city + ", " + this.state;
   }
   
   public static class AddressBuilder
   {
      private StreetAddress nestedStreetAddress;
      private final City nestedCity;
      private final State nestedState;

      public AddressBuilder(final City newCity, final State newState)
      {
         this.nestedCity = newCity;
         this.nestedState = newState;
      }

      public AddressBuilder streetAddress(final StreetAddress newStreetAddress)
      {
         this.nestedStreetAddress = newStreetAddress;
         return this;
      }

      public Address createAddress()
      {
         return new Address(nestedStreetAddress, nestedCity, nestedState);
      }
   }
}

 With the above builders included in the classes, a Person instance can be created as shown in the next code listing. A more traditional instantiation of a Person instance is shown after that for comparison.

final Person person1 = new Person.PersonBuilder(
   new FullName.FullNameBuilder(
      new Name("Dynamite"), new Name("Napoleon")).createFullName(),
   new Address.AddressBuilder(
      new City("Preston"), State.ID).createAddress()).createPerson();

final Person person2 = new Person.PersonBuilder(
   new FullName.FullNameBuilder(
      new Name("Coltrane"), new Name("Rosco")).middleName(new Name("Purvis")).createFullName(),
   new Address.AddressBuilder(
      new City("Hazzard"), State.GA).createAddress())
      .gender(Gender.MALE).employment(EmploymentStatus.EMPLOYED).createPerson();

 

Instantiating a Person Without a Builder

final person = new Person("Coltrane", "Rosco", "Purvis", null, "Hazzard", "Georgia", false, true, true); 

 

As the previous code snippets show, the client code for calling a traditional Java constructor is far less readable and far easier to mess up than use of the builder classes. The variety of the same types (strings and booleans) and the necessity to place nulls in the constructor call for optional attributes make provide many ways for this approach to end badly.

Benefits and Advantages

There is a considerable cost to the Builder pattern in that one must essentially double the number of lines of code each attribute and for setting those attributes. This price pays off, however, when the client code benefits greatly in terms of usability and readability. The parameters to the constructor are reduced and are provided in highly readable method calls.

Another advantage of the Builder approach is the ability to acquire an object in a single statement and state without the object in multiple states problem presented by using "set" methods. I am increasingly appreciating the value of immutability in a multi-core world and the Builder pattern is perfectly suited for an immutable class when that class features a large number of attributes. I also like that there is no need to pass in null for optional parameters to the constructor.

The Builder pattern not only makes the code more readable, but makes it even easier to apply an IDE's code completion feature. Further benefits of the Builder pattern when used with constructors are outlined in Item #2 of the Second Edition of Effective Java.

Costs and Disadvantages

As shown and mentioned above, the number of lines of code of a given class must be essentially doubled for "set" methods with the builder approach. Furthermore, although client code is more readable, the client code is also more verbose. I consider the benefit of greater readability worth the cost as the number of arguments increase or as more arguments share the same type or as the number of optional arguments increase.

More lines of code in the class with the builder sometimes mean that developers may forget to add support for a new attribute to the builder when they add that attribute to the main class. To try to help with this, I like to nest my builders inside the class that they build so that it's more obvious to the developer that there is a relevant builder that needs to be similarly updated. Although there is still risk of the developer forgetting to add support for a new attribute to the builder, this is really no different than the risk of forgetting to add a new attribute to a class's toString()equals(Object)hashCode() or other methods often based on all attributes of a class.

In my implementation of the Builder, I made the client pass required attributes into the builder's constructor rather than via "set" methods. The advantage of this is that the object is always instantiated in a "complete" state rather than sitting in an incomplete state until the developer calls (if ever calls) the appropriate "set" method to set additional fields. This is necessary to enjoy the benefits of immutability. However, a minor disadvantage of that approach is that I don't get the readability advantages of methods named for the field I am setting.

The Builder, as its name suggests, is really only an alternative to constructors and not directly used to reduce the number of non-constructor method parameters. However, the builder can be used in conjunction with parameters objects to reduce the number of non-constructor method arguments. Further arguments against use of the Builder for object construction can be found in a comment on the A dive into the Builder pattern post.

Conclusion

I really like the Builder pattern for constructing objects when I have a lot of parameters, especially if many of these parameters are null and when many of them share the same data type. A developer might feel that the extra code to implement a Builder might not justify its benefits for a small number of parameters, especially if the few parameters are required and of different types. In such cases, it might be considered desirable to use traditional constructors or, if immutability is not desired, use a no-argument constructor and require the client to know to call the necessary "set" methods.

 

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics