Статьи

Слишком много параметров в методах Java, часть 7: изменяемое состояние

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

Возможно, самый известный и наиболее презираемый подход во всех разработках программного обеспечения для использования состояния для уменьшения параметров — это использование глобальных переменных . Хотя с семантической точностью можно сказать, что в Java нет глобальных переменных , реальность такова, что в любом случае эквивалент глобальных переменных в Java достигается посредством открытых статических конструкций . Особенно популярный способ достичь этого в Java — через Stateful Singleton .

Мартин Фаулер (Martin Fowler) в статье «Шаблоны архитектуры корпоративных приложений» писал, что «любые глобальные данные всегда виновны, пока их не доказывают невиновность». Глобальные переменные и «глобальные» конструкции в Java считаются плохой формой по нескольким причинам . Они могут затруднить разработчикам, поддерживающим и читающим код, возможность узнать, где определены значения, когда они в последний раз изменялись или вообще пришли. По своей природе и предназначению глобальные данные нарушают принципы инкапсуляции и сокрытия данных.

Мишко Хевери написал следующее о проблемах статических глобалов в объектно-ориентированном языке:


Статический доступ к глобальному состоянию не проясняет эти общие зависимости для читателей конструкторов и методов, которые используют глобальное состояние. Global State и Singletons заставляют API лгать об их истинных зависимостях. Корень проблемы с глобальным состоянием в том, что оно глобально доступно. В идеальном мире объект должен иметь возможность взаимодействовать только с другими объектами, которые были непосредственно переданы ему (через конструктор или вызов метода).

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

Изменчивое состояние также становится все более серьезной проблемой, поскольку параллельные приложения становятся все более распространенными. В своей презентации JavaOne 2012, посвященной Scala , создатель Scala Мартин Одерски заявил, что «каждый изменяемый вами элемент является обязательством» в мире с высокой степенью одновременности, и добавил, что проблема заключается в «недетерминированности, вызванной одновременными потоками, обращающимися к общему изменяемому состоянию. »

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

Синглтон с состоянием и статические переменные

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

Любые данные, хранящиеся повсеместно (по крайней мере, с точки зрения JVM / загрузчика классов), уже доступны клиентскому коду в той же JVM и загружены с тем же загрузчиком классов. По этой причине нет необходимости передавать эти данные между клиентами и методами или конструкторами в той же комбинации JVM / загрузчик классов.

Состояние экземпляра

В то время как «статика» считается «доступной глобально», более узкое состояние уровня экземпляра также можно использовать аналогичным образом, чтобы уменьшить необходимость передачи параметров между методами одного и того же класса. Преимущество этого перед глобальными переменными состоит в том, что доступность ограничена экземплярами класса ( приватные поля) или экземплярами дочерних элементов класса ( приватные поля). Конечно, если поля являются общедоступными , доступность довольно широко открыта, но те же данные автоматически не доступны для другого кода в той же JVM / classloader.

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

Пример состояния экземпляра, используемого для избежания передачи параметров

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
/**
    * Simple example of using instance variable so that there is no need to
    * pass parameters to other methods defined in the same class.
    */
   public void doSomethingGoodWithInstanceVariables()
   {
      this.person =
         Person.createInstanceWithNameAndAddressOnly(
            new FullName.FullNameBuilder(new Name("Flintstone"), new Name("Fred")).createFullName(),
            new Address.AddressBuilder(new City("Bedrock"), State.UN).createAddress());
      printPerson();
   }
 
   /**
    * Prints instance of Person without requiring it to be passed in because it
    * is an instance variable.
    */
   public void printPerson()
   {
      out.println(this.person);
   }

Вышеприведенный пример несколько надуманный и упрощенный, но он иллюстрирует суть: переменная экземпляра person может быть доступна с помощью других методов экземпляра, определенных в том же классе, так что экземпляр не нужно передавать между этими методами экземпляра. Это уменьшает сигнатуру потенциально ( public доступности означает, что она может использоваться внешними методами) внутренних методов, но также вводит состояние и теперь означает, что вызванный метод влияет на состояние этого же объекта. Другими словами, преимущество отсутствия необходимости передавать параметр достигается за счет другого фрагмента изменяемого состояния. Другая сторона компромисса, требующая передачи экземпляра Person поскольку он не является переменной экземпляра, показана в следующем листинге кода для сравнения.

