- PVSM.RU - https://www.pvsm.ru -
Шаблон проектирования «строитель» [1] — один из самых популярных [2] в Java.
Он простой, он помогает делать объекты неизменяемыми, и его можно генерировать инструментами вроде @Builder [3] в Project Lombok или Immutables [4].
Но так ли удобен этот паттерн в Java?
Пример этого шаблона с вызовом методов цепочкой:
public class User {
private final String firstName;
private final String lastName;
User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
String firstName;
String lastName;
Builder firstName(String value) {
this.firstName = value;
return this;
}
Builder lastName(String value) {
this.lastName = value;
return this;
}
public User build() {
return new User(firstName, lastName);
}
}
}
User.Builder builder = User.builder().firstName("Sergey").lastName("Egorov");
if (newRules) {
builder.firstName("Sergei");
}
User user = builder.build();
Что мы тут получаем:
Так… и в чём тут проблема?
Представим, что мы захотели унаследовать класс User:
public class RussianUser extends User {
final String patronymic;
RussianUser(String firstName, String lastName, String patronymic) {
super(firstName, lastName);
this.patronymic = patronymic;
}
public static RussianUser.Builder builder() {
return new RussianUser.Builder();
}
public static class Builder extends User.Builder {
String patronymic;
public Builder patronymic(String patronymic) {
this.patronymic = patronymic;
return this;
}
public RussianUser build() {
return new RussianUser(firstName, lastName, patronymic);
}
}
}
RussianUser me = RussianUser.builder()
.firstName("Sergei") // возвращает User.Builder :(
.patronymic("Valeryevich") // Метод не вызвать!
.lastName("Egorov")
.build();
Проблема возникает в связи с тем, что метод firstName определён так:
User.Builder firstName(String value) {
this.value = value;
return this;
}
И у Java-компилятора нет никакой возможности определить, что в данном случае this означает RussianUser.Builder, а не просто User.Builder!
Даже изменение порядка не поможет:
RussianUser me = RussianUser.builder()
.patronymic("Valeryevich")
.firstName("Sergei")
.lastName("Egorov")
.build() // ошибка компиляции! User нельзя присвоить RussianUser
;
Один из способов решения проблемы — добавить к User.Builder дженерик, указывающий, какой тип надо вернуть:
public static class Builder<SELF extends Builder<SELF>> {
SELF firstName(String value) {
this.firstName = value;
return (SELF) this;
}
И установить там RussianUser.Builder:
public static class Builder extends User.Builder<RussianUser.Builder> {
Теперь это работает:
RussianUser.builder()
.firstName("Sergei") // возвращает RussianUser.Builder :)
.patronymic("Valeryevich") // RussianUser.Builder
.lastName("Egorov") // RussianUser.Builder
.build(); // RussianUser
И с несколькими уровнями наследования тоже работает:
class A<SELF extends A<SELF>> {
SELF self() {
return (SELF) this;
}
}
class B<SELF extends B<SELF>> extends A<SELF> {}
class C extends B<C> {}
Так что, проблема решена? Не совсем… Теперь невозможно получить объект базового типа!
Поскольку мы используем рекурсивное определение с дженериками, у нас появилась проблема с рекурсией!
new A<A<A<A<A<A<A<...>>>>>>>()
В принципе, это можно решить (если вы не используете Kotlin [5]):
A a = new A<>();
Тут мы используем «сырые типы» (raw types) и diamond operator из Java. Но, как упомянуто выше, это не работает с другими языками, да и вообще в целом это хак.
Сразу предупрежу: этого решения не существует (по крайней мере, пока что).
Было бы здорово такое получить, но пока я не слышал о существовании JEP об этом.
P.S. Кто-нибудь знает, как заводить новые JEP? ;)
Self typing существует как языковая фича в языках вроде Swift.
Представьте следующий выдуманный Java-пример:
class A {
@Self
void withSomething() {
System.out.println("something");
}
}
class B extends A {
@Self
void withSomethingElse() {
System.out.println("something else");
}
}
new B()
.withSomething() // использует получателя вместо void
.withSomethingElse();
Как видите, проблема может быть решена на уровне компилятора.
Для этого существуют даже плагины к javac вроде аннотации Self в Manifold [6].
Но что, если вместо попыток решить проблему возвращаемого типа, мы… уберём тип вообще?
public class User {
// ...
public static class Builder {
String firstName;
String lastName;
void firstName(String value) {
this.firstName = value;
}
void lastName(String value) {
this.lastName = value;
}
public User build() {
return new User(firstName, lastName);
}
}
}
public class RussianUser extends User {
// ...
public static class Builder extends User.Builder {
String patronymic;
public void patronymic(String patronymic) {
this.patronymic = patronymic;
}
public RussianUser build() {
return new RussianUser(firstName, lastName, patronymic);
}
}
}
RussianUser.Builder b = RussianUser.builder();
b.firstName("Sergei");
b.patronymic("Valeryevich");
b.lastName("Egorov");
RussianUser user = b.build(); // RussianUser
«Это неудобно и многословно, по крайней мере, в Java» — скажете вы.
И я соглашусь, но… является ли это проблемой самого паттерна Строитель?
Помните, как я сказал, что он может быть изменяемым? Давайте тогда этим воспользуемся!
Добавим это к нашему исходному строителю:
public class User {
// ...
public static class Builder {
public Builder() {
this.configure();
}
protected void configure() {}
И используем его как анонимный объект:
RussianUser user = new RussianUser.Builder() {
@Override
protected void configure() {
firstName("Sergei"); // из User.Builder
patronymic("Valeryevich"); // из RussianUser.Builder
lastName("Egorov"); // из User.Builder
}
}.build();
Наследование перестало быть проблемой, но многословность осталась.
Тут пригодится другая «фича» Java: инициализация с двойными фигурными скобками [7].
RussianUser user = new RussianUser.Builder() {{
firstName("Sergei");
patronymic("Valeryevich");
lastName("Egorov");
}}.build();
Тут мы используем блок инициализации, чтобы задать все поля. Любители Swing/Vaadin могут узнать этот подход ;)
Некоторым он не нравится (кстати, напишите тогда в комментариях, почему). Мне нравится. Я не стал бы использовать его там, где критична производительность, но, скажем, в случае с тестами он выглядит соответствующим всем критериям:
Как мы увидели, хоть Java и не предлагает синтаксис для self typing, мы можем решить проблему с помощью другой возможности Java (и не портя всю малину другим JVM-языкам).
Хотя некоторые разработчики считают инициализацию с двойными фигурными скобками антипаттерном, она выглядит ценной для определённых сценариев. В конце концов, это просто синтаксический сахар для определения конструктора внутри анонимного класса.
Мне интересно, как другие люди подходят к этой проблеме и что вы думаете о компромиссах разных подходов!
P.S. Большое спасибо Ричарду Норсу [8] и Кевину Виттеку [9] за проверку текста.
Минутка рекламы. С прошлого года я работаю в Pivotal над Project Reactor, и на JPoint (5-6 апреля) выступлю с докладом [10] о нём — а в дискуссионной зоне после этого можно будет зарубиться хоть о Reactor, хоть о шаблонах проектирования!
Автор: bsideup
Источник [11]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/java/307871
Ссылки в тексте:
[1] «строитель»: https://ru.wikipedia.org/wiki/%D0%A1%D1%82%D1%80%D0%BE%D0%B8%D1%82%D0%B5%D0%BB%D1%8C_(%D1%88%D0%B0%D0%B1%D0%BB%D0%BE%D0%BD_%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F)
[2] один из самых популярных: https://www.quora.com/Which-are-the-important-and-widely-used-design-patterns-in-Java
[3] @Builder: https://projectlombok.org/features/Builder
[4] Immutables: https://immutables.github.io
[5] если вы не используете Kotlin: https://youtrack.jetbrains.com/issue/KT-17186
[6] Manifold: http://manifold.systems/docs.html#the-self-type
[7] инициализация с двойными фигурными скобками: http://wiki.c2.com/?DoubleBraceInitialization
[8] Ричарду Норсу: https://twitter.com/whichrich
[9] Кевину Виттеку: https://twitter.com/Kiview
[10] докладом: http://jpoint.ru/talks/7m8s8rp8df5oseq1aky6lk/?utm_source=habr&utm_medium=438866
[11] Источник: https://habr.com/ru/post/438866/?utm_campaign=438866
Нажмите здесь для печати.