«1. Обзор

Библиотека Lombok предоставляет отличный способ упростить объекты данных. Одной из ключевых особенностей Project Lombok является аннотация @Builder, которая автоматически создает классы Builder для создания неизменяемых объектов. Однако заполнение коллекций в наших объектах может быть неуклюжим со стандартными классами Builder, сгенерированными Lombok.

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

2. Построители и коллекции

Классы построителей упрощают создание неизменяемых объектов данных благодаря их простому и беглому синтаксису. Давайте посмотрим на классы-примеры, аннотированные аннотацией Lombok @Builder:

@Getter
@Builder
public class Person {
    private final String givenName;
    private final String additionalName;
    private final String familyName;
    private final List<String> tags;
}

Теперь мы можем создавать экземпляры класса Person, используя шаблон построителя. Обратите внимание, что свойство tags — это список. Кроме того, стандартный Lombok @Builder предоставляет методы для установки этого свойства точно так же, как и для свойств, не являющихся списками:

Person person = Person.builder()
  .givenName("Aaron")
  .additionalName("A")
  .familyName("Aardvark")
  .tags(Arrays.asList("fictional","incidental"))
  .build();

Это работоспособный, но довольно неуклюжий синтаксис. Мы можем создать встроенную коллекцию, как мы сделали выше. Или мы можем объявить об этом заранее. В любом случае, это прерывает процесс создания нашего объекта. Здесь пригодится аннотация @Singular.

2.1. Использование аннотации @Singular со списками

Давайте добавим еще один список к нашему объекту Person и аннотируем его с помощью @Singular. Это даст нам параллельное представление одного поля, которое аннотировано, и другого, которое не аннотировано. Помимо общего свойства tags, мы добавим список интересов к нашему Person:

@Singular private final List<String> interests;

Теперь мы можем создать список значений по одному:

Person person = Person.builder()
  .givenName("Aaron")
  .additionalName("A")
  .familyName("Aardvark")
  .interest("history")
  .interest("sport")
  .build();

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

2.2. Работа с другими типами коллекций

Здесь мы проиллюстрировали работу @Singular с java.util.List, но его также можно применять к другим классам коллекций Java. Давайте добавим еще несколько элементов в наш Person:

@Singular private final Set<String> skills;
@Singular private final Map<String, LocalDate> awards;

Набор будет вести себя почти как список, что касается Строителей — мы можем добавлять элементы один за другим:

Person person = Person.builder()
  .givenName("Aaron")
  .skill("singing")
  .skill("dancing")
  .build();

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

Карты обрабатываются немного по-другому, а Builder предоставляет методы, которые принимают ключ и значение соответствующих типов: последнее значение, если одна и та же клавиша назначается более одного раза.

Person person = Person.builder()
  .givenName("Aaron")
  .award("Singer of the Year", LocalDate.now().minusYears(5))
  .award("Best Dancer", LocalDate.now().minusYears(2))
  .build();

3. Именование методов @Singular

До сих пор мы полагались на одну часть магии в аннотации @Singular, не привлекая к ней внимания. Сам Builder предоставляет метод для одновременного присвоения всей коллекции, использующий форму множественного числа, например, «награды». Дополнительные методы, добавленные аннотацией @Singular, используют форму единственного числа, например, «award».

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

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

Давайте построим простую модель моря, содержащую рыбу и водоросли:

Ломбок понимает слово «травы», но теряется при слове «рыба». В английском языке формы единственного и множественного числа совпадают, как ни странно. Этот код не скомпилируется, и мы получим сообщение об ошибке:

@Getter
@Builder
public class Sea {
    @Singular private final List<String> grasses;
    @Singular private final List<String> fish;
}

Мы можем разобраться, добавив значение в аннотацию, которое будет использоваться в качестве единственного имени метода:

Can't singularize this name; please specify the singular explicitly (i.e. @Singular("sheep"))

Теперь мы можем скомпилируйте наш код и используйте Builder:

@Singular("oneFish") private final List<String> fish;

«

Sea sea = Sea.builder()
  .grass("Dulse")
  .grass("Kelp")
  .oneFish("Cod")
  .oneFish("Mackerel")
  .build();

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

4. Неизменяемость

Мы видели, как аннотация @Singular помогает нам работать с коллекциями в Ломбоке. Помимо обеспечения удобства и выразительности, это также может помочь нам поддерживать чистоту нашего кода.

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

Когда наши объекты данных содержат классы Collection, легко упустить неизменяемость. Интерфейсы базовой коллекции — List, Set и Map — все имеют изменяемые и неизменяемые реализации. Если мы полагаемся на стандартный компоновщик Lombok, мы можем случайно передать изменяемую коллекцию, а затем изменить ее:

List<String> tags= new ArrayList();
tags.add("fictional");
tags.add("incidental");
Person person = Person.builder()
  .givenName("Aaron")
  .tags(tags)
  .build();
person.getTags().clear();
person.getTags().add("non-fictional");
person.getTags().add("important");

В этом простом примере нам пришлось немало потрудиться, чтобы допустить ошибку. Если бы мы использовали Arrays.asList(), например, для создания тегов переменных, мы получили бы неизменяемый список бесплатно, а вызовы add() или clear() вызвали бы исключение UnsupportedOperationException.

В реальном кодировании ошибка может возникнуть, например, если коллекция передается в качестве параметра. Однако хорошо знать, что с @Singular мы можем работать с базовыми интерфейсами Collection и получать неизменяемые экземпляры при вызове build().

5. Заключение

В этом руководстве мы увидели, как аннотация Lombok @Singular обеспечивает удобный способ работы с интерфейсами List, Set и Map с использованием шаблона Builder. Шаблон Builder поддерживает неизменяемость, и @Singular предоставляет нам для этого первоклассную поддержку.

Как обычно, полные примеры кода доступны на GitHub.