Пример передачи параметра вместо использования переменной экземпляра

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
/**
    * Simple example of passing a parameter rather than using an instance variable.
    */
   public void doSomethingGoodWithoutInstanceVariables()
   {
      final Person person =
         Person.createInstanceWithNameAndAddressOnly(
            new FullName.FullNameBuilder(new Name("Flintstone"), new Name("Fred")).createFullName(),
            new Address.AddressBuilder(new City("Bedrock"), State.UN).createAddress());
      printPerson(person);
   }
 
   /**
    * Prints instance of Person that is passed in as a parameter.
    *
    * @param person Instance of Person to be printed.
    */
   public void printPerson(final Person person)
   {
      out.println(person);
   }

Предыдущие два листинга кода показывают, что передача параметров может быть уменьшена с помощью экземпляра состояния. Я обычно предпочитаю не использовать состояние экземпляра исключительно для того, чтобы избежать передачи параметров. Если состояние экземпляра необходимо по другим причинам, то уменьшение передаваемых параметров является хорошим дополнительным преимуществом, но мне не нравится вводить ненужное состояние экземпляра просто для удаления или уменьшения количества параметров. Хотя было время, когда читаемость сокращенных параметров могла оправдать состояние экземпляра в большой однопоточной среде, я чувствую, что небольшое увеличение читаемости от сокращенных параметров не стоит затрат на классы, которые не являются поточно-ориентированными во все возрастающей многопоточный мир. Мне все еще не нравится передавать множество параметров между методами одного и того же класса, но я могу использовать объект параметров (возможно, с классом области пакета ), чтобы уменьшить количество этих параметров и передать объект этих параметров вокруг вместо большого количества параметров.

Конструкция в стиле JavaBean

Соглашение / стиль JavaBeans стали чрезвычайно популярными в сообществе разработчиков Java. Многие фреймворки, такие как Spring Framework и Hibernate, полагаются на классы, соответствующие соглашениям JavaBeans, а некоторые стандарты, такие как Java Persistence API, также построены на соглашениях JavaBeans. Существует множество причин популярности стиля JavaBeans, в том числе его простота использования и возможность использовать отражение в этом коде, придерживаясь этого соглашения, чтобы избежать дополнительной настройки.

Общая идея стиля JavaBean состоит в том, чтобы создать экземпляр объекта с помощью конструктора без аргументов, а затем установить его поля с помощью методов «set» с одним аргументом и получить доступ к полям с помощью методов «get» без аргументов. Это продемонстрировано в следующих листингах кода. Первый листинг показывает простой пример класса PersonBean с конструкторами без аргументов и методами получения и установки. Этот листинг кода также включает некоторые классы в стиле JavaBeans, которые он использует. За этим списком кода следует код, использующий этот класс стиля JavaBean.

Примеры стилей JavaBeans

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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
public class PersonBean
{
   private FullNameBean name;
   private AddressBean address;
   private Gender gender;
   private EmploymentStatus employment;
   private HomeownerStatus homeOwnerStatus;
 
   /** No-arguments constructor. */
   public PersonBean() {}
 
   public FullNameBean getName()
   {
      return this.name;
   }
 
   public void setName(final FullNameBean newName)
   {
      this.name = newName;
   }
 
   public AddressBean getAddress()
   {
      return this.address;
   }
 
   public void setAddress(final AddressBean newAddress)
   {
      this.address = newAddress;
   }
 
   public Gender getGender()
   {
      return this.gender;
   }
 
   public void setGender(final Gender newGender)
   {
      this.gender = newGender;
   }
 
   public EmploymentStatus getEmployment()
   {
      return this.employment;
   }
 
   public void setEmployment(final EmploymentStatus newEmployment)
   {
      this.employment = newEmployment;
   }
 
   public HomeownerStatus getHomeOwnerStatus()
   {
      return this.homeOwnerStatus;
   }
 
   public void setHomeOwnerStatus(final HomeownerStatus newHomeOwnerStatus)
   {
      this.homeOwnerStatus = newHomeOwnerStatus;
   }
}
 
/**
 * Full name of a person in JavaBean style.
 *
 * @author Dustin
 */
public final class FullNameBean
{
   private Name lastName;
   private Name firstName;
   private Name middleName;
   private Salutation salutation;
   private Suffix suffix;
 
   /** No-args constructor for JavaBean style instantiation. */
   private FullNameBean() {}
 
   public Name getFirstName()
   {
      return this.firstName;
   }
 
   public void setFirstName(final Name newFirstName)
   {
      this.firstName = newFirstName;
   }
 
