Шаблон проектирования Builder, цитирую, «отделяет конструирование сложного объекта от его представления, так что в результате одного и того же процесса конструирования могут получаться разные представления.» На практике это означает, что пользоватся builder обычно удобно, а вот реализовывать его — адов геморрой.
@Builder
В project lombok реализация Builder для какого-либо класса делается одной строкой:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | /** * Sample address entity. */ @Value @Builder(toBuilder = true) public class Address { /** * City name. */ String city; /** * Street name. */ String street; /** * Building name. */ String building; } |
@Builder делает кучу вещей: создаёт статический метод builder(); генерирует внутренний класс, реализующий Builder; генерирует код этого класса, устанавливающий фактические значения; генерирует для него метод toString(); и, наконец, генерирует метод build(), создающий ваш класс. Кроме того, поведение аннотации @Builder можно настраивать:
- Параметр builderClassName задаёт имя внутреннего класса (по умолчанию «конструируемый тип+Builder»).
- Параметр buildMethodName задаёт имя метода, создающего ваш класс (по умолчанию build())
- Параметр builderMethodName задаёт имя статического метода, возвращающего Builder (по умолчанию builder())
- Параметр toBuilder=true генерирует метод toBuilder(). Метод toBuilder() позволяет построить Builder из уже существующего класса, который будет инициализирован данными класса. По умолчанию такой метод не создаётся.
Использовать автоматически сгеренированный Builder проще простого:
1 2 3 4 5 6 7 8 9 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 | public class AddressTest { @Test public void testBuildAddress() { Address testedObject = Address .builder() .city("Dublin") .street("O'Connell Street") .building("General Post Office") .build(); assertThat(testedObject.getCity(), is("Dublin")); assertThat(testedObject.getStreet(), is("O'Connell Street")); assertThat(testedObject.getBuilding(), is("General Post Office")); System.out.println(testedObject.toString()); } @Test public void testToBuilder() { Address sourceObject = Address .builder() .city("Dublin") .street("O'Connell Street") .building("General Post Office") .build(); Address testedObject = sourceObject.toBuilder() .building("Belvedere House") .build(); assertThat(testedObject.getCity(), is("Dublin")); assertThat(testedObject.getStreet(), is("O'Connell Street")); assertThat(testedObject.getBuilding(), is("Belvedere House")); System.out.println(testedObject.toString()); } } |
1 2 | Address(city=Dublin, street=O'Connell Street, building=General Post Office) Address(city=Dublin, street=O'Connell Street, building=Belvedere House) |
@Singular
Аннотация @Singular упрощает строительство коллекций. Если поле аннотировано @Singular, для него будет сформировано два метода добавления в коллекцию: один метод принимает единственный элемент будущей коллекции и добавляет его к ней, второй принимает коллекцию и добавляет её к будущей коллекции. Методов для замены будущей коллекции не будет сгенерировано.
@Singular работает только с некоторыми типами коллекций, как то:- Iterable, Collection, List
- Set, SortedSet, NavigableSet
- Map, SortedMap, NavigableMap
- ImmutableCollection, ImmutableList
- ImmutableSet, ImmutableSortedSet
- ImmutableMap, ImmutableBiMap, ImmutableSortedMap
Кроме того, аннотация @Singular анализирует имя переменной и пытается его перевести из формы множественного числа, в форму единственного числа по правилам английского языка. Например, если коллекция называется adresses, то будет сгенерировано два метода: address для одного аргумента и addresses для коллекции. В случае, если lombok не сможет перевести имя переменной из множественного числа в единственное, необходимо будет задать имя метода для одного аргумента явно в параметре аннотации.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | /** * Sample person entity. */ @Data @FieldDefaults(level = AccessLevel.PRIVATE) @Builder public class Person { /** * Some id; */ @NonNull Integer id; /** * Person name */ String name; /** * Person's addresses. */ @Singular List<Address> addresses; } |
1 2 3 4 5 6 7 8 9 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 | public class PersonTest { @Test(expected = NullPointerException.class) public void testCantNull() { Person testedObject = Person .builder() .name("Test von Testoff") .build(); } @Test public void testTwoAddresses() { val addr1 = Address .builder() .city("Dublin") .street("O'Connell Street") .building("General Post Office") .build(); val addr2 = addr1.toBuilder() .building("Belvedere House") .build(); Person testedObject = Person .builder() .id(1) .name("Test von Testoff") .address(addr1) .address(addr2) .build(); assertThat(testedObject.getAddresses().size(), is(2)); System.out.println(testedObject.toString()); } } |
1 | Person(id=1, name=Test von Testoff, addresses=[Address(city=Dublin, street=O'Connell Street, building=General Post Office), Address(city=Dublin, street=O'Connell Street, building=Belvedere House)]) |
Как видно из примера, @Builder не позволяет создавать объекты с незаполненными @NonNull полями.
В этот же тесте я использова другую функциональность lombok: ключевое слово val. val как бы говорит компилятору: «Эй, ты там ведь сам всё равно знаешь, какой-там тип у данных, так не выноси мне мозг и сделай переменную именно того типа, который нужен». Другими словами, val позволяет определить локальную переменную не указывая её типа, в случае, если она сразу будет проинициализирована. Причём переменная будет объявлена final. val нельзя использовать для определения полей класса, параметров функций итд.
Код примера доступен на github.