Статьи

Java Enums: у вас есть грация, элегантность и сила, и это то, что я люблю

Пока идет Java 8, вы уверены, что хорошо знаете перечисления, представленные в Java 5? Перечисления Java по-прежнему недооцениваются, и жаль, поскольку они более полезны, чем вы думаете, они не только для ваших обычных перечисляемых констант!

Перечисление Java полиморфно

Перечисления Java — это реальные классы, которые могут иметь поведение и даже данные.

Давайте представим игру Rock-Paper-Scissors, используя перечисление одним методом. Вот модульные тесты для определения поведения:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
@Test
public void paper_beats_rock() {
 assertThat(PAPER.beats(ROCK)).isTrue();
 assertThat(ROCK.beats(PAPER)).isFalse();
}
@Test
public void scissors_beats_paper() {
 assertThat(SCISSORS.beats(PAPER)).isTrue();
 assertThat(PAPER.beats(SCISSORS)).isFalse();
}
@Test
public void rock_beats_scissors() {
 assertThat(ROCK.beats(SCISSORS)).isTrue();
 assertThat(SCISSORS.beats(ROCK)).isFalse();
}

А вот реализация перечисления, которая в первую очередь опирается на порядковое целое число каждой константы перечисления, например, элемент N + 1 выигрывает над элементом N. Эта эквивалентность между константами перечисления и целыми числами довольно удобна во многих случаях.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
/** Enums have behavior! */
public enum Gesture {
 ROCK() {
  // Enums are polymorphic, that's really handy!
  @Override
  public boolean beats(Gesture other) {
   return other == SCISSORS;
  }
 },
 PAPER, SCISSORS;
 
 // we can implement with the integer representation
 public boolean beats(Gesture other) {
  return ordinal() - other.ordinal() == 1;
 }
}

Обратите внимание, что нигде нет ни одного оператора IF, вся бизнес-логика обрабатывается целочисленной логикой и полиморфизмом, где мы переопределяем метод для случая ROCK. Если бы упорядочение между элементами не было циклическим, мы могли бы реализовать его, просто используя естественное упорядочение перечисления, здесь полиморфизм помогает справиться с циклом.

Вы можете сделать это без какого-либо заявления IF! Да, ты можешь!

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

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

Если вы ищете Value Objects и можете представить часть своего домена с ограниченным набором экземпляров, тогда enum — это то, что вам нужно! Это немного похоже на класс Sealed Case Class в Scala , за исключением того, что он полностью ограничен набором экземпляров, определенных во время компиляции. Ограниченный набор экземпляров во время компиляции является реальным ограничением, но теперь с непрерывной доставкой , вы можете, вероятно, дождаться следующего выпуска, если вам действительно нужен один дополнительный случай.  

Хорошо подходит для модели стратегии

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

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
/** The policy on how to notify the user of any Eurovision song contest event */
public enum EurovisionNotification {
 
 /** I love Eurovision, don't want to miss it, never! */
 ALWAYS() {
  @Override
  public boolean mustNotify(String eventCity, String userCity) {
   return true;
  }
 },
 
 /**
  * I only want to know about Eurovision if it takes place in my city, so
  * that I can take holidays elsewhere at the same time
  */
 ONLY_IF_IN_MY_CITY() {
  // a case of flyweight pattern since we pass all the extrinsi data as
  // arguments instead of storing them as member data
  @Override
  public boolean mustNotify(String eventCity, String userCity) {
   return eventCity.equalsIgnoreCase(userCity);
  }
 },
 
 /** I don't care, I don't want to know */
 NEVER() {
  @Override
  public boolean mustNotify(String eventCity, String userCity) {
   return false;
  }
 };
 
 // no default behavior
 public abstract boolean mustNotify(String eventCity, String userCity);
 
}

И юнит-тест для нетривиального случая ONLY_IF_IN_MY_CITY:

