Для упрощения буду использовать аннотации lombok'a:
@Value
@Builder
Недолго погуглив, получаем, что builder — Отделяет конструирование сложного объекта от его представления так, что в результате одного и того же процесса конструирования могут получаться разные представления. Только ли для сложных объектов?
Рассмотрим на простом примере:
@Value
public class Info {
@Nullable String uuid;
@Nullable String email;
@Nullable String phone;
}
Довольно-таки простой класс. На деле получаем иммутабельный объект, который инициализируется через конструктор.
Но, как мы видим, все поля nullable, и создание такие объектов будет выглядеть не очень красиво:
final Info info1 = new Info(null, "email@email.com", "79998888888");
final Info info2 = new Info("3d107928-d225-11ea-87d0-0242ac130003", null, null);
final Info info3 = new Info("3d107928-d225-11ea-87d0-0242ac130003 ", "email@email.com", null);
...
Безусловно, есть варианты:
- Объекты, где немного полей разных типов, можно завезти несколько конструкторов. Но это не решает проблему класса выше.
- Использовать setter'ы — субьективно, нагромождает код.
А что с билдером?
@Value
@Builder
public class Info {
@Nullable String uuid;
@Nullable String email;
@Nullable String phone;
}
Мы получаем весьма элегантное построение несложного объекта:
final Info info1 = Info.builder()
.uuid("3d107928-d225-11ea-87d0-0242ac130003")
.phone("79998888888")
.build();
final Info2 info2 = Info.builder()
.email("email@email.com")
.phone("79998888888")
.build();
...
}
Однако, для использования в проекте jackson'а необходимо дополнить наш класс, чтобы он успешно десериализовывался:
@Value
@Builder(builderClassName = "InfoBuilder")
@JsonDeserialize(builder = Info.InfoBuilder.class)
public class Info {
@Nullable String uuid;
@Nullable String email;
@Nullable String phone;
@JsonPOJOBuilder(withPrefix = "")
public static class InfoBuilder {
}
}
Получаем свои плюсы и минусы за оба подхода:
builder:
+
1. Код становится лаконичнее.
3. null в параметрах конструктора не бросается в глаза.
2. Меньше шанс перепутать параметры одного типа.
-
1. Создаем лишний объект, который GC в целом благополучно уберет, но забывать об этом не стоит.
2. При необходимости использовать jackson — нагромоздим класс.
конструктор:
+
1. Минимально нагромождает наш класс, никакой воды.
2. Нет создания лишних объектов.
-
1. Весьма часто в конструктор такого объекта будет прилетать null.
2. Есть вероятность ошибится, когда кто-то будет вносить изменения в код.
Итог
Опираясь на свой опыт — склоняюсь к использованию билдеров. Плата за это не высока, а на выходе имеем код, который приятно читать.
И конечно же, пишите тесты, чтобы избежать 2-го отрицательного пункта использования конструкторов.
P.S. Это моя первая статья, буду благодарен конструктивной критике и комментариям.
arrakisfremen
В это время в шарпе:
Как с этим дела обстоят в котлине, кто в курсе?
jreznot
Хорошо обстоят:
imanushin
Отличия:
val
, вместоvar
. Если переменная неизменяемая, тоval
(как в нашем случае)new
писать не надо. В Java классы начинаются с большой буквы, а методы — с маленькой. По этому признаку легко отличить вызов метода от вызова конструктора.;
не требуется, если это последний statement в строке.В примере выше создается immutable объект из декларации
class(val email: String, val phone: String)
.Однако, если её немного изменить на
data class(val email: String, val phone: String)
, то можно применять паттерн "прототипирование":erzi
Если не изменяет память и в java давно почти также
St_one
удачи с отловом утечек памяти потом.
erzi
Вот что интересно ничего подобного нигде об этом не слышал, почему утечки памяти, пояснить сможете?
St_one
если это объявление происходит в не статическом методе класса A(допустим), то в качестве значения info1 будет присвоен объект (не статического)анонимного класса
Не статический анонимный класс содержит ссылку на класс, в методе которого был объявлен.Таким образом метод m() вернёт объект info1 со ссылкой на объект a. Дальше можно положить info1 в какую-нибудь коллекцию, и таким образом существенно продлить жизненный цикл объекта a(вызвать утечку памяти).
erzi
Спасибо, вот не знал, т.е. получается при вызове в объемлющем методе
с инициализацией мы получаем объект анонимного класса? Странно почему так сделаноmayorovp
Потому что синтаксис
new T() { ... }
как раз и предназначен для создания анонимного класса, а вложенные{ ... }
— это всего лишь его блок инициализации (такой блок возможен в любом классе, компилятор дописывает код из этих блоков во все конструкторы).Вот что и правда странно — так это почему this всегда попадает в замыкание. Другие-то переменные попадают в замыкание только в случае их использования. Видимо, это связано с тем, что компилятор захватывает this не напрямую, а сначала преобразует анонимный класс в inner class.
anonymous
вторые {… } это же объявление статической секции, которая выполнится до конструктора класса, разве нет?
mayorovp
Ну нет, статическая секция объявляется как
static { ... }
.anonymous
косяк, вы правы… {… } чуть иначе работает
erzi
Спасибо за пояснение. Тогда уж ещё вопрос.
А как узнать, что this попал в замыкание, по файлу ...$.class?
mayorovp
Как вариант...
Bakuard
Если класс объявлен как final, то не сработает.
Firsto
dopusteam
Не совсем корректный пример. Т.к. тут либо использовать C#9 с init свойствами, либо объект получится мутабельный, хотя в примерах, если не ошибаюсь, делали иммутабельный объект