Шаблон проектирования 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.