company_banner

Строители против синтаксиса Java

Автор оригинала: Sergei Egorov
  • Перевод

Шаблон проектирования «строитель»один из самых популярных в Java.


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


Но так ли удобен этот паттерн в 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();

Что мы тут получаем:


  1. Класс User — иммутабельный, мы не можем изменить объект после создания.
  2. У его конструктора видимость в пределах пакета, и для создания экземпляра User надо обращаться к строителю.
  3. Поля Builder изменяемые, и перед созданием экземпляра User могут меняться неоднократно.
  4. Сеттеры собираются в цепочки и возвращают this (типа Builder).

Так… и в чём тут проблема?


Проблема с наследованием


Представим, что мы захотели унаследовать класс 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
    ;

Возможное решение: self typing


Один из способов решения проблемы — добавить к 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):


A a = new A<>();

Тут мы используем «сырые типы» (raw types) и diamond operator из Java. Но, как упомянуто выше, это не работает с другими языками, да и вообще в целом это хак.


Идеальное решение: Self typing в 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.


Реальное решение: подойти иначе


Но что, если вместо попыток решить проблему возвращаемого типа, мы… уберём тип вообще?


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: инициализация с двойными фигурными скобками.


RussianUser user = new RussianUser.Builder() {{
    firstName("Sergei");
    patronymic("Valeryevich");
    lastName("Egorov");
}}.build();

Тут мы используем блок инициализации, чтобы задать все поля. Любители Swing/Vaadin могут узнать этот подход ;)


Некоторым он не нравится (кстати, напишите тогда в комментариях, почему). Мне нравится. Я не стал бы использовать его там, где критична производительность, но, скажем, в случае с тестами он выглядит соответствующим всем критериям:


  1. Может быть использован с любой версией Java со времён царя Гороха.
  2. Работает с другими JVM-языками.
  3. Краткий.
  4. Нативная возможность языка, а не хак.

Заключение


Как мы увидели, хоть Java и не предлагает синтаксис для self typing, мы можем решить проблему с помощью другой возможности Java (и не портя всю малину другим JVM-языкам).


Хотя некоторые разработчики считают инициализацию с двойными фигурными скобками антипаттерном, она выглядит ценной для определённых сценариев. В конце концов, это просто синтаксический сахар для определения конструктора внутри анонимного класса.


Мне интересно, как другие люди подходят к этой проблеме и что вы думаете о компромиссах разных подходов!


P.S. Большое спасибо Ричарду Норсу и Кевину Виттеку за проверку текста.


Минутка рекламы. С прошлого года я работаю в Pivotal над Project Reactor, и на JPoint (5-6 апреля) выступлю с докладом о нём — а в дискуссионной зоне после этого можно будет зарубиться хоть о Reactor, хоть о шаблонах проектирования!
JUG.ru Group
922,00
Конференции для программистов и сочувствующих. 18+
Поделиться публикацией

Похожие публикации

