🦘

Перечисления в публичном API

При использовании библиотеки Jackson для сериализации POJO в JSON перечисления вполне можно использовать в качестве поля такого POJO. Если при этом используются какие-либо генераторы openApi документации, то они автоматически подхватят это перечисление и сгенерируют корректное описание.

Если же нужно, чтобы в коде использовалось одно название перечисления, а в API другое (не капслоковая строка или даже целое число), то удобно пользоваться аннотацией @JsonValue. Ее нужно поставить над полем, в которое будет преобразовано перечисление при сериализации в JSON (и наоборот).

public enum Grade {
    EXCELLENT('A'),
    SATISFACTORY('B'),
    MEDIOCRE('C'),
    INSUFFICIENT('D'),
    FAILURE('F');

    @JsonValue
    private final char mark;
}

Перечисления в БД

Многие СУБД позволяют создавать в таблицах колонки с перечисляемым типом. Но это влечет за собой некоторые проблемы:

  • Добавление нового значения перечисления влечет за собой изменение схемы
  • Liquibase не поддерживает перечисляемые типы при описании миграции с помощью xml. Из-за этого такие миграции приходится писать на SQL, что требует написания еще и down миграций.

Зато взамен вы получаете увеличенный перфоманс при фильтрации по этой колонке и существенно сокращаете размер индекса по этой колонке.

При использовании JPA можно над полем перечисляемого типа в сущности поставить аннотацию @Enumerated.

@Enumerated(EnumType.STRING)
private Type type;

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

Получение перечисления по значению

Часто требуется получать перечисление по значению (строковому, целочисленному или др.). Для этого рекомендуется создать в перечислении приватную мапу ‘значение к перечислению’. Для перечисления ErrorCode, базирующегося на строковом значении, это будет выглядеть так:

private static final Map<String, ErrorCode> VALUE_TO_ENUM = EnumSet.allOf(ErrorCode.class).stream()
    .collect(Collectors.toMap(ErrorCode::getValue, Function.identity()));

public static ErrorCode byCount(String value) {
    return Optional.ofNullable(VALUE_TO_ENUM.get(value))
        .orElseThrow(() -> new IllegalArgumentException(String.format("Have no code for value '%s'", value)));
}

Коллекции с перечислениями

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

EnumMap - мапа, внутри которой значения укладываются в обычный массив. У этой коллекции не самый удобный API, что ограничивает ее применение.

Про использование этих коллекций хорошо написано в книге Джошуа Блоха “Effective Java”.

В библиотеке Guava есть дополнительные коллекции, упрощающие работу с перечислениями.

Как писать тесты с перечислениями

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

В JUnit для прогонки параметрического теста по значениям перечисления существует аннотация @EnumSource. К сожалению, ей становится неудобно пользоваться, когда необходимо исключить какие-то значения перечисления из теста. Неудобство заключается в том, что конкретные значения перечисления передаются в виде строк, что может грозить ошибками при рефакторинге.

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