Java方法参数太多怎么办—Part3—Builder模式

目录

  1. 自定义类型
  2. 引入参数对象
  3. Builder模式

本文是这个系列的第三篇文章,介绍了通过Builder模式应对参数过多的问题。如果你也希望参与类似的系列文章翻译,可以加入我们的Java开发技术翻译 小组。

在前两篇文章中,我分别使用了自定义类型和参数对象方法来减少构造器或方法调用传入的参数数量。本文关注的是Builder模式。我认为它不仅能够帮助构造函数“瘦身”,甚至可以对非构造函数也能发挥同样的作用。

Effective Java第二版中,Josh Bloch第二章中就提到使用Builder模式处理需要很多参数的构造函数。他不仅展示了Builder的使用,也描述了相这种方法相对使用带很多参数的构造函数带来的好处。在本文的结尾我将进一步总结Builde模式的优点。需要指出的是Josh Bloch已经在他的书本贯穿了这一思想。

为了说明这种方法的优点,我会继续使用Person类。需要指出的是,为了更好地聚焦于类构造本身我只添加了个别方法。

Person.java (非Builder模式)

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

这样写的类构造器也无可厚非,但当有许多参数时编写客户端代码会很困难。这时,如果使用Builder模式会有效提高代码的可读性。在我的博客中曾写过使用NetBeans能方便地实现代码重构。下面就是使用NetBeans重构过的代码。

PersonBuilder.java

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);
   }

}

相对于之前的写法,我更趋向于将builder作为它构建类的静态成员类(嵌套类)。即便是NetBeans自动生成的Builder使用起来也很方便。另一个区别在于,我希望Builder实现一个包含了必要字段的构造函数,而不是一个无参构造函数。

下面显示的是改进后的代码。

Person.java(Person.Builder嵌套类)

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);
      }
   }
}

使用Builder模式时,如果结合我前两篇文章介绍的自定义类型和参数对象方法一起使用效果会更好。示例代码如下。

Person.java (嵌套类Builder,自定义类型加参数对象)

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);
      }
   }
}

最后2段代码列表展示了Builder构建对象的常见方法。实际上,《在Effective Java 第二版》第2条中就是用Builder来创建和销毁对象。此外,Builder能更简单地通过参数对象传递间接实现了一个非构造函数方法。在上面最后一段代码中,这些方法都接收了一些参数对象。诚然,即使使用了Builder构建参数对象还是比较枯燥的。但每一次Builder构建都是间接让非构造函数方法受益,因为参数对象使用确实有效减少了方法参数的数量。

下面重新定义的FullName和Address类既能做参数本身也使用了Builder模式。

FullName.java(使用Builder)

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);
      }
   }
}

Address.java (使用Builder)

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);
      }
   }
}

由于用了Builder,新的Person实例可以通过下面这种方式创建。当然为了比较,也给出了传统的Person实例化方法。

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();

实例化Person(不使用Builder)

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

看完代码高下立判,调用传统JAVA构造器的客户端代码的可读性远不如使用了Builder的客户端代码。同一类型的变量和空放置在一起被调用将会导致一些微妙的错误。(试想,如果客户端不小心颠倒了其中的几个参数顺序,编译不会出错但在运行时肯定出错)

Builder模式好处和优点

使用Builder模式必然会导致写两遍相关属性的代码和SETTER方法,看起来有点吃力不讨好。然而需要看到的是,客户端代码的可用性和可读性得到了大大提高。与此同时,构造函数的参数数量明显减少调用起来非常直观。

Builder方法另外一个优势在于,单个builder构建多个对象时Builder参数可在创建期间进行调整,还可以根据对象不同而进行改变。这就像我越来越推崇的以“不变”应“万变”Builder模式特别适合那些属性个数很多的类,我认为没有必要给那些本不需要设置值传递参数(设置null)。

Builder模式在提高代码可读性的同时,使用IDE提供的代码补全功能也更加容易。Builder模式在与构造函数一起使用带来的更大优势在Josh Bloch的Effective Java第二版第2条中有详细阐述。

Builder模式的代价和缺点

使用Builder模式是肯定会增加代码量的。此外,尽管客户端的代码可读性明显改善,但随之而来的客户端代码变得更加冗长。我还是坚持这一观点:相比较参数数量的增加,相同类型的参数混在一起,可选参数的增加而言,改善代码可读性更有价值。

Builder会增加个类代码,这也意味着开发者在给类增加属性时有时会忘记给该属性添加支持的builder。为了克服这个问题,通常我会将builder嵌套到类中,这样可以很容易地发现哪个相关的builder需要更新。尽管忘记的风险依旧存在,但是这风险就像忘记给类的新属性增加 toString()、 equals(Object)、 hashCode()或其他类基于是所有属性的方法一样。

在我的Builder实现中,我会用Builder的构造函数而不是set方法传递客户需要的属性。这样做的好处在于,对象总是能被一次完整的实例化,而不是靠开发人员调用时用set方法补充额外的属性完成实例化。这也体现了不可变性带来的好处。然而,相应地也会造成自己设定的属性方法可读性降低。

正如它名字表示的那样,Builder只是一个替代构造器的选择不能直接用于降低非构造函数方法的参数数量,但是结合参数对象的使用就能达到这一点。有关更多反对用Builder来进行对象构建的讨论可以参见 A dive into the Builder pattern上的相关评论

总结

构建对象时,如果碰到类有很多参数——其中很多参数类型相同而且很多参数可以为空时,我更喜欢Builder模式来完成。当参数数量不多、类型不同而且都是必须出现时,通过增加代码实现Builder往往无法体现它的优势。在这种情况下,理想的方法是调用传统的构造函数。再者,如果不需要保持不变,那么就使用无参构造函数调用相应的set方法吧。

原文链接: dzone 翻译: ImportNew.com - 韩远青
译文链接: http://www.importnew.com/6605.html
[ 转载请保留原文出处、译者和译文链接。]



相关文章

发表评论

Comment form

(*) 表示必填项

2 条评论

  1. shellvon 说道:

    我擦。。。这真的可以createFullName么?没有哪个构造函数啊。。连续的set我都相信。。

    Thumb up 1 Thumb down 0

  2. Retina 说道:

    Builder模式这样使用,我觉得吧简单的问题弄复杂了。

    Thumb up 1 Thumb down 0

跳到底部
返回顶部