1
2
3
4
5
@Test
public void notify_users_in_Baku_only() {
 assertThat(ONLY_IF_IN_MY_CITY.mustNotify("Baku", "BAKU")).isTrue();
 assertThat(ONLY_IF_IN_MY_CITY.mustNotify("Baku", Paris")).isFalse();
}

Здесь мы определяем метод abstract , только чтобы реализовать его для каждого случая. Альтернативой может быть реализация поведения по умолчанию и переопределение его только для каждого случая, когда это имеет смысл, как в игре «Рок-бумага-ножницы».

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

Для стратегии Евровидения, используя TDD, мы могли бы начать с простого логического значения для случаев ВСЕГДА и НИКОГДА. Затем он будет переведен в перечисление, как только мы представим третью стратегию ONLY_IF_IN_MY_CITY. Продвижение примитивов также в духе 7-го правила « Обернуть все примитивы » из Object Calisthenics , и перечисление — это идеальный способ обернуть булево или целое число ограниченным набором возможных значений.

Поскольку шаблон стратегии часто контролируется конфигурацией, встроенная сериализация в и из String также очень удобна для хранения ваших настроек.  

Идеально подходит для Государственного образца

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

Ребенок как конечный автомат (фото с www.alongcamebaby.ca)

Давайте возьмем пример ребенка, упрощенного как конечный автомат, и сделаем его перечислением:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
/**
 * The primary baby states (simplified)
 */
public enum BabyState {
 
 POOP(null), SLEEP(POOP), EAT(SLEEP), CRY(EAT);
 
 private final BabyState next;
 
 private BabyState(BabyState next) {
  this.next = next;
 }
 
 public BabyState next(boolean discomfort) {
  if (discomfort) {
   return CRY;
  }
  return next == null ? EAT : next;
 }
}

И, конечно же, некоторые юнит-тесты для определения поведения:

01
02
03
04
05
06
07
08
09
10
11
12
@Test
public void eat_then_sleep_then_poop_and_repeat() {
 assertThat(EAT.next(NO_DISCOMFORT)).isEqualTo(SLEEP);
 assertThat(SLEEP.next(NO_DISCOMFORT)).isEqualTo(POOP);
 assertThat(POOP.next(NO_DISCOMFORT)).isEqualTo(EAT);
}
 
@Test
public void if_discomfort_then_cry_then_eat() {
 assertThat(SLEEP.next(DISCOMFORT)).isEqualTo(CRY);
 assertThat(CRY.next(NO_DISCOMFORT)).isEqualTo(EAT);
}

Да, мы можем ссылаться на константы перечисления между ними с тем ограничением, на которое могут ссылаться только константы, определенные ранее. Здесь у нас есть цикл между состояниями EAT -> SLEEP -> POOP -> EAT и т. Д., Поэтому нам нужно открыть цикл и закрыть его с помощью обходного пути во время выполнения.

У нас действительно есть граф с состоянием CRY, к которому можно получить доступ из любого состояния.

Я уже использовал перечисления для представления простых деревьев по категориям, просто ссылаясь в каждом узле на его элементы, все с константами перечисления.  

Enum-оптимизированные коллекции

Enums также имеет преимущества, связанные с их специальными реализациями для Map и Set: EnumMap и EnumSet .

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

Чтобы проиллюстрировать использование этих специализированных коллекций, давайте представим 7 карточек в Jurgen Appelo’s Delegation Poker :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
public enum AuthorityLevel {
 
 /** make decision as the manager */
 TELL,
 
 /** convince people about decision */
 SELL,
 
 /** get input from team before decision */
 CONSULT,
 
 /** make decision together with team */
 AGREE,
 
 /** influence decision made by the team */
 ADVISE,
 
 /** ask feedback after decision by team */
 INQUIRE,
 
 /** no influence, let team work it out */
 DELEGATE;

Есть 7 карт, первые 3 более ориентированы на контроль, средняя карта сбалансирована, а 3 последние карты более ориентированы на делегирование (я объяснил эту интерпретацию, пожалуйста, обратитесь к его книге за разъяснениями). В Delegation Poker каждый игрок выбирает карту для данной ситуации и зарабатывает столько же очков, сколько и стоимость карты (от 1 до 7), за исключением игроков из «высшего меньшинства».

Тривиально вычислить количество точек, используя порядковое значение + 1. Также легко выбрать ориентированные на контроль карты по их порядковому значению, или мы можем использовать Набор, построенный из диапазона, как мы делаем ниже, чтобы выбрать ориентированный на делегирование открытки:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
public int numberOfPoints() {
  return ordinal() + 1;
 }
 
 // It's ok to use the internal ordinal integer for the implementation
 public boolean isControlOriented() {
  return ordinal() < AGREE.ordinal();
 }
 
 // EnumSet is a Set implementation that benefits from the integer-like
 // nature of the enums
 public static Set DELEGATION_LEVELS = EnumSet.range(ADVISE, DELEGATE);
 
 // enums are comparable hence the usual benefits
 public static AuthorityLevel highest(List levels) {
  return Collections.max(levels);
 }
}

EnumSet предлагает удобные статические фабричные методы, такие как range (от, до), для создания набора, который включает каждую константу перечисления, начинающуюся между ADVISE и DELEGATE в нашем примере, в порядке объявления.

Чтобы вычислить наибольшее меньшинство, мы начинаем с самой высокой карты, которая является ничем иным, как поиском максимума, что-то тривиальное, поскольку перечисление всегда сопоставимо.

Всякий раз, когда нам нужно использовать это перечисление в качестве ключа на карте, мы должны использовать EnumMap, как показано в тесте ниже:

1
2
3
4
5
6
7
8
9
// Using an EnumMap to represent the votes by authority level
@Test
public void votes_with_a_clear_majority() {
 final Map<AuthorityLevel, Integer> votes = new EnumMap(AuthorityLevel.class);
 votes.put(SELL, 1);
 votes.put(ADVISE, 3);
 votes.put(INQUIRE, 2);
 assertThat(votes.get(ADVISE)).isEqualTo(3);
}

Перечисления Java хороши, ешьте их!

Я люблю перечисления Java: они просто идеальны для объектов-значений в смысле доменно-управляемого проектирования, где множество всевозможных значений ограничено. В недавнем проекте мне сознательно удалось получить большинство типов значений, выраженных в виде перечислений. Вы получаете много удивительного бесплатно, и особенно практически без технического шума. Это помогает улучшить отношение сигнал / шум между словами из области и техническим жаргоном.

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

(фотография с sys-con.com — статья Джима Барнаби) »]
Сила полиморфизма

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

Мне бы очень хотелось, чтобы перечисление Java завершалось аналогичной конструкцией, аналогично классу case в Scala, когда набор возможных значений не может быть ограничен. И способ обеспечить неизменность любого класса тоже был бы хорош. Я слишком много спрашиваю?

Также <troll> даже не пытайтесь сравнивать перечисление Java с перечислением C #… </ troll>

Ссылка: Java Enums: у вас есть грация, элегантность и сила, и это то, что я люблю! от нашего партнера JCG