   public Name getLastName()
   {
      return this.lastName;
   }
 
   public void setLastName(final Name newLastName)
   {
      this.lastName = newLastName;
   }
 
   public Name getMiddleName()
   {
      return this.middleName;
   }
 
   public void setMiddleName(final Name newMiddleName)
   {
      this.middleName = newMiddleName;
   }
 
   public Salutation getSalutation()
   {
      return this.salutation;
   }
 
   public void setSalutation(final Salutation newSalutation)
   {
      this.salutation = newSalutation;
   }
 
   public Suffix getSuffix()
   {
      return this.suffix;
   }
 
   public void setSuffix(final Suffix newSuffix)
   {
      this.suffix = newSuffix;
   }
 
   @Override
   public String toString()
   {
      return  this.salutation + " " + this.firstName + " " + this.middleName
            + this.lastName + ", " + this.suffix;
   }
}
 
package dustin.examples;
 
/**
 * Representation of a United States address (JavaBeans style).
 *
 * @author Dustin
 */
public final class AddressBean
{
   private StreetAddress streetAddress;
   private City city;
   private State state;
 
   /** No-arguments constructor for JavaBeans-style instantiation. */
   private AddressBean() {}
 
   public StreetAddress getStreetAddress()
   {
      return this.streetAddress;
   }
 
   public void setStreetAddress(final StreetAddress newStreetAddress)
   {
      this.streetAddress = newStreetAddress;
   }
 
   public City getCity()
   {
      return this.city;
   }
 
   public void setCity(final City newCity)
   {
      this.city = newCity;
   }
 
   public State getState()
   {
      return this.state;
   }
 
   public void setState(final State newState)
   {
      this.state = newState;
   }
 
   @Override
   public String toString()
   {
      return this.streetAddress + ", " + this.city + ", " + this.state;
   }
}

Пример реализации стиля JavaBeans и популяция

01
02
03
04
05
06
07
08
09
10
11
12
13
public PersonBean createPerson()
   {
      final PersonBean person = new PersonBean();
      final FullNameBean personName = new FullNameBean();
      personName.setFirstName(new Name("Fred"));
      personName.setLastName(new Name("Flintstone"));
      person.setName(personName);
      final AddressBean address = new AddressBean();
      address.setStreetAddress(new StreetAddress("345 Cave Stone Road"));
      address.setCity(new City("Bedrock"));
      person.setAddress(address);
      return person;
   }

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

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

Относительно распространенного использования шаблона JavaBeans в Java, несколько заслуживающих доверия авторов поставили под сомнение его ценность. Спорная статья Аллена Холуба « Почему методы получения и установки зла начинаются без преград»:


Хотя методы получения / установки являются обычным явлением в Java, они не особенно объектно-ориентированы (ОО). Фактически, они могут повредить удобству сопровождения вашего кода. Более того, наличие многочисленных методов получения и установки является красным сигналом того, что программа не обязательно хорошо разработана с точки зрения ОО.

Джош Блох в своем менее убедительном и более мягком убедительном тоне говорит о стиле получения / установки JavaBeans: «У шаблона JavaBeans есть свои серьезные недостатки» ( Effective Java , Second Edition, Item # 2 ). Именно в этом контексте Блох рекомендует вместо этого шаблон построения для построения объекта.

Я не против использования стиля get / set JavaBeans, когда среда, которую я выбрал по другим причинам, требует этого, и причины использования этой платформы оправдывают это. Существуют также области, где класс стиля JavaBeans особенно хорошо подходит, например взаимодействие с хранилищем данных и хранение данных из хранилища данных для использования приложением. Однако я не являюсь поклонником использования стиля JavaBeans для создания экземпляра вопроса просто для того, чтобы избежать необходимости передавать параметры. Я предпочитаю один из других подходов, таких как строитель для этой цели.

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

В этой статье я рассмотрел различные подходы к сокращению количества аргументов для метода или конструктора, но они также имеют один и тот же компромисс: предоставление изменяемого состояния для уменьшения или устранения количества параметров, которые должны быть переданы методу или конструктору. Преимуществами этих подходов являются простота, как правило, удобочитаемая (хотя «глобальные» могут быть трудно читаемыми) и простота первого написания и использования. Конечно, их самое большое преимущество с точки зрения этого поста состоит в том, что они обычно исключают необходимость передачи каких-либо параметров.

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

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

Вывод

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

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