Шаблон проектирования «строитель»один из самых популярных в 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, хоть о шаблонах проектирования!

Комментарии (140)


  1. j_wayne
    06.02.2019 13:46

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

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

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


    1. bsideup Автор
      06.02.2019 14:04
      +1

      Serializable

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


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


      1. j_wayne
        06.02.2019 14:10
        +1

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


        1. bsideup Автор
          06.02.2019 14:13
          +1

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


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


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


          1. sshikov
            06.02.2019 18:54

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


          1. igor_suhorukov
            06.02.2019 21:52

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

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


            1. bsideup Автор
              06.02.2019 23:23

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


              1. zeldigas
                07.02.2019 12:39

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


                1. bsideup Автор
                  07.02.2019 12:46
                  +1

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


        1. lany
          07.02.2019 10:37

          Как раз для HashMap (если религия не позволяет Java 9 или Guava) совершенно несложно написать в своём проекте свой нормальный билдер и использовать его.


    1. skapral
      06.02.2019 14:49

      ИМХО 90% кейсов, где нужен Builder, покрыли бы именованные аргументы конструкторов с дефалтными значениями


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


      1. bsideup Автор
        06.02.2019 14:49

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


        1. skapral
          06.02.2019 14:53

          А, ну да. Именованности нет. Частично проблему чтения IDE решают подсвечиванием.


          1. tbl
            06.02.2019 15:17
            +1

            когда на code-review смотришь такое, то подсвечивания нет


            1. lany
              07.02.2019 10:39

              Так это проблема системы code-review, требуйте фича-реквест! Хорошая система могла бы резолвить вызов конструктора и оставлять подсказки.


              1. tbl
                07.02.2019 13:09
                +1

                «Без статической типизации я бы так и не узнал, зачем мне IDE вместо редактора».


          1. Dim0v
            06.02.2019 15:23
            +2

            Кроме непосредственно чтения вызовов с 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. skapral
              06.02.2019 15:56
              +1

              Ок. Резонно.


  1. fzn7
    06.02.2019 14:15

    Простое решение — отказаться от наследования и использовать композицию


    1. bsideup Автор
      06.02.2019 14:19

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


      1. fzn7
        06.02.2019 14:36

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


        1. bsideup Автор
          06.02.2019 14:42

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


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


          1. fzn7
            06.02.2019 14:51

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

            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. bsideup Автор
              06.02.2019 15:48

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


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


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


              1. fzn7
                06.02.2019 15:58

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


                1. bsideup Автор
                  06.02.2019 16:00

                  оказывается не про корректную архитектуру приложения

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


                  Ему не удобно

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


                  1. fzn7
                    06.02.2019 16:06

                    Так что мешает под код выше написать (сгенерировать?) fluent api?

                    MysqlDBContainer
                    .builder()
                    .withConfig(
                        MysqlConfig.builder()
                            .withLogin("login")
                            .withPassword("password")
                            .build())
                    .withStrategy(
                        KafcaContainerStrategy.builder()
                        .build())
                    .build();


                    Норм, не?


                    1. bsideup Автор
                      06.02.2019 16:12

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


                      1. fzn7
                        06.02.2019 16:24

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


                        1. bsideup Автор
                          06.02.2019 16:32

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


                          1. fzn7
                            06.02.2019 16:45

                            Так вы делайте проще, но зачем нарушаете?


                        1. IvanVakhrushev
                          07.02.2019 08:29
                          +1

                          Лисковски — это Barbara Liskov? Или какой-то новый персонаж?


                          1. bsideup Автор
                            07.02.2019 12:48
                            +1

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


                          1. fzn7
                            07.02.2019 22:20
                            -1

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


                            1. Dim0v
                              08.02.2019 12:01

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


                      1. vladimirsitnikov
                        07.02.2019 13:32

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

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


                        1. bsideup Автор
                          07.02.2019 13:37

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


    1. skapral
      06.02.2019 14:55

      Правильное решение — перестать хаять наследование по поводу и без, и просто перестать его использовать для создания типов, которые не являются subtypeами по LSP от родителя.


      1. fzn7
        06.02.2019 15:00

        Согласен, я поправил себя чуть ниже и уточнил про Лисковски. Его легко нарушить, что выливается в «проще вообще запретить наследоваться»


        1. skapral
          06.02.2019 15:16
          +1

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


          1. fzn7
            06.02.2019 15:29

            Можете привести пример как можно нарушить lsp имплементацией интерфейса? (без дефолтных методов, которые есть суть ересь)


            1. skapral
              06.02.2019 15:46

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


            1. DukeKan
              06.02.2019 19:48

              Суть кстати — это устаревший вариант множественного числа слова «есть»
              www.slovomania.ru/dnevnik/2007/01/25/sut


              1. fzn7
                06.02.2019 23:39

                Спасибо, не знал


  1. nicholas_k
    06.02.2019 14:25

    ИМХО проблема на ровном месте.
    Наследоваться вообще надо крайне осторожно, а уж наследоваться от иммутабельного класса выглядит откровенным извращением, поскольку в Java иммутабельность как свойство класса есть, а наследования иммутабельности нет. То есть заложена бомба под принцип L из набора SOLID.


    1. bsideup Автор
      06.02.2019 14:32

      Пример:


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


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


      1. Kelsink
        06.02.2019 16:37

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

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

        Реализации интерфейсов, которые никак друг друга не наследуют.


    1. gnefedev
      06.02.2019 14:39

      У нас в проекте такие же билдеры с наследованием как в статье. И они валидны, потому что родитель абстрактный. А в одном месте наслдеование только в билдерах, стоят они один и тот же immutable объект но с разным наполнением.


  1. Evgenij_Popovich
    06.02.2019 14:53

    Для себя проблему наследования решил выделением абстрактного билдера с дженерик аргументами. Выглядит «слегка» монстроуозно, но работает

    Код
    @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. bsideup Автор
      06.02.2019 15:41
      +1

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

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


  1. reforms
    06.02.2019 15:41

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


    1. bsideup Автор
      06.02.2019 15:42

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


  1. bsideup Автор
    06.02.2019 15:48

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


    1. j_wayne
      06.02.2019 16:05

      И как он вам?


      1. bsideup Автор
        06.02.2019 16:07

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


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


  1. olegnyr
    06.02.2019 15:49

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


    1. bsideup Автор
      06.02.2019 15:52

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


  1. speakingfish
    06.02.2019 19:14
    +1

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


  1. alexeiz
    06.02.2019 20:33

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


    1. Cerberuser
      07.02.2019 05:26

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


  1. poxvuibr
    06.02.2019 22:12

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


    1. bsideup Автор
      06.02.2019 23:24

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


      1. poxvuibr
        07.02.2019 00:35

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


  1. vintage
    07.02.2019 07:44

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


    Что-то типа:


    RussianUser user = new RussianUser
    user.firstName = "Sergei";
    user.patronymic ="Valeryevich";
    user.lastName = "Egorov";
    
    UserView userView =  RussianUser.view();
    userView.lastName = "Egorov"; //error


    1. mayorovp
      07.02.2019 09:04

      Так в итоге так и вышло, с той только разницей что RussianUser называется RussianUser.Builder, а UserView называется RussianUser.


      1. vintage
        07.02.2019 09:51

        Не, в статье билдер описывает поля, сеттеры и передаёт это всё конктруктору, который проставляет это всё юзеру. В моём коде же нет никаких конструкторов и сеттеров, а UserView — не более чем публичный интерфейс.


        1. poxvuibr
          07.02.2019 10:14

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


          1. vintage
            07.02.2019 20:49

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


            1. poxvuibr
              07.02.2019 22:07
              +1

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


        1. a_e_tsvetkov
          07.02.2019 10:18

          А в реализации этого интерфейса конструктор, такой же как и у оригинального User.


          1. vintage
            07.02.2019 20:51

            Сам User является реализацией интерфейса UserView. Ну или их лучше назвать UserRaw и User, ибо интерфейс чаще используется.


            1. a_e_tsvetkov
              08.02.2019 06:48

              Т.е. метод view будет возвращать this. Тогда иммутабельность будет не настоящая. Состояние можно будет изменить через оригинальный user, а это не то что изначально требовалось.


              1. vintage
                08.02.2019 06:52

                Ну да, а можно не изменять. Какие проблемы?


                1. a_e_tsvetkov
                  08.02.2019 06:59
                  +1

                  Изначально задача стояла так чтобы сделать чтобы было нельзя изменять.


                  1. vintage
                    08.02.2019 08:36
                    -1

                    В данном случае решение о том «можно или нельзя менять» принимается там же, где и «менять или не менять», то есть при создании объекта. Так что разницы — никакой.


                    1. poxvuibr
                      08.02.2019 09:15

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


                      1. vintage
                        08.02.2019 09:21

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


                        1. poxvuibr
                          08.02.2019 11:52

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


                          1. vintage
                            08.02.2019 14:10

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


                            1. poxvuibr
                              08.02.2019 14:20

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


                              1. vintage
                                08.02.2019 14:27

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


                                1. poxvuibr
                                  08.02.2019 14:51

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


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


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


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


                                  1. vintage
                                    08.02.2019 16:11

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

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

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


                                    1. poxvuibr
                                      08.02.2019 16:58

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

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


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

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


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

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


                                      1. vintage
                                        08.02.2019 17:23

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

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


                                        1. poxvuibr
                                          08.02.2019 18:06
                                          +1

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


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

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


                                          1. vintage
                                            08.02.2019 19:06
                                            -1

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


                                            1. poxvuibr
                                              08.02.2019 22:13

                                              В более других языках synchronized ставится на классы, а volatile нет вообще.

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


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

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


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


                                              1. vintage
                                                08.02.2019 22:52

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

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


                                                1. poxvuibr
                                                  09.02.2019 13:42
                                                  +1

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


                                                  1. vintage
                                                    09.02.2019 14:20

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

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


                                                    1. poxvuibr
                                                      09.02.2019 14:36

                                                      О каких ключевых словах идёт речь?

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


                                                      1. vintage
                                                        09.02.2019 15:07

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

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


                                                        1. poxvuibr
                                                          09.02.2019 15:42

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

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


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

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


                                                          1. vintage
                                                            09.02.2019 17:29

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

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


                    1. mayorovp
                      08.02.2019 12:54

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

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


                      1. vintage
                        08.02.2019 14:13

                        Ява разве умеет давать гарантию глубокой иммутабельности? Если нет, то защитную копию по любому делать придётся.


                        1. mayorovp
                          08.02.2019 14:19

                          Такую гарантию умеет давать программист. Нужно всего лишь объявить все поля как final и выбрать для них иммутабельные типы данных.


                          1. vintage
                            08.02.2019 14:30

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


                            1. mayorovp
                              08.02.2019 14:50

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


                              1. vintage
                                08.02.2019 16:13

                                «Написанность» кода ортогональна обсуждаемому вопросу.


                    1. a_e_tsvetkov
                      08.02.2019 13:20

                      Так не принимается же.

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


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


                      1. vintage
                        08.02.2019 14:16

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


                        1. poxvuibr
                          08.02.2019 14:54

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


                          1. vintage
                            08.02.2019 16:14

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


                            1. poxvuibr
                              08.02.2019 17:07

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


                              1. vintage
                                08.02.2019 17:27

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


                                1. poxvuibr
                                  08.02.2019 18:07

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

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


    1. babylon
      07.02.2019 15:39

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


      1. vintage
        07.02.2019 20:53

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


        1. babylon
          08.02.2019 16:47

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


          1. vintage
            08.02.2019 16:54

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


  1. reforms
    07.02.2019 09:59
    +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;
            }
    ...
    }
    


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


    1. mayorovp
      07.02.2019 10:40

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


    1. bsideup Автор
      07.02.2019 12:56

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


      1. reforms
        07.02.2019 13:42

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

        Идея — 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);
            }
        }
        


        1. bsideup Автор
          07.02.2019 13:50

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


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


  1. a_e_tsvetkov
    07.02.2019 10:07

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

    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. reforms
      07.02.2019 10:14
      +1

      Ваш пример есть же в статье?


      1. a_e_tsvetkov
        07.02.2019 18:07

        Не совсем. Там есть версия у которой по мнению автора есть проблемы. Мой код решает эти проблемы.


  1. ovsale
    07.02.2019 10:16

    я остановился на след подходе:

    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");
    

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


    1. bsideup Автор
      07.02.2019 12:58

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


      1. ovsale
        07.02.2019 13:23

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


        1. bsideup Автор
          07.02.2019 13:33
          +1

          class MyTag extends Tag {}
          
          MyTag tag = new MyTag().setId("yo"); // <-- Error!

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


          1. ovsale
            07.02.2019 16:09
            -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. bsideup Автор
              07.02.2019 16:15
              +1

              и никаких new

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


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


              1. ovsale
                07.02.2019 17:05

                это на любителя. есть классы которые используются настолько часто что можно потратить время на их написание. один раз написал зато потом 30 раз удобно пользоваться.
                действительно при добавлении нового поля нужно N раз скопипастить строку с конструктором (она одинаковая во всех сеттерах):
                return new ColoredTag(id, name, color);


          1. ovsale
            08.02.2019 11:06
            -1

            MyTag tag = new MyTag().setId(«yo»); // < — Error!

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

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

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

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


  1. Throwable
    07.02.2019 13:41

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


    1. sshikov
      07.02.2019 21:04
      +1

      Ну, на самом деле решение-то возможно, билдер вполне может возвращать разные объекты после каждого вызова. У тех из них, где еще нельзя вызывать build, его просто не будет. Другое дело, что построить такую конструкцию достаточно сложно.


    1. ovsale
      08.02.2019 11:10

      в моем подходе это можно сделать так:

      	public static Tag create(int id) {
      		return new Tag(id, "");
      	}

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


      1. Throwable
        08.02.2019 23:06

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


        1. ovsale
          09.02.2019 11:43

          1. Throwable
            09.02.2019 13:09

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


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

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


            user = set(User.contact.address.street, "Lenina");


            1. ovsale
              09.02.2019 13:38

              насчет nested immutable объектов в качестве полей согласен. без геттеров покрасивее но всеже

              user = user.setContact(user.contact.setAddress(user.contact.address.setStreet("Lenina")))
              

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


  1. busuzima
    07.02.2019 13:59

    Я могу ошибаться, но в случае с наследованием от класса User почему бы просто не добавить необходимое поле в сам класс User и непосредственно в билдер, а затем вызывать сеттер только в случае необходимости?


    1. bsideup Автор
      07.02.2019 14:00

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


      1. busuzima
        07.02.2019 14:40

        Все верно, я и не настаивал на наследовании, я просто предложил проблему наследования решить таким способом.


      1. poxvuibr
        07.02.2019 15:33
        -2

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

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


  1. justboris
    07.02.2019 23:20

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