Комментарии 154

    +1
    > Некоторым он не нравится (кстати, напишите тогда в комментариях, почему).

    Если родитель реализует Serializable, то будет предупреждение о том, что нет serialVersionUID.
    Нужен или @Supress, или сгенерированный/дефалтный serialVersionUID.
    Это загромождает и без того многословный код.

    UPD: ИМХО 90% кейсов, где нужен Builder, покрыли бы именованные аргументы конструкторов с дефалтными значениями (может быть когда-нибудь добавят?). По крайней мере, как у вас в примере, когда это просто заменяет сеттеры.
      +1
      Serializable

      Валидно, но Я честно говоря не видел Serializable билдеров о_О


      А с именованными аргументами есть другая проблема — наследование таких конструкторов.
      Python, например, динамический и умеет *kvargs, но в Java такое не прокатит я думаю.

        +1
        Насчет билдеров вы правы.
        Я имел в виду про использование двойных кавычек вообще.
        В частности HashMap serializable, а для ее заполнения, в принципе, трюк удобный.
          +1

          А, да, тут абсолютно согласен.


          Я там в конце статьи попытался написать "не стоит использовать везде", но такое стоит повторять — этот трюк не везде применим и может сделать больно в неправильных местах (capturing, serialization, classloading, вот это вот всё), особенно в production коде.


          А вот для Testcontainers пока что выглядит очень даже норм :) Но всё ещё думаем...

            0
            >и может сделать больно в неправильных местах
            и делает. Правда, надо сказать, что я натыкался на такое раза два, и настолько редко, что пожалуй не смогу вспомнить подробности.
              0
              В анонимным внутренним классе-наследнике HashMap как раз спотыкался на сериализации. После этого не особо жалую конструкцию {{}}. С загрузкой классов, беда миновала, даже при активном использовании RMI over SSH.

              bsideup есть вопрос по testcontainers. Коллеги сталкивались с багом testcontainers+localstack, не только мы одни используем AWS. Какие у них есть варианты по интеграции localstack?
                0

                LocalStackContainer наследуется же от GenericContainer, можно любую env variaible указать с помощью .withEnv("FOO", "BAR").

                  0

                  Можно но раз localstack берет на себя вопросы портов, хоста и прочего, имхо это должно делаться в модуле который поддержку localstack в testcontainers и добавляет.
                  Решение с ENV лишь чуть-чуть элегантнее чем переписывание очереди в полученном queueUrl

                    +1

                    мы это конечно же добавим в будущем, просто хотел поделиться быстрым workaround-ом :)

                      0
                      Спасибо за универсальный workaround!
              0
              Как раз для HashMap (если религия не позволяет Java 9 или Guava) совершенно несложно написать в своём проекте свой нормальный билдер и использовать его.
            0
            ИМХО 90% кейсов, где нужен Builder, покрыли бы именованные аргументы конструкторов с дефалтными значениями


            Это решаемо уже сегодня — вторичными конструкторами, делегирующими в this, не? С точки зрения класса это может и громозко, но с точки зрения клиента — никаких отличий от дефолтных значений.
              +4

              Не совсем решаемо — аргументы конструктора не именованны и читать вызовы конструкторов с 10 параметрами — то ещё развлечение

                0
                А, ну да. Именованности нет. Частично проблему чтения IDE решают подсвечиванием.
                  +3
                  когда на code-review смотришь такое, то подсвечивания нет
                    +1
                    Так это проблема системы code-review, требуйте фича-реквест! Хорошая система могла бы резолвить вызов конструктора и оставлять подсказки.
                      +1
                      «Без статической типизации я бы так и не узнал, зачем мне IDE вместо редактора».
                    +4

                    Кроме непосредственно чтения вызовов с 10 параметрами есть еще проблемы, которые IDE не решит даже частично.
                    Например, есть конструктор
                    C(int a, int b, int c, D d, E e)


                    Будь у нас именованные параметры и дефолтные значения — мы могли бы его вызывать, передавая только недефолтные параметры (собственно, получив поведение билдеров, только удобнее и без билдеров).
                    Без них же проблемы следующие:


                    • Количество делегирующих конструкторов под все возможные наборы аргументов растет экспоненциально. В данном случае (всего 5 параметров. Очень немного) потребовалось бы 32 конструктора. Для 10 аргументов — уже 1024. "С точки зрения класса" это капец как громоздко. Без всяких "может" :). Да, не всегда нужна возможность задавать любое подмножество параметров, некоторые параметры обязательны и т.д., но тем не менее. Собственно, ни разу и не встречал, чтобы так делали. Если какие-то делегирующие конструкторы и есть, то максимум — убирающие по одному параметру с конца. А если нужно задать нестандартные первый и последний аргументы — будь добр напихать в месте вызова null-ов в середину списка.
                    • Для некоторых наборов аргументов создать делегирующие конструкторы невозможно в принципе. Например, для примера выше не выйдет создать 3 конструктора, принимающих параметры, соответственно, (int a, int b), (int b, int c) и (int a, int c). Можно обойти фабричными методами, но по сути — такой же костыль, как и билдеры.
                      +1
                      Ок. Резонно.
              +4
              Простое решение — отказаться от наследования и использовать композицию
                0

                А можно с примерами Java кода?

                  0
                  Код вам ничего не даст, вы нарушаете принцип Лисковски (ниже написали уже). Необходимо убрать все поля, относящиеся к имени пользователя в отдельный класс Credentials и работать с ним, дополняя при необходимости. Если у вас появляется необходимость в локализации, то user должен держать у себя список всех Credentials для всех поддерживаемых локалей и доставаться по ключу при необходимости.
                    +1

                    там внизу в комментарии есть problem definition. Ваш "идеальный" код никто не станет использовать, потому что он громоздкий и неудобный (по крайней мере в той библиотеке что я описываю).


                    Поэтому я и предлагаю переубедить, с примерами кода, иначе ваши комментарии выглядят как "я прочитал книжку, вы всё делаете неправильно, а я — умный".

                      0
                      Ок, для вашего примера предлагаю подобную структуру

                      JDBCContainer extends AbstractJDBCContainer implements IJDBCContainer
                        -> config<T extends BaseJDBCConfig> 
                        -> containerStrategy<T extends JDBCContainerStrategy>
                      
                      ConcreteJDBCContainerStrategy<T extends BaseJDBCConfig> config extends AbstractJDBCContainerStrategy implements JDBCContainerStrategy
                      
                      ConcreteJDBCContainerStrategy.setConfig(T config)
                      


                      Хочу отметить, что абстрактные классы служат лишь возможности добавления хуков вокруг методов интерфейса и не реализуют никакой конкретной логики
                        +1

                        Ух ты как Java изменилась при Собянине


                        Допустим, я прочитал Ваш код. Но он не отвечает на вопрос! Как сделать удобный API для всего этого, чтобы пользователям было удобно?


                        Вас сразу выдаёт то, что вы приводите примеры объявления классов, а не пример их использования.
                        Когда мы у себя проектируем API наших DSL, мы начинаем с "как пользователь, я хочу использовать это вот так", а потом ищем варианты как это реализовать, на грани с возможностями языка, с учетом наших пользователей. А уже потом думаем, чтобы это ещё было поддерживаемо и читаемо.

                          –6
                          О, началось. Ему не удобно. И почему я не удивлен данным развитием событий? Пост оказывается не про корректную архитектуру приложения с попыткой найти оптимум, а что-бы вам вместо alt+insert надо было только точку жмакать.
                            +1
                            оказывается не про корректную архитектуру приложения

                            Внезапно то как!


                            Ему не удобно

                            Мне удобно было бы вообще не иметь public API. Нет API — нет проблем. Клаааас.

                              0
                              Так что мешает под код выше написать (сгенерировать?) fluent api?
                              MysqlDBContainer
                              .builder()
                              .withConfig(
                                  MysqlConfig.builder()
                                      .withLogin("login")
                                      .withPassword("password")
                                      .build())
                              .withStrategy(
                                  KafcaContainerStrategy.builder()
                                  .build())
                              .build();


                              Норм, не?
                                0

                                как пользователь, мне б это не понравилось, и вот почему:
                                1) API discoverability — если я хочу настроить порт, то мне надо знать наперёд в каком из билдеров этот метод для настройки порта указан
                                2) многословность — в вашем примере вы по сути дела устанавливаете 3 параметра, но при этом код нагружен .builder(), .build() и их друзьями
                                3) результат .build() должен содержать параметры всех "билдеров" (к сожалению в вашем примере это невозможно продемонстрировать), и, если это был билдер BarBuilder, то результат должен быть знать о свойствах Bar, а не только Foo

                                  –1
                                  А потому-что писать правильно это не бесплатно. В данном случае плата это +4 слова build() и builder() в коде. Печально конечно. Но еще более печально забирать на поддержку очередной написанный с нарушением Лисковски проект, переживший уже три бригады бракоделов. Приходится с грустным лицом умножать смету на 3
                                    0

                                    Самое забавное — в моём личном опыте сложней всего для поддержки проекты доставались именно от товарищей, которые пропагандируют "правильные" способы, пишут на Java как на Scala, а ночью под подушкшой читают куски Haskell кода.
                                    Так что думаю тут знаний "мудрейших" не достаточно, и надо уметь их применять там, где оно уместно, и если что-то можно сделать проще — то почему бы и нет?

                                      0
                                      Так вы делайте проще, но зачем нарушаете?
                                      +6
                                      Лисковски — это Barbara Liskov? Или какой-то новый персонаж?
                                        +2

                                        Я готов все свои карма поинты обменять на плюсы к Вашему комментарию, это просто прекрасно!

                                          –2
                                          Ок, уровень дискуссии понятен
                                            +1

                                            А что не так с уровнем дискуссии? Принцип Лисков код из статьи никак не нарушает. Если он нарушает какой-то принцип какого-то (какой-то) Лисковски, то было бы неплохо пояснить, кто это такой (такая) и что это за принцип, а то я вот тоже ничего о таком персонаже не слышал.

                                        0
                                        bsideup, справедливости ради, пункт №2 решается, если методы withStrategy, withConfig и прочие принимают аргументы типа Builder.
                                        Да, пущай они сами вызовут .build(), зато в клиентском коде этого мусора не будет. Или я чего-то упускаю?

                                        В простых случаях может быть даже такое: withStrategy(KafkaContainerStrategy::builder);
                                          0

                                          ну, не решается, скорей просто убирает часть проблемы. Но проблема всё же остаётся, особенно когда на реальных примерах её погонять (прошли через это, был одним из вариантов API)

                        +3
                        Правильное решение — перестать хаять наследование по поводу и без, и просто перестать его использовать для создания типов, которые не являются subtypeами по LSP от родителя.
                          0
                          Согласен, я поправил себя чуть ниже и уточнил про Лисковски. Его легко нарушить, что выливается в «проще вообще запретить наследоваться»
                            +2
                            LSP вообще просто нарушить, даже если тупо интерфейс имплементировать. И ничто не проверит за девелопера корректность LSP — ни компилятор, ни статический анализатор, ни даже система типов. Единственный реальный недостаток наследования по отношению к остальным инструментам sybtypingа только в том, что с ним нарушить LSP проще всего.
                              0
                              Можете привести пример как можно нарушить lsp имплементацией интерфейса? (без дефолтных методов, которые есть суть ересь)
                                +2
                                Ну например взять какой нибудь интерфейс SortedSet, сымплементировать от него класс, и допустить в имплементации какой нибудь косяк с сортировкой. В итоге, все программы, принимающие на вход SortedSet и справедливо делающие предположение о том что итерация по коллекции будет идти по определенному порядку, перестанут быть корректными после подстановки класса с косяком. Прямое нарушение LSP. Но языкам пофиг — пока синтаксически все верно, они все сбилдят и запустят.
                                  +1
                                  Суть кстати — это устаревший вариант множественного числа слова «есть»
                                  www.slovomania.ru/dnevnik/2007/01/25/sut
                                    0
                                    Спасибо, не знал
                          +2
                          ИМХО проблема на ровном месте.
                          Наследоваться вообще надо крайне осторожно, а уж наследоваться от иммутабельного класса выглядит откровенным извращением, поскольку в Java иммутабельность как свойство класса есть, а наследования иммутабельности нет. То есть заложена бомба под принцип L из набора SOLID.
                            0

                            Пример:


                            Есть проект — Testcontainers. В нём есть базовый класс GenericContainer.
                            Есть наследники типа KafkaContainer.
                            Есть даже промежуточные наследования: MySQLContainer -> JDBCContainer -> GenericContainer.


                            Должна быть возможность их конфигурировать, чтобы удобно, красиво и вот это вот всё.
                            Как бы вы решили эту проблему на ровном месте? :)

                              +1
                              А что делает этот GenericContainer и что в нем наследовать?

                              Интерфейс с дефолтной реализацией (если она нужна) — Container.
                              Любые промежуточные интерфейсы (миксины).

                              Реализации интерфейсов, которые никак друг друга не наследуют.
                              0
                              У нас в проекте такие же билдеры с наследованием как в статье. И они валидны, потому что родитель абстрактный. А в одном месте наслдеование только в билдерах, стоят они один и тот же immutable объект но с разным наполнением.
                              +2
                              Для себя проблему наследования решил выделением абстрактного билдера с дженерик аргументами. Выглядит «слегка» монстроуозно, но работает
                              Код
                              @Test public void testBuilders(){
                                  User user = new User.Builder()
                                          .firstName("Sergei")
                                          .lastName("Egorov")
                                          .build();
                                  assertEquals("Sergei", user.firstName);
                                  assertEquals("Egorov", user.lastName);
                              
                                  User userCopy = new User.Builder(user)
                                          .build();
                                  assertEquals("Sergei", userCopy.firstName);
                                  assertEquals("Egorov", userCopy.lastName);
                              
                                  RussianUser russianUser = new RussianUser.Builder()
                                          .firstName("Sergei")
                                          .patronymic("Valeryevich")
                                          .lastName("Egorov")
                                          .build();
                                  assertEquals("Sergei", russianUser.firstName);
                                  assertEquals("Valeryevich", russianUser.patronymic);
                                  assertEquals("Egorov", russianUser.lastName);
                              
                                  RussianUser russianUserCopy = new RussianUser.Builder(russianUser)
                                          .build();
                                  assertEquals("Sergei", russianUserCopy.firstName);
                                  assertEquals("Valeryevich", russianUserCopy.patronymic);
                                  assertEquals("Egorov", russianUserCopy.lastName);
                              }
                              
                              public static class User {
                              
                                  public final String firstName;
                              
                                  public final String lastName;
                              
                                  User(AbstractBuilder builder) {
                                      firstName = builder.firstName;
                                      lastName = builder.lastName;
                                  }
                              
                                  public static class Builder extends AbstractBuilder<Builder, User> {
                              
                                      public Builder() {
                                      }
                              
                                      public Builder(User item) {
                                          super(item);
                                      }
                              
                                      @Override public User build() {
                                          return new User(this);
                                      }
                                  }
                              
                                  public static abstract class AbstractBuilder<
                                          BUILDER extends AbstractBuilder,
                                          RETURN extends User> {
                              
                                      String firstName;
                                      String lastName;
                              
                                      public AbstractBuilder() {
                                      }
                              
                                      public AbstractBuilder(RETURN item) {
                                          firstName(item.firstName);
                                          lastName(item.lastName);
                                      }
                              
                                      BUILDER firstName(String value) {
                                          this.firstName = value;
                                          return getBuilder();
                                      }
                              
                                      BUILDER lastName(String value) {
                                          this.lastName = value;
                                          return getBuilder();
                                      }
                              
                                      public BUILDER getBuilder() {
                                          return (BUILDER) this;
                                      }
                              
                                      public abstract RETURN build();
                                  }
                              }
                              
                              public static class RussianUser extends User {
                                  final String patronymic;
                              
                                  RussianUser(AbstractBuilder builder) {
                                      super(builder);
                                      patronymic = builder.patronymic;
                                  }
                              
                                  public static class Builder extends AbstractBuilder<Builder, RussianUser> {
                              
                                      public Builder() {
                                      }
                              
                                      public Builder(RussianUser item) {
                                          super(item);
                                      }
                              
                                      @Override public RussianUser build() {
                                          return new RussianUser(this);
                                      }
                                  }
                              
                                  public static abstract class AbstractBuilder<
                                          BUILDER extends AbstractBuilder,
                                          RETURN extends RussianUser>
                                          extends User.AbstractBuilder<BUILDER, RETURN> {
                                      String patronymic;
                              
                                      public AbstractBuilder() {
                                      }
                              
                                      public AbstractBuilder(RETURN item) {
                                          super(item);
                                          patronymic(item.patronymic);
                                      }
                              
                                      BUILDER patronymic(String value) {
                                          this.patronymic = value;
                                          return getBuilder();
                                      }
                                  }
                              }
                              

                                +1

                                Это примерно то, что у нас сейчас (и в статье описано в секции про generic версию, только без 2х классов).
                                Такой подход имеет место быть (так например работает SuperBuilder в Lombok на сколько знаю), но, к сожалению, он очень тяжело даётся контрибьюторам в проект, особенно рекурсивные конструкции вида <SELF extends MyClass>.

                                Это, кстати, очень интересная тема. Одно дело — умные книжки про как можно и нельзя, а другое — как потом с таким кодом работать, особенно в OSS, где каждый контрибьютор важен.
                                Разработчики Testcontainers тоже не глупые и тоже разные книжки читали, но, как и во всём — лучшее враг хорошего :)

                                0
                                К сожалению в методе configure Вам пришлось отказаться от чейна, но Вы ведь так за него боролись?
                                  0

                                  Оно потому и "подойти иначе". Мы долго боролись за chaining, что забыли что есть другие способы, и что без него можно сделать вполне читаемый вариант :)

                                  0

                                  Я до сих пор удивлён что никто не упомянул трюк с .and() как это делает Spring Security:
                                  https://docs.spring.io/spring-security/site/docs/current/reference/html/jc.html#jc-httpsecurity

                                    0
                                    И как он вам?
                                      0

                                      в Spring Security норм, но им можно — у них не используется возвращаемый результат их DSL :)


                                      Но даже если и адаптировать его под этот случай — DSL становится сложней читать из-за обилия .and()

                                    0
                                    Хотелось бы добавить что в lombok добавили аннотацию @SuperBuilder
                                    которая делает все это. Но плагин для idea JetBrains пока не поддерживает но есть issues
                                      0

                                      Продублирую мой ответ из твиттера:
                                      SuperBuilder работает только если код и сторонние библиотеки к этому коду используют Java и Lombok. В нашем случае это не всегда так.

                                      +2
                                      Уже разбирал эту тему с незаслуженно забытым double brace initialization: habr.com/ru/post/261163
                                      И тут: stackoverflow.com/questions/1958636/what-is-double-brace-initialization-in-java/32404313#32404313
                                        +1

                                        Жду продолжения темы: "Дальнобольщики против оператора goto" и "Грузщики против синглтонов".

                                          0

                                          Дальнобольщики — это те, у кого боль от дальних прыжков?

                                          0

                                          А почему бы не сделать отдельный билдер для каждого юзера? Какую проблему мы тут решаем введением наследования для билдеров?

                                            0

                                            параметров может быть не 3, а 30.

                                              +1

                                              Может быть и больше. Это будет, конечно, странно, но может. Однако вопрос, какую проблему мы решаем введением наследования для билдеров по прежнему актуален.

                                            –2

                                            А почему бы не оставить User мутабельным, но через отдельный его метод получать иммутабельный интерфейс?


                                            Что-то типа:


                                            RussianUser user = new RussianUser
                                            user.firstName = "Sergei";
                                            user.patronymic ="Valeryevich";
                                            user.lastName = "Egorov";
                                            
                                            UserView userView =  RussianUser.view();
                                            userView.lastName = "Egorov"; //error
                                              +1
                                              Так в итоге так и вышло, с той только разницей что RussianUser называется RussianUser.Builder, а UserView называется RussianUser.
                                                0
                                                Не, в статье билдер описывает поля, сеттеры и передаёт это всё конктруктору, который проставляет это всё юзеру. В моём коде же нет никаких конструкторов и сеттеров, а UserView — не более чем публичный интерфейс.
                                                  0

                                                  В вашем коде объект User — изменяемый. А в коде из статьи есть гарантии, что он не изменится.

                                                    0
                                                    И как же он может вдруг измениться через иммутабельный интерфейс?
                                                      +1

                                                      Через интерфейс не изменится, но вообще измениться может, гарантий нет.

                                                    0
                                                    А в реализации этого интерфейса конструктор, такой же как и у оригинального User.
                                                      0
                                                      Сам User является реализацией интерфейса UserView. Ну или их лучше назвать UserRaw и User, ибо интерфейс чаще используется.
                                                        0
                                                        Т.е. метод view будет возвращать this. Тогда иммутабельность будет не настоящая. Состояние можно будет изменить через оригинальный user, а это не то что изначально требовалось.
                                                          0
                                                          Ну да, а можно не изменять. Какие проблемы?
                                                            +1
                                                            Изначально задача стояла так чтобы сделать чтобы было нельзя изменять.
                                                              –2
                                                              В данном случае решение о том «можно или нельзя менять» принимается там же, где и «менять или не менять», то есть при создании объекта. Так что разницы — никакой.
                                                                0

                                                                Это вам разницы никакой, а jvm разницу найдёт. Не увидит final на полях и поймёт, что никакого решения о неизменяемости никто никогда не принимал.

                                                                  0
                                                                  И что она сможет сделать с этой информацией?
                                                                    0

                                                                    С этой — ничего. А вот, если бы у неё была информация о том, что объект неизменяем, то можно было бы применить какие-нибудь оптимизации.

                                                                      0
                                                                      Так какие оптимизации?
                                                                        0

                                                                        В контексте вопроса о разнице между неизменяемым объектом и неизменяемым интерфейсом к изменяемому объекту не важно о каких конкретно оптимизациях идёт речь. Важно, что в одном случае они возможны, а в другом нет.

                                                                          0
                                                                          Как лихо вы утверждаете что возможно, а что нет, не зная сути оптимизаций.
                                                                            0

                                                                            Мне кажется я понял ваш вопрос. Вы сомневаетесь, что существуют оптимизации, которые можно сделать, когда на поле стоит final и нельзя, когда final не стоит и к нему есть сеттер?


                                                                            Так как final поля никогда не меняются, можно точно знать, что если прочитать final поле из соседнего потока, то его значение будет точно таким же, каким было в момент создания объекта. Про не final поля такого сказать нельзя.


                                                                            Конкретные оптимизации, которые jvm применяет для таких полей зависят от того, о какой jvm идёт речь. Насколько я представляю себе вопрос — в первую очередь речь идёт о кешировании. Ещё вроде некотороые сборщики мусора могут использовать эту информацию.


                                                                            Опять же, прогресс не стоит на месте и те оптимизации, которых нет сегодня — появятся завтра. Поэтому, а ещё потому, что final нужен не только jvm, но и программисту, правило правой руки — ставь final везде, где можно. А если получится, то и где нельзя.

                                                                              0
                                                                              Ява разве даёт какие-либо гарантии касательно многопоточности, чтобы было что там оптимизировать?

                                                                              Кешировать чтение из объекта она имеет полное право, ибо опять же ничего не гарантирует касательно видимости изменений из соседнего потока.

                                                                              Ок, прогресс не стоит на месте, например компилятор начинает лучше понимать код и сам расставлять final, inline, pure, nogc и прочие атрибуты.
                                                                                0
                                                                                Ява разве даёт какие-либо гарантии касательно многопоточности, чтобы было что там оптимизировать?

                                                                                Да, эти гарантии описаны в Java Memory Model.


                                                                                Кешировать чтение из объекта она имеет полное право, ибо опять же ничего не гарантирует касательно видимости изменений из соседнего потока.

                                                                                Гарантии видимости поля из соседнего потока дать можно, просто для final полей они обойдутся дешевле, посколько достаточно эти поля закешить, и не надо их каждый раз обновлять.


                                                                                например компилятор начинает лучше понимать код и сам расставлять final, inline, pure, nogc и прочие атрибуты.

                                                                                Если проставлять final, это не помешает оптимизациям, описанным вами. Но поможет тем оптимизациям, которые опираются на final. Реализовать эти последние оптимизации, кстати, проще, чем первые.

                                                                                  0
                                                                                  Да, эти гарантии описаны в Java Memory Model.

                                                                                  Там, насколько мне известно, описана семантика synchronized, который ставится программистом вручную на те поля, которые могут измениться в процессе жизни объекта. Такие поля не могут быть final по очевидным причинам. Там же, где synchronized не используется, final ничего и не даст. Аналогично и с volatile.

                                                                                    +1

                                                                                    synchronized ставится не на поля, а на методы.


                                                                                    Там же, где synchronized не используется, final ничего и не даст. Аналогично и с volatile.

                                                                                    То, что вы называете synchronized — нам самом деле volatile. Объявить поле одновременно как final и volatile нельзя. Фактически для поля final даёт те же гарантии, что volatile, но дешевле.

                                                                                      –3
                                                                                      Это замечательно, что вы знаете Яву лучше меня. В более других языках synchronized ставится на классы, а volatile нет вообще. Но давайте по существу. Зачем ставить volatile на поле, которое вы не меняете?
                                                                                        0
                                                                                        В более других языках synchronized ставится на классы, а volatile нет вообще.

                                                                                        Наверное, там synchronized обладает не той семантикой, которой обладает в джаве. И вообще конкарренси в более других языках — более другая штука. Какие языки вы имеете в виду, кстати? Это не по теме ветки, просто любопытно.


                                                                                        Но давайте по существу. Зачем ставить volatile на поле, которое вы не меняете?

                                                                                        Я видимо, не совсем ясно выразился. Нельзя поставить volatile на поле, которое не меняется, поэтому вопрос "зачем" отпадает сам по себе. volatile нельзя поставить на неизменяемое поле, потому что это бессмысленно, так как неизменяемые поля при чтении уже и так ведут себя как будто volatile на них висит. Но единственный способ сделать поле неизменяемым — повесить на него final.


                                                                                        Если final на поле нет, гарантий, что поле не изменится тоже нет.

                                                                                          0
                                                                                          dlang.org/spec/class.html#synchronized-classes

                                                                                          По остальному не буду повторяться.
                                                                                            +1

                                                                                            Хочу уточнить, что в D synchronized, как и в Java ставится на методы, а synchronized на классе просто добавляет synchronized на все методы. volatile нет, но ключевые слова, которые делают примерно то же самое есть.

                                                                                              0
                                                                                              Всё это конечно очень важные уточнения. Ну коли пошла такая пьянка, то лишь на все публичные методы.

                                                                                              О каких ключевых словах идёт речь? В D для этого используется прямое указание барьеров памяти через подключаемую библиотеку.
                                                                                                0
                                                                                                О каких ключевых словах идёт речь?

                                                                                                Прежде всего о shared. Ещё есть const и immutale, я подозреваю, что они дают эффект похожий на final в джаве.

                                                                                                  0
                                                                                                  volatile-то тут при чём?

                                                                                                  shared, const и immutable — не более чем атрибуты, используемые для проверки типов.
                                                                                                    0
                                                                                                    volatile-то тут при чём?

                                                                                                    volatile делает так, чтобы изменения, которые произошли с полем в одном потоке были видны в другом. shared делает так, чтобы изменения, сделанные с переменной в одном потоке, впринципе могли быть видны в другом. В общем, похоже на volatile. Правда, в отличии от volatile, если shared нет, то изменения в другом потоке точно не будут видны.


                                                                                                    shared, const и immutable — не более чем атрибуты, используемые для проверки типов.

                                                                                                    Нет, помимо этого они ещё указывают где хранить переменную. Делать локальную копию для каждого потока или во всех потоках использовать одну и ту же переменную.

                                                                                                      0
                                                                                                      Помимо этого они ещё указывают где хранить переменную.

                                                                                                      Только shared и только для глобальных переменных.

                                                                                                        0

                                                                                                        Ну да, только shared, но вот насчёт только для глобальных переменных я не совсем понимаю, что вы имеете в виду. Посмотрел на сайт по dlang, там в первом же попавшемся примере shared стоит на локальной переменной. Вот тут https://tour.dlang.org/tour/en/multithreading/synchronization-sharing

                                                                0
                                                                При создании объекта разницы, может быть, и никакой нет. А вот при использовании разница есть.

                                                                Либо известно, что объект гарантированно никогда не изменится независимо от того что во внешнем коде нахимичили — либо такой гарантии нет и нужно делать защитную копию.
                                                                  0
                                                                  Ява разве умеет давать гарантию глубокой иммутабельности? Если нет, то защитную копию по любому делать придётся.
                                                                    0
                                                                    Такую гарантию умеет давать программист. Нужно всего лишь объявить все поля как final и выбрать для них иммутабельные типы данных.
                                                                      0

                                                                      И чем принципиально отличается обещание программиста "я этот объект после создания уже не меняю" от "я нигде не забыл проставить final и нигде не использую мутабельные типы данных"?

                                                                        0
                                                                        Давать обещания относительно уже написанного кода проще, чем относительно ненаписанного.
                                                                          0
                                                                          «Написанность» кода ортогональна обсуждаемому вопросу.
                                                                  0
                                                                  Так не принимается же.

                                                                  RussianUser user = new RussianUser()ж
                                                                  user.firstName = "Sergei";
                                                                  user.lastName = "Egorov";
                                                                  
                                                                  UserView userView =  RussianUser.view();
                                                                  userView.getLastName(); // возвращает Egorov
                                                                  user.lastName = "Ivanov";
                                                                  userView.getLastName(); // перестало возвращть Egorov
                                                                  


                                                                  Вы конечно можете сказать что не надо переприсваивать, но хотелось бы чтобы компилятор проверял что никто не переприсвоит.
                                                                    0

                                                                    Ссылку на user этот кто-то как получит, если вы передадите ему userView?

                                                                      0

                                                                      Ссылка на user ему уже передана в объекте userView. Это ведь один и тот же объект. Осталось только скастовать его к User и можно делать всё, что захочется.

                                                                        0
                                                                        Ну если у вас параноя, то можно и в прокси спрятать.
                                                                          0

                                                                          Можно. Но по сложности это примерно то же самое, что использование билдера. Но билдер в нагрузку ещё выдаёт неизменяемый объект.

                                                                            0

                                                                            Ну конечно. В билдере надо объявить все те же свойства, прокинуть их потом в конструктор, в конструкторе присвоить полям. Прокси же в конструкторе имеет одно единственное поле и портянку делегирующих методов, которые и кодогенератор легко сделает.

                                                                              0
                                                                              Прокси же в конструкторе имеет одно единственное поле и портянку делегирующих методов, которые и кодогенератор легко сделает.

                                                                              Я же говорю, всё как в билдере один в один. Только билдер целиком делается генератором, который уже вот сейчас есть в среде разработки в поставке по умолчанию.

                                                    0

                                                    Необязательно иммутабельный, но в пропсах интерфейса логично предусмотреть обязательный мод, задающий иммутабельность текущего контента. Что-то из контента ведь может быть и мутабельным. Зачем закрывать такую возможность.

                                                      0
                                                      Разумеется иммутабельно в нём должно быть только то, что не должно меняться.
                                                        0

                                                        Вопрос можно поставить шире — как одному и тому же контексту технично задавать разные свойства. Это может и пермишн, и скрытость и мутабельность Типы ключей к удивлению не могут быть объектами.

                                                          0
                                                          Тем не менее членам класса можно устанавливать разные атрибуты, которые могут быть и объектами.
                                                    +1
                                                    Мне интересно, как другие люди подходят к этой проблеме и что вы думаете о компромиссах разных подходов!

                                                    Целый день думал, как еще можно решить пролему паттерна builder с сохранением цепочки вызовов. Получилость так:
                                                    RussianUser user = RussianUser.builder().apply(userBuilder -> userBuilder
                                                            .firstName("Sergey")
                                                            .lastName("Egorov"))
                                                            .patronymic("Valeryevich")
                                                            .build();
                                                    
                                                    // где apply это
                                                    public static class Builder extends User.Builder {
                                                    ...
                                                            // можно прокинуть логику заполнения базовой сущности
                                                            public Builder apply(Consumer<User.Builder> baseConfigure) {
                                                                baseConfigure.accept(this);
                                                                return this;
                                                            }
                                                    ...
                                                    }
                                                    


                                                    Что думаете???

                                                      0
                                                      Думаю что методы builder() и build() тут лишние.
                                                        0

                                                        Думали про такой вариант. Неплохой, но отпал т.к.
                                                        1) ухудшается API discoverabilitiy — надо знать какой из apply дёрнуть чтобы настроить firstName, вместо .builder().firstName()
                                                        2) если наследование глубже 1 класса, то вообще страшно выходит
                                                        3) лямбду нельзя на инстанс "забиндить"

                                                          0
                                                          Подход с интерфейсным программированием не рассматривали?

                                                          Идея — pojo объекты и билдеры к ним — чистые контракты, а реализация достигается за счет кодогенерации.
                                                          В примере ниже рабочий эскиз, правда вместо кодогенерации реализация через интерфейсное программирование

                                                          package code_gen;
                                                          
                                                          import java.lang.invoke.MethodHandles.Lookup;
                                                          import java.lang.reflect.Field;
                                                          import java.lang.reflect.InvocationHandler;
                                                          import java.lang.reflect.Method;
                                                          import java.lang.reflect.Proxy;
                                                          import java.util.HashMap;
                                                          import java.util.Map;
                                                          
                                                          public class JustForFun {
                                                          
                                                              public static void main(String[] args) {
                                                                  // Нюансы с конфигурацией, лямбдами не рассматриваются
                                                                  IRussianUserBuilder builder = ProxyBuilder.getBuilder(IRussianUserBuilder.class);
                                                                  builder.firstName("Sergey");
                                                                  builder.lastName("Egorov");
                                                                  builder.patronymic("Valeryevich");
                                                                  IRussianUser user = builder.build();
                                                                  System.out.println(user);               // >> {firstName=Sergey, lastName=Egorov, patronymic=Valeryevich}
                                                                  System.out.println(user.fullName());    // >> Sergey Egorov Valeryevich
                                                              }
                                                          }
                                                          
                                                          //----------------- ИДЕЯ - интерфейсное программирование ---------------------------------
                                                          
                                                          // Контракт. Реализация или кодогенерацией или java.lang.reflect.InvocationHandler (академически/для тестов)
                                                          interface IUser {
                                                          
                                                              String firstName();
                                                          
                                                              String lastName();
                                                          
                                                              /** Бизнес логика все еще возможна, но без состояния :) */
                                                              default String fullName() {
                                                                  return new StringBuilder().append(firstName()).append(" ").append(lastName()).toString();
                                                              }
                                                          }
                                                          
                                                          //Контракт. Реализация или кодогенерацией или java.lang.reflect.InvocationHandler (академически/для тестов)
                                                          interface IRussianUser extends IUser {
                                                          
                                                              String patronymic();
                                                          
                                                              /** Бизнес логика все еще возможна, но без состояния :) */
                                                              @Override
                                                              default String fullName() {
                                                                  return new StringBuilder().append(firstName()).append(" ").append(lastName()).append(" ").append(patronymic()).toString();
                                                              }
                                                          }
                                                          
                                                          //Контракт. Реализация или кодогенерацией или java.lang.reflect.InvocationHandler (академически/для тестов)
                                                          interface IUserBuilder {
                                                          
                                                              IUserBuilder firstName(String firstName);
                                                          
                                                              IUserBuilder lastName(String secondName);
                                                          
                                                              IUser build();
                                                          }
                                                          
                                                          //Контракт. Реализация или кодогенерацией или java.lang.reflect.InvocationHandler (академически/для тестов)
                                                          interface IRussianUserBuilder extends IUserBuilder {
                                                          
                                                              IRussianUserBuilder patronymic(String patronymic);
                                                          
                                                              @Override
                                                              IRussianUser build();
                                                          }
                                                          
                                                          //---------------- java.lang.reflect.InvocationHandler (для Академических целей/для тестов) ---------------------------------
                                                          
                                                          class ProxyBuilder implements InvocationHandler {
                                                          
                                                              private Map<String, Object> data = new HashMap<>();
                                                          
                                                              @Override
                                                              public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                                                                  // TODO: Обработка методов equals/hashcode и прочее
                                                                  if ("toString".equals(method.getName())) {
                                                                      return data.toString();
                                                                  }
                                                                  if ("build".equals(method.getName()) && (args == null || args.length == 0)) {
                                                                      return ProxyPojo.getPojo(method.getReturnType(), data);
                                                                  }
                                                                  // TODO: можно добавить любые методы..
                                                                  // Реализация максимум упрощена
                                                                  return data.put(method.getName(), args[0]);
                                                              }
                                                          
                                                              @SuppressWarnings("unchecked")
                                                              public static <T> T getBuilder(Class<T> builderType) {
                                                                  return (T) Proxy.newProxyInstance(builderType.getClassLoader(), new Class[] {builderType}, new ProxyBuilder());
                                                              }
                                                          }
                                                          
                                                          class ProxyPojo implements InvocationHandler {
                                                          
                                                              private final Map<String, Object> data;
                                                          
                                                              ProxyPojo(Map<String, Object> data) {
                                                                  this.data = data;
                                                              }
                                                          
                                                              @Override
                                                              public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                                                                  // TODO: Обработка методов equals/hashcode и прочее
                                                                  if (method.isDefault()) {
                                                                      return invokeDefaultMethod(proxy, method, args);
                                                                  }
                                                                  if ("toString".equals(method.getName())) {
                                                                      return data.toString();
                                                                  }
                                                                  // Реализация максимум упрощена
                                                                  return data.get(method.getName());
                                                              }
                                                          
                                                              @SuppressWarnings("unchecked")
                                                              public static <T> T getPojo(Class<T> pojoType,  Map<String, Object> data) {
                                                                  return (T) Proxy.newProxyInstance(pojoType.getClassLoader(), new Class[] {pojoType}, new ProxyPojo(data));
                                                              }
                                                          
                                                              //--------- поддержка default --------------------------------
                                                              private static final Lookup TRUSTED_LOOKUP = getLookupField();
                                                          
                                                              private static Lookup getLookupField() {
                                                                  try {
                                                                      Field lookupField = Lookup.class.getDeclaredField("IMPL_LOOKUP");
                                                                      lookupField.setAccessible(true);
                                                                      return (Lookup) lookupField.get(null);
                                                                  } catch (Exception ex) {
                                                                      throw new RuntimeException(ex);
                                                                  }
                                                              }
                                                          
                                                              private Object invokeDefaultMethod(Object proxy, Method method, Object[] args) throws Throwable {
                                                                  return TRUSTED_LOOKUP
                                                                          .in(method.getDeclaringClass())
                                                                          .unreflectSpecial(method, method.getDeclaringClass())
                                                                          .bindTo(proxy)
                                                                          .invokeWithArguments(args);
                                                              }
                                                          }
                                                          
                                                            0

                                                            Рассматривали.


                                                            С таким очень сложно работать, нет нормального способа хранить состояние (например, коллекции открытых портов, или переменных окружения), ну и не очевидный способ создания через прокси и вот это вот всё (в PR кстати чуть по-другому это решили)

                                                              0
                                                              И всё это лишь бы не переходить на Котлин…
                                                              sealed class User {
                                                                  abstract val firstName: String
                                                                  abstract val lastName: String
                                                                  
                                                                  data class RussianUser(override val firstName: String, override val lastName: String, val patronymic: String): User()
                                                              
                                                                  object SingletonUser: User() {
                                                                      override val firstName = "Name"
                                                                      override val lastName = "Last name"
                                                                  }
                                                              }
                                                              
                                                              fun main() {
                                                                 val user = User.RussianUser(lastName = "Фамилия", firstName = "Имя", patronymic = "Отчество")
                                                                 println(user)
                                                              }
                                                                –4
                                                                data class RussianUser(override val firstName: String, override val lastName: String, val patronymic: String): User()

                                                                Даже ваш великий Котлин не спасает от тонны ненужного синтаксиса здесь. Особенно когда таких полей десятки.

                                                                  0
                                                                  1) Какой тонны синтасиса?
                                                                  2) А этих полей точно нужны десятки?
                                                                    0

                                                                    1) дублирование объявлений полей
                                                                    2) Да.

                                                                      0
                                                                      Выделите все поля, которые должны быть во всех потомках, в отдельный дата-класс, и храните его. И это не тонна синтаксиса, а максимум килограмм. По сравнению с тем, на что я отвечал.
                                                                        +1
                                                                        Выделите все поля, которые должны быть во всех потомках, в отдельный дата-класс

                                                                        Это уже не "наследование".

                                                                          0

                                                                          И всё-таки, какая проблема решается с помощью наследования билдеров друг от друга?

                                                                            0

                                                                            Значительно упрощенный API благодаря этому.
                                                                            Юзеру не надо знать о всех существующих билдерах, не надо знать в каком из них настаривается foo, а в каком — bar (типичная проблема композиции).

                                                                              0

                                                                              Судя по коду из статьи, для того, чтобы получить Builder для RussianUser, нужно явным образом написать new RussianUser.Builder(). И только в этом билдере в статье есть pantonymic и об этом пользователю API надо знать.


                                                                              Если под Юзером вы имели в виду объект User, то точно такого же эффекта, как в статье можно было бы достигнуть, сгенерировав билдер для каждого объекта заново.

                                                                                0

                                                                                Надо знать только этот класс, и на нём будут все методы.


                                                                                И только в этом билдере в статье есть pantonymic

                                                                                Не совсем. Юзер просто использует $DesiredUserType.Builder и видит все поля, которые можно "настроить" с помощью билдера.
                                                                                Т.е. пользователь не ищет "а где же настроить pantonymic", он имеет тип, и видит какие параметры этого типа можно установить.

                                                                                  0
                                                                                  Т.е. пользователь не ищет "а где же настроить pantonymic", он имеет тип, и видит какие параметры этого типа можно установить.

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

                                                          0
                                                          Если уж хочется странного то что мешает сделать так:

                                                          class Base {
                                                              private final int field1;
                                                          
                                                              Base(int field1) {
                                                                  this.field1 = field1;
                                                              }
                                                          
                                                              public static class BaseBuilder extends BaseBuilderEx<BaseBuilder> {
                                                                  public Base build() {
                                                                      return new Base(field1);
                                                                  }
                                                              }
                                                          
                                                              protected static class BaseBuilderEx<T> {
                                                                  protected int field1;
                                                          
                                                                  public T field1(int field1) {
                                                                      this.field1 = field1;
                                                                      return (T) this;
                                                                  }
                                                          
                                                              }
                                                          }
                                                          
                                                          class Derived extends Base {
                                                              private final int field2;
                                                          
                                                              Derived(int field1, int field2) {
                                                                  super(field1);
                                                                  this.field2 = field2;
                                                              }
                                                          
                                                          
                                                              static class DerivedBuilder extends DerivedBuilderEx<DerivedBuilder> {
                                                                  public Derived build() {
                                                                      return new Derived(field1, field2);
                                                                  }
                                                              }
                                                          
                                                              protected static class DerivedBuilderEx<T> extends Base.BaseBuilderEx<T> {
                                                                  protected int field2;
                                                          
                                                                  public T field2(int field2) {
                                                                      this.field2 = field2;
                                                                      return (T) this;
                                                                  }
                                                              }
                                                          }
                                                          


                                                          И создавай себе инстансы

                                                          new Derived.DerivedBuilder()    
                                                              .field1(1)
                                                              .field2(1)
                                                              .build();
                                                          
                                                          new Base.BaseBuilder()
                                                              field1(1)
                                                              .build();
                                                          
                                                            +1
                                                            Ваш пример есть же в статье?
                                                              0
                                                              Не совсем. Там есть версия у которой по мнению автора есть проблемы. Мой код решает эти проблемы.
                                                            0
                                                            я остановился на след подходе:
                                                            public class Tag {
                                                            	
                                                            	public final int id;
                                                            	public final String name;
                                                            	
                                                            	private Tag(int id, String name) {
                                                            		this.id = id;
                                                            		this.name = name;
                                                            	}
                                                            	
                                                            	public static Tag create() {
                                                            		return new Tag(-1, "");
                                                            	}
                                                            	
                                                            	public Tag setId(int id) {
                                                            		return new Tag(id, name);
                                                            	}
                                                            	
                                                            	public Tag setName(String name) {
                                                            		return new Tag(id, name);
                                                            	}
                                                            
                                                            }
                                                            
                                                            Tag tag1 = Tag.create().setId(5);
                                                            tag1 = tag1.setName("Test");
                                                            

                                                            плюсы:
                                                            — значения по умолчанию определяются в одном месте
                                                            — удобно менять любое количество полей. главное не забывать что создается новый объект
                                                            — не нужен билдер
                                                            минусы:
                                                            — при изменении одного поля происходит создание объекта с «копированием» всех полей

                                                              0

                                                              Такой подход реализуется аннотацией @Wither в Lombok-е, но, к сожалению, не подходит для унаследованных сущностей.

                                                                0
                                                                тут с наследованием проблем нет. и в общем синтаксически нет так и сложно. а пользы много. я широко пользуюсь подобной конструкцией
                                                                  +1
                                                                  class MyTag extends Tag {}
                                                                  
                                                                  MyTag tag = new MyTag().setId("yo"); // <-- Error!

                                                                  Летали. Знаем.

                                                                    –2
                                                                    вот так:
                                                                    public class ColoredTag extends Tag {
                                                                    	
                                                                    	public final String color;
                                                                    	
                                                                    	private ColoredTag(int id, String name, String color) {
                                                                    		super(id, name);
                                                                    		this.color = color;
                                                                    	}
                                                                    	
                                                                    	public static ColoredTag create() {
                                                                    		return new ColoredTag(-1, "", null);
                                                                    	}
                                                                    	
                                                                    	public ColoredTag setId(int id) {
                                                                    		return new ColoredTag(id, name, color);
                                                                    	}
                                                                    	
                                                                    	public ColoredTag setName(String name) {
                                                                    		return new ColoredTag(id, name, color);
                                                                    	}
                                                                    
                                                                    	public ColoredTag setColor(String color) {
                                                                    		return new ColoredTag(id, name, color);
                                                                    	}
                                                                    
                                                                    }
                                                                    

                                                                    и никаких new
                                                                      +1
                                                                      и никаких new

                                                                      по-моему в вашем примере кол-во "new" удваивается при добавлении нового наследника ;)


                                                                      А когда пропертей десятки, то такой код начинает пугать. И ещё, у него есть один важный недостаток — при добавлении метода в базовый класс, он не попадает в наследников пока его вручную везде не добавят.

                                                                        0
                                                                        это на любителя. есть классы которые используются настолько часто что можно потратить время на их написание. один раз написал зато потом 30 раз удобно пользоваться.
                                                                        действительно при добавлении нового поля нужно N раз скопипастить строку с конструктором (она одинаковая во всех сеттерах):
                                                                        return new ColoredTag(id, name, color);
                                                                      –3
                                                                      MyTag tag = new MyTag().setId(«yo»); // < — Error!

                                                                      это действительно ошибка ибо id имеет тип int.

                                                                      как вы рассчитываете создать sub класс методом parent класса???

                                                                      в java если вы хотите работать с immutable объектами — придется немного попотеть. так как есть только один способ создания таких объектов через конструктор устанавливающий все поля. и если их 10 — значит 10. мой способ позволяет вынести эти конструкторы внутрь класса и не видеть их в основном (сложном) коде.

                                                                      а еще ваш Builder не умеет устанавливать отдельные поля.
                                                                0

                                                                Основной недостаток паттерна Builder — это то, что в отличие от конструктора, он синтаксически никак не учитывает обязательные поля, и позволяет вызвать build() до того, как все необходимые значения будут установлены. При этом даже в рантайме проверка может как делаться, так и не делаться. И для этого нет хорошего решения. Поэтому паттерн Builder полезен лишь когда у нас все поля опциональные.

                                                                  +1
                                                                  Ну, на самом деле решение-то возможно, билдер вполне может возвращать разные объекты после каждого вызова. У тех из них, где еще нельзя вызывать build, его просто не будет. Другое дело, что построить такую конструкцию достаточно сложно.
                                                                    0
                                                                    в моем подходе это можно сделать так:
                                                                    	public static Tag create(int id) {
                                                                    		return new Tag(id, "");
                                                                    	}

                                                                    id становится обязательно устанавливаемым
                                                                      0

                                                                      По-идее так и нужно делать, но уж больно смахивает на обычный конструктор. Когда полей немного проще всех запихать в конструктор (или несколько перегруженных), объявив опциональные значения как @Nullable, и вообще не заморачиваться ни с какими билдерами. Я вообще все поля делаю public final, чтобы еще и геттеры убрать. Вот в Immutables у билдеров есть очень полезная фича — это клонирование объекта с изменениями.

                                                                          0

                                                                          Все-равно необходим какой-то кодогенератор, иначе много бойлерплейта получается, особенно когда у вас сильно больше двух полей. Еще такая сильно неудобная вещь, как мутирование полей у nested-объектов:


                                                                          user = user.setContact(user.getContact().setAddress(user.getContact().getAddress().setStreet("Lenina"));

                                                                          когда хотелось бы что-то вроде:


                                                                          user = set(User.contact.address.street, "Lenina");
                                                                            0
                                                                            насчет nested immutable объектов в качестве полей согласен. без геттеров покрасивее но всеже
                                                                            user = user.setContact(user.contact.setAddress(user.contact.address.setStreet("Lenina")))
                                                                            

                                                                            с другой стороны когда нужно изменить у юзера только улицу — не представляю. изменится весь контакт или как минимум весь адрес.
                                                                            у меня в большом проекте есть несколько центральных классов для представления базы данных в памяти. и мне принципиально важна immutability. с наследованием. с большим количеством полей. но без большой nested immutable глубины. все получилось очень органично. сами классы большие, добавлять поля сложновато да — но использовать их очень удобно.
                                                                    0
                                                                    Я могу ошибаться, но в случае с наследованием от класса User почему бы просто не добавить необходимое поле в сам класс User и непосредственно в билдер, а затем вызывать сеттер только в случае необходимости?
                                                                      0

                                                                      Тогда это уже не называется "наследование"

                                                                        0
                                                                        Все верно, я и не настаивал на наследовании, я просто предложил проблему наследования решить таким способом.
                                                                          –2
                                                                          Тогда это уже не называется "наследование"

                                                                          Я, наверное, покажусь назойливым, но я ещё раз спрошу, какая проблема тут решается наследованием?

                                                                        0
                                                                        Увидел название статьи, заинтересовался, чем каменщикам и штукатурам не угодила Java, зашел почитать, а тут про паттерны оказалось.

                                                                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                                        Самое читаемое