Статьи

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

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

Длинные списки параметров для методов и конструкторов имеют несколько недостатков. Большое количество параметров может быть утомительным и трудным для вызова кода для использования. Длинные списки параметров также могут привести к непреднамеренному переключению параметров в вызовах. Эти ошибки могут быть трудно найти в некоторых случаях. К счастью, большинству из них не приходится сталкиваться с другим недостатком длинных списков параметров: JVM, ограничивающей количество параметров для метода из -за ошибки времени компиляции .

Одним из подходов, который не уменьшает количество параметров для метода или конструктора, но который делает эти длинные списки параметров более читабельными и с меньшей вероятностью будет предоставлен в неправильном порядке, является использование пользовательских типов . Эти пользовательские типы могут быть реализованы как объекты передачи данных (DTO), как JavaBeans , как объекты значений , как ссылочные объекты или любой другой пользовательский тип (в Java, как правило, класс или перечисление ).

Вот надуманный пример метода, который принимает несколько параметров, многие из которых имеют тип String а многие – тип boolean .

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
/**
    * Instantiate a Person object.
    *
    * @param lastName
    * @param firstName
    * @param middleName
    * @param salutation
    * @param suffix
    * @param streetAddress
    * @param city
    * @param state
    * @param isFemale
    * @param isEmployed
    * @param isHomeOwner
    * @return
    */
   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
   }

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

Каждый из трех параметров имени может быть изменен на собственный тип Name а не String . Этот тип Name определяется следующим.

Name.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
package dustin.examples;
 
/**
 * Name representation.
 *
 * @author Dustin
 */
public final class Name
{
   private final String name;
 
   public Name(final String newName)
   {
      this.name = newName;
   }
 
   public String getName()
   {
      return this.name;
   }
 
   @Override
   public String toString()
   {
      return this.name;
   }
}

Типы приветствия и суффикса String также можно заменить пользовательскими типами, как показано в следующих двух листингах кода.

Salutation.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
package dustin.examples;
 
/**
 * Salutations for individuals' names.
 *
 * @author Dustin
 */
public enum Salutation
{
   DR,
   MADAM,
   MISS,
   MR,
   MRS,
   MS,
   SIR
}

Suffix.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
package dustin.examples;
 
/**
 * Suffix representation.
 *
 * @author Dustin
 */
public enum Suffix
{
   III,
   IV,
   JR,
   SR
}

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

Gender.java

01
02
03
04
05
06
07
08
09
10
11
12
package dustin.examples;
 
/**
 * Gender representation.
 *
 * @author Dustin
 */
public enum Gender
{
   FEMALE,
   MALE
}

EmploymentStatus.java

01
02
03
04
05
06
07
08
09
10
11
12
package dustin.examples;
 
/**
 * Representation of employment status.
 *
 * @author Dustin
 */
public enum EmploymentStatus
{
   EMPLOYED,
   NOT_EMPLOYED
}

HomeOwnerStatus.java

01
02
03
04
05
06
07
08
09
10
11
12
package dustin.examples;
 
/**
 * Representation of homeowner status.
 *
 * @author Dustin
 */
public enum HomeownerStatus
{
   HOME_OWNER,
   RENTER
}

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

StreetAddress.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
package dustin.examples;
 
/**
 * Street Address representation.
 *
 * @author Dustin
 */
public final class StreetAddress
{
   private final String address;
 
   public StreetAddress(final String newStreetAddress)
   {
      this.address = newStreetAddress;
   }
 
   public String getAddress()
   {
      return this.address;
   }
 
   @Override
   public String toString()
   {
      return this.address;
   }
}

City.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
package dustin.examples;
 
/**
 * City representation.
 *
 * @author Dustin
 */
public final class City
{
   private final String cityName;
 
   public City(final String newCityName)
   {
      this.cityName = newCityName;
   }
 
   public String getCityName()
   {
      return this.cityName;
   }
 
   @Override
   public String toString()
   {
      return this.cityName;
   }
}

State.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
package dustin.examples;
 
/**
 * Simple representation of a state in the United States.
 *
 * @author Dustin
 */
public enum State
{
   AK,
   AL,
   AR,
   AZ,
   CA,
   CO,
   CT,
   DE,
   FL,
   GA,
   HI,
   IA,
   ID,
   IL,
   IN,
   KS,
   KY,
   LA,
   MA,
   MD,
   ME,
   MI,
   MN,
   MO,
   MS,
   MT,
   NC,
   ND,
   NE,
   NH,
   NJ,
   NM,
   NV,
   NY,
   OH,
   OK,
   OR,
   PA,
   RI,
   SC,
   SD,
   TN,
   TX,
   UT,
   VA,
   VT,
   WA,
   WI,
   WV,
   WY
}

Благодаря реализации этих пользовательских типов сигнатура нашего исходного метода становится намного более читабельной и с меньшей вероятностью случайного предоставления параметров в неправильном порядке. Это показано в следующем листинге кода.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
public Person createPerson(
      final Name lastName,
      final Name firstName,
      final Name middleName,
      final Salutation salutation,
      final Suffix suffix,
      final StreetAddress address,
      final City city,
      final State state,
      final Gender gender,
      final EmploymentStatus employment,
      final HomeownerStatus homeowner)
   {
      // implementation goes here
   }

В приведенном выше листинге кода компилятор теперь будет помогать разработчику, не допуская случайного смешивания большинства предыдущих параметров String или boolean параметров. Три имени все еще являются потенциальной проблемой, так как вызывающая сторона может предоставить их не по порядку, но я мог бы написать конкретные типы (классы) для FirstName , LastName и MiddleName если бы меня это беспокоило. Вместо этого я предпочитаю использовать новый класс, который представляет полное имя и имеет все три из этих имен в качестве своих атрибутов, но этот подход будет темой будущей статьи о слишком большом количестве параметров для метода Java.

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

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

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

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

Один из наиболее часто упоминаемых недостатков подхода с пользовательским типом – это накладные расходы на дополнительные экземпляры и использование памяти. Например, класс Name требует создания экземпляра самого класса Name и его инкапсулированной String . Однако, по моему мнению, этот аргумент часто делается скорее с точки зрения преждевременной оптимизации, чем с обоснованной проблемой измеряемой производительности. Существуют ситуации, в которых дополнительные экземпляры являются слишком дорогостоящими, чтобы оправдать улучшенную читаемость и проверку во время компиляции, но во многих (возможно, в большинстве ) ситуациях дополнительные экземпляры могут предоставить дополнительные возможности с незначительным наблюдаемым воздействием. Мне особенно трудно поверить, что использование пользовательского перечисления вместо String или boolean в большинстве случаев приведет к проблемам с производительностью.

Другим аргументированным недостатком использования пользовательских типов, а не встроенных типов, является дополнительное усилие по написанию и тестированию этих пользовательских типов. Однако, как показали мои примеры в этом посте, обычно есть очень простые классы или перечисления, и их нетрудно написать или проверить. С хорошей IDE и хорошим языком сценариев, таким как Groovy, их особенно легко писать и тестировать, часто автоматически.

Вывод

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