Статьи

Слишком много параметров в методах Java, часть 3: шаблон компоновщика

В двух моих предыдущих предыдущих постах я рассмотрел уменьшение количества параметров, необходимых для вызова конструктора или метода с помощью пользовательских типов и объектов параметров. В этой статье я расскажу об использовании шаблона построителя для уменьшения количества параметров, требуемых для конструктора, и расскажу о том, как этот шаблон может даже помочь с неконструктивными методами, которые принимают слишком много параметров.

Во втором издании Effective Java Джош Блох представляет использование шаблона компоновщика в Item # 2 для работы с конструкторами, которые требуют слишком много параметров. Bloch не только демонстрирует, как использовать Builder, но и объясняет его преимущества перед конструкторами, принимающими большое количество параметров. Я доберусь до этих преимуществ в конце этого поста, но думаю, что важно отметить, что Блох посвятил этой статье целый пункт в своей книге.

Чтобы проиллюстрировать преимущества этого подхода, я буду использовать следующий пример класса Person . В нем нет всех методов, которые я обычно добавляю к такому классу, потому что я хочу сосредоточиться на его построении.

Person.java (без Pattern Builder)

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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 делает это путем создания всего нового класса Builder).

PersonBuilder.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
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 был вложенным классом внутри класса, объект которого он создает, но автоматическое создание автономного Builder в NetBeans очень просто в использовании. Другое различие между созданным NetBeans Builder и Builders, которые я люблю писать, состоит в том, что в моих предпочтительных реализациях Builder обязательные поля предусмотрены в конструкторе Builder, а не в конструкторе без аргументов. Следующий листинг кода показывает мой класс Person сверху с добавленным в него в качестве вложенного класса Builder.

Person.java с Nested Person.Builder

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
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);
      }
   }
}

Построитель может быть еще лучше, если его улучшить за счет использования пользовательских типов и параметров объектов, как описано в моих первых двух статьях о проблеме «слишком много параметров». Это показано в следующем листинге кода.

Person.java с вложенным конструктором, пользовательскими типами и объектом параметров

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
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
    * Effective Java 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);
      }
   }
}

Последняя пара листингов кода показывает, как обычно используется Builder — для создания объекта. В самом деле, пункт о конструкторе (элемент № 2) во Втором издании Джошуа Блоха « Эффективной Java» находится в главе о создании (и уничтожении) объекта. Тем не менее, конструктор может косвенно помочь с неконструктивными методами, предоставляя более простой способ построения объектов параметров, которые передаются в методы.

Например, в последнем листинге кода методам FullName некоторые объекты параметров ( FullName и Address ). Для клиентов может быть утомительно создавать объекты этих параметров, а компоновщик можно использовать, чтобы сделать этот процесс менее утомительным. Таким образом, хотя компоновщик используется для конструирования в каждом случае, он косвенно выигрывает у неконструктивных методов, позволяя упростить использование объектов параметров, которые уменьшают количество аргументов метода.

Новые определения классов FullName и Address которые будут использоваться в качестве объектов параметров и используются самим Builder, показаны далее.

FullName.java с помощью Builder

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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);
      }
   }
}

С помощью вышеуказанных компоновщиков, включенных в классы, можно создать экземпляр Person как показано в следующем листинге кода. После этого для сравнения показано более традиционное создание экземпляра Person .

Два примера клиентского кода, демонстрирующего Person со строителями

01
02
03
04
05
06
07
08
09
10
11
12
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();

Создание личности без строителя

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

Как показывают предыдущие фрагменты кода, клиентский код для вызова традиционного Java-конструктора гораздо менее читабелен и намного проще испортить, чем использование классов построителя. Разнообразие одних и тех же типов (строки и логические значения) и необходимость помещать пустые значения в вызове конструктора для необязательных атрибутов делают многие способы неудачного завершения этого подхода.

Преимущества и преимущества

Шаблон Builder требует значительных затрат, поскольку необходимо существенно удвоить количество строк кода для каждого атрибута и установить эти атрибуты. Однако эта цена окупается, когда клиентский код значительно выигрывает в плане удобства использования и удобочитаемости. Параметры конструктора уменьшены и предоставляются в легко читаемых вызовах методов.

Еще одним преимуществом подхода Builder является возможность получения объекта в одном выражении и состоянии без проблемы объекта в нескольких состояниях, представленной с использованием методов «set». Я все больше оцениваю ценность неизменяемости в многоядерном мире, и шаблон Builder идеально подходит для неизменяемого класса, когда этот класс имеет большое количество атрибутов. Мне также нравится, что нет необходимости передавать значение NULL для необязательных параметров конструктору.

Шаблон Builder не только делает код более читабельным, но еще более упрощает применение функции автозавершения кода в среде IDE. Дополнительные преимущества шаблона Builder при использовании с конструкторами описаны в пункте № 2 второго издания Effective Java .

Затраты и недостатки

Как показано и упомянуто выше, количество строк кода данного класса должно быть по существу удвоено для методов «set» с подходом построителя . Кроме того, хотя клиентский код более читабелен, клиентский код также более многословен. Я считаю, что преимущество большей читабельности стоит затрат по мере увеличения числа аргументов или увеличения числа аргументов одного типа или увеличения числа необязательных аргументов.

Дополнительные строки кода в классе вместе со сборщиком иногда означают, что разработчики могут забыть добавить поддержку нового атрибута построителю, когда они добавят этот атрибут в основной класс. Чтобы попытаться помочь с этим, мне нравится вкладывать моих разработчиков в создаваемый ими класс, чтобы для разработчика было более очевидно, что существует соответствующий конструктор, который необходимо аналогичным образом обновить. Хотя по-прежнему существует риск того, что разработчик забудет добавить поддержку нового атрибута в конструктор, на самом деле это ничем не отличается от риска забыть добавить новый атрибут в класс toString () , equals (Object) , hashCode (). или другие методы, часто основанные на всех атрибутах класса.

В моей реализации Builder я заставил клиента передавать требуемые атрибуты в конструктор Builder, а не с помощью методов «set». Преимущество этого состоит в том, что объект всегда создается в «завершенном» состоянии, а не находится в неполном состоянии, пока разработчик не вызовет (если вообще не вызовет) соответствующий метод «set» для установки дополнительных полей. Это необходимо, чтобы пользоваться преимуществами неизменности. Однако незначительным недостатком этого подхода является то, что я не получаю преимущества читабельности методов, названных для поля, которое я устанавливаю.

Конструктор, как следует из его названия, на самом деле является лишь альтернативой конструкторам и не используется напрямую для уменьшения количества параметров неконструктивного метода. Однако конструктор можно использовать вместе с объектами параметров, чтобы уменьшить количество аргументов неконструктивного метода. Дальнейшие аргументы против использования Builder для создания объектов можно найти в комментарии к статье A aive to the Builder .

Вывод

Мне очень нравится шаблон Builder для построения объектов, когда у меня много параметров, особенно если многие из этих параметров имеют значение null и когда многие из них имеют один и тот же тип данных. Разработчик может почувствовать, что дополнительный код для реализации Builder может не оправдать его преимущества для небольшого числа параметров, особенно если требуется несколько параметров и разных типов. В таких случаях может быть сочтено желательным использовать традиционные конструкторы или, если неизменность не требуется, использовать конструктор без аргументов и требовать, чтобы клиент знал, чтобы вызывать необходимые методы «set».