Элегантный Builder на Java

    Наверняка большинство сколько-нибудь опытных программистов знакомы с паттерном Builder. Он позволяет сделать инициализацию структур данных более наглядной, гибкой при этом сохраняя такое полезное их свойство как неизменяемость (immutability). Вот классический пример с первой страницы выдачи гугла на запрос «java builder pattern example». При всех своих преимуществах, самый главный недостаток данной реализации паттерна — в два раза больше кода, по сравнению с обычным плоским бином. Если генерация этого дополнительного кода не проблема для любой популярной IDE, то редактировать такой класс становится достаточно утомительно и читабельность страдает в любом случае.

    В какой-то момент я решил, что хватит это терпеть и занялся поисками альтернативы. Надо сказать, альтернатива нашлась достаточно быстро. В Java есть редко использующийся механизм нестатических внутренних классов. Экземпляр такого класса можно создать только через экземпляр класса-родителя с помощью оператора .new. Что важно, такой объект имеет доступ к приватным полям своего родителя.

    Итак, у нас есть неизменяемая структура

    public class Account {
    
        private final String userId;
        private final String token;
    
        public Account(String token, String userId) {
            this.token = token;
            this.userId = userId;
        }
    
        public String getUserId() {
            return userId;
        }
    
        public String getToken() {
            return token;
        }
    
    }
    

    Здесь сейчас всего два поля, но билдер все равно был бы полезен чтобы не путать порядок параметров в конструкторе и если понадобится проинициализировать только одно поле из двух или оба, но в разные моменты времени. Что говорить, когда полей станет 20!

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

    public class Account {
    
        private String userId;
        private String token;
    
        private Account() {
            // private constructor
        }
    
        public String getUserId() {
            return userId;
        }
    
        public String getToken() {
            return token;
        }
    
        public class Builder {
    
            private Builder() {
                // private constructor
            }
    
            public Builder setUserId(String userId) {
                Account.this.userId = userId;
                
                return this;
            }
    
            public Builder setToken(String token) {
                Account.this.token = token;
                
                return this;
            }
            
            public Account build() {
                return Account.this;
            }
    
        }
    
    }
    

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

    Последний штрих — добавляем в метод для создания экземпляра билдера.

    public class Account {
    
        private String userId;
        private String token;
    
        private Account() {
            // private constructor
        }
    
        public String getUserId() {
            return userId;
        }
    
        public String getToken() {
            return token;
        }
    
        public static Builder newBuilder() {
            return new Account().new Builder();
        }
    
        public class Builder {
    
            private Builder() {
                // private constructor
            }
    
            public Builder setUserId(String userId) {
                Account.this.userId = userId;
    
                return this;
            }
    
            public Builder setToken(String token) {
                Account.this.token = token;
    
                return this;
            }
    
            public Account build() {
                return Account.this;
            }
    
        }
    
    }
    

    Сравните с традиционной реализацией:

    public class Account {
    
        private final String userId;
        private final String token;
    
        public Account(String userId, String token) {
            this.userId = userId;
            this.token = token;
        }
    
        public String getUserId() {
            return userId;
        }
    
        public String getToken() {
            return token;
        }
    
        public static class Builder {
    
            private String userId;
            private String token;
    
            public Builder setUserId(String userId) {
                this.userId = userId;
                
                return this;
            }
    
            public Builder setToken(String token) {
                this.token = token;
                
                return this;
            }
            
            public Account build() {
                return new Account(userId, token);
            }
            
        }
    
    }
    

    Попробуйте добавить новое поле или изменить тип поля token в том и другом случае. С увеличением количества полей разница в объеме кода и читабельности будет все более заметной. Сравним пример из статьи, на которую я ссылался в начале топика (я его изменил, чтобы стили примеров совпадали):

    public class Person {
        
        private final String lastName;
        private final String firstName;
        private final String middleName;
        private final String salutation;
        private final String suffix;
        private final String streetAddress;
        private final String city;
        private final String state;
        private final boolean isFemale;
        private final boolean isEmployed;
        private final boolean isHomeOwner;
    
        public Person(
                final String newLastName,
                final String newFirstName,
                final String newMiddleName,
                final String newSalutation,
                final String newSuffix,
                final String newStreetAddress,
                final String newCity,
                final String newState,
                final boolean newIsFemale,
                final boolean newIsEmployed,
                final boolean newIsHomeOwner) {
            
            this.lastName = newLastName;
            this.firstName = newFirstName;
            this.middleName = newMiddleName;
            this.salutation = newSalutation;
            this.suffix = newSuffix;
            this.streetAddress = newStreetAddress;
            this.city = newCity;
            this.state = newState;
            this.isFemale = newIsFemale;
            this.isEmployed = newIsEmployed;
            this.isHomeOwner = newIsHomeOwner;
        }
    
        public String getLastName() {
            return lastName;
        }
    
        public String getFirstName() {
            return firstName;
        }
    
        public String getMiddleName() {
            return middleName;
        }
    
        public String getSalutation() {
            return salutation;
        }
    
        public String getSuffix() {
            return suffix;
        }
    
        public String getStreetAddress() {
            return streetAddress;
        }
    
        public String getCity() {
            return city;
        }
    
        public String getState() {
            return state;
        }
    
        public boolean isFemale() {
            return isFemale;
        }
    
        public boolean isEmployed() {
            return isEmployed;
        }
    
        public boolean isHomeOwner() {
            return isHomeOwner;
        }
    
        public static class Builder {
            
            private String nestedLastName;
            private String nestedFirstName;
            private String nestedMiddleName;
            private String nestedSalutation;
            private String nestedSuffix;
            private String nestedStreetAddress;
            private String nestedCity;
            private String nestedState;
            private boolean nestedIsFemale;
            private boolean nestedIsEmployed;
            private boolean nestedIsHomeOwner;
    
            public Builder setNestedLastName(String nestedLastName) {
                this.nestedLastName = nestedLastName;
                
                return this;
            }
    
            public Builder setNestedFirstName(String nestedFirstName) {
                this.nestedFirstName = nestedFirstName;
    
                return this;
            }
    
            public Builder setNestedMiddleName(String nestedMiddleName) {
                this.nestedMiddleName = nestedMiddleName;
    
                return this;
            }
    
            public Builder setNestedSalutation(String nestedSalutation) {
                this.nestedSalutation = nestedSalutation;
                
                return this;
            }
    
            public Builder setNestedSuffix(String nestedSuffix) {
                this.nestedSuffix = nestedSuffix;
    
                return this;
            }
    
            public Builder setNestedStreetAddress(String nestedStreetAddress) {
                this.nestedStreetAddress = nestedStreetAddress;
    
                return this;
            }
    
            public Builder setNestedCity(String nestedCity) {
                this.nestedCity = nestedCity;
    
                return this;
            }
    
            public Builder setNestedState(String nestedState) {
                this.nestedState = nestedState;
    
                return this;
            }
    
            public Builder setNestedIsFemale(boolean nestedIsFemale) {
                this.nestedIsFemale = nestedIsFemale;
    
                return this;
            }
    
            public Builder setNestedIsEmployed(boolean nestedIsEmployed) {
                this.nestedIsEmployed = nestedIsEmployed;
    
                return this;
            }
    
            public Builder setNestedIsHomeOwner(boolean nestedIsHomeOwner) {
                this.nestedIsHomeOwner = nestedIsHomeOwner;
    
                return this;
            }
    
            public Person build() {
                return new Person(
                        nestedLastName, nestedFirstName, nestedMiddleName,
                        nestedSalutation, nestedSuffix,
                        nestedStreetAddress, nestedCity, nestedState,
                        nestedIsFemale, nestedIsEmployed, nestedIsHomeOwner);
            }
    
        }
    
    }
    

    И реализация через внутренний класс:

    public class Person {
    
        private String lastName;
        private String firstName;
        private String middleName;
        private String salutation;
        private String suffix;
        private String streetAddress;
        private String city;
        private String state;
        private boolean isFemale;
        private boolean isEmployed;
        private boolean isHomeOwner;
    
        private Person() {
            // private constructor
        }
    
        public String getLastName() {
            return lastName;
        }
    
        public String getFirstName() {
            return firstName;
        }
    
        public String getMiddleName() {
            return middleName;
        }
    
        public String getSalutation() {
            return salutation;
        }
    
        public String getSuffix() {
            return suffix;
        }
    
        public String getStreetAddress() {
            return streetAddress;
        }
    
        public String getCity() {
            return city;
        }
    
        public String getState() {
            return state;
        }
    
        public boolean isFemale() {
            return isFemale;
        }
    
        public boolean isEmployed() {
            return isEmployed;
        }
    
        public boolean isHomeOwner() {
            return isHomeOwner;
        }
        
        public static Builder newBuilder() {
            return new Person().new Builder();
        }
    
        public class Builder {
    
            private Builder() {
                // private constructor
            }
    
            public Builder setLastName(String lastName) {
                Person.this.lastName = lastName;
                
                return this;
            }
    
            public Builder setFirstName(String firstName) {
                Person.this.firstName = firstName;
    
                return this;
            }
    
            public Builder setMiddleName(String middleName) {
                Person.this.middleName = middleName;
    
                return this;
            }
    
            public Builder setSalutation(String salutation) {
                Person.this.salutation = salutation;
    
                return this;
            }
    
            public Builder setSuffix(String suffix) {
                Person.this.suffix = suffix;
    
                return this;
            }
    
            public Builder setStreetAddress(String streetAddress) {
                Person.this.streetAddress = streetAddress;
    
                return this;
            }
    
            public Builder setCity(String city) {
                Person.this.city = city;
    
                return this;
            }
    
            public Builder setState(String state) {
                Person.this.state = state;
    
                return this;
            }
    
            public Builder setFemale(boolean isFemale) {
                Person.this.isFemale = isFemale;
    
                return this;
            }
    
            public Builder setEmployed(boolean isEmployed) {
                Person.this.isEmployed = isEmployed;
    
                return this;
            }
    
            public Builder setHomeOwner(boolean isHomeOwner) {
                Person.this.isHomeOwner = isHomeOwner;
    
                return this;
            }
    
            public Person build() {
                return Person.this;
            }
    
        }
    
    }
    

    Можно заметить, что с точки зрения организации кода такой класс отличается от обычного плоского бина с полями и гетерами-сетерами только тем, что сеттеры сгруппированы в отдельном внутреннем классе, добавились только пара методов newBuilder() и build(), строчка с объявлением внутреннего класса и приватные конструкторы.

    Важные замечания:

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

    public Account build() {
        Account account = new Account();
        account.userId = Account.this.userId;
        account.token = Account.this.token;
    
        return account;
    }
    

    При этом возвращается часть дублирующего кода, от которого мы пытались избавиться. Обычно ссылка на билдер не покидает метод, поэтому я предпочитаю вариант, который показал сначала. Если вы часто передаете билдер туда-сюда и переиспользуете его для повторной генерации объектов, используйте вариант, как показано выше.

    2. Спасибо комментаторам, мои сомнения развеяны — объект, получаемый из такого билдера, не потокобезопасен из-за того, что поля в нем не объявлены как final. Если в вашем приложении важен этот момент, лучше использовать классический билдер.

    И напоследок — использование билдера.

    Account account = Account.newBuilder()
                        .setToken("hello")
                        .setUserId("habr")
                        .build();
    

    Ну или

    Account.Builder accountBuilder = Account.newBuilder();
    ...
    accountBuilder.setToken("hello");
    ...
    accountBuilder..setUserId("habr");
    
    return accountBuilder.build();
    

    Опять же Account.newBuilder() моим программерским глазам милее, чем new Account.Builder(), хотя это уже дело вкуса.

    Всем чистого кода!

    UPD: Как часто бывает на хабре, комментарии оказались полезнее самого топика, рекомендуются к ознакомлению.
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 86

      +2
      Внутренние классы используются часто.

      Вместо
      public Account build() {
          Account account = new Account();
          account.userId = Account.this.userId;
          account.token = Account.this.token;
      
          return account;
      }
      

      можно реализовать Cloneable. Это избавляет от вероятных ошибок при добавлении полей. И на 20 полях будет разница в объеме в пользу Cloneable.
        0
        Про Cloneable хорошее замечание.

        Можете привести примеры, где используются нестатические неанонимные внутренние классы? По крайней мере в тех проектах, где я успел поучаствовать за почти 10 лет работы не то что частого, вообще хоть какого-то их использования я не видел.
          0
          Очень любопытный опыт. У меня в каждом первом классе есть внутренние.
          0
          Разве в clone вам не нужно будет проделать по факту тоже самое? Или вы хотите делегировать вызов в super.clone()?
            0
            Конечно, хочу
              0
              Это не всегда сработает. К примеру если есть mutable поля. Придется все-равно их ручками прописывать. Так что clone не предотвращает от появления ошибок в ходи копирования объекта.
                0
                Вы имеете ввиду, что копия будет не «глубокая»? Ну так и в исходном варианте это надо учитывать, не забывать. А еще делать копии в сеттере билдера. А еще делать defensive копии при возвращении из геттера…
                  0
                  Именно. Согласен полностью.
          +1
          При таком подходе теряется важное свойство неизменяемости. К тому же после build() по-прежнему можно менять поля через тот же Builder.
          Также ваш Builder не может быть фабрикой, т.к. повторный build() вернёт уже существующий объект.
            +1
            Я об этом написал, ближе к концу топика. При необходимости можно сделать фабрику, коментарием выше подсказывают, что даже без необходимости ручного копирования полей.
            +3
            Достаточно бессмысленно делать билдеры для таких бинов. Билдер имеет смысл, если есть некоторая логика создания и инициализации, иначе можно просто из сеттеров возвращать объект чтобы делать цепочки вызовов. Ведь иммутабельности тут нет, а копировать объект всё время бывает накладно.
              +1
              Ведь иммутабельности тут нет

              Поля приватные, сеттеров нет. Единственный способ изменить поля объекта без рефлексии — только через билдер, а у него время жизни короткое.
              Чем не иммутабельность?
                0
                сделай поля final, тогда они станут немутабельными
                  0
                  20 финальных полей?! Их все в конструкторе инициализировать?

                  Одно из назначений этого паттерна в том, чтобы избавиться от конструтора с кучей параметров и при этом сделать класс немутабельным.
                    0
                    вот именно! для этого они мутабельные в билдере, и немутабельные в основном классе
                      0
                      А если передать объект билдера в конструктор?
                      public class Something {
                      
                          private Something(Builder builder) {
                              value1 = builder.value1;
                              value2 = builder.value2;
                              value3 = builder.value3;
                          }
                      
                          private final int value1;
                          private final String value2;
                          private final Object value3;
                      
                          public static class Builder {
                      
                              private int value1;
                              private String value2;
                              private Object value3;
                      
                              public void setValue1(int value1) {
                                  this.value1 = value1;
                              }
                      
                              public void setValue2(String value2) {
                                  this.value2 = value2;
                              }
                      
                              public void setValue3(Object value3) {
                                  this.value3 = value3;
                              }
                      
                              public Something build() {
                                  return new Something(this);
                              }
                      
                      
                          }
                      
                      }
                      


                        +1
                        Тогда весь набор полей надо повторить в билдере. Имеет смысл в случае когда очень нужно сделать поля final.
                      +1
                      Можете аргументированно пояснить почему класс Account из топика не немутабелен? Если вам пришла откуда-то ссылка на объект этого класса, через его интерфейс вы никак значения полей не измените. Если я правильно понимаю, с точки зрения многопоточности тоже все ок, другие треды не видят ссылку на частично собраный объект. final в самом первом примере Account больше для читабельности, если их убрать код будет работать в точности как и с ними. Я в чем-то заблуждаюсь?
                        +6
                        посмотрите Effective Java Блоха (имхо должна быть настольной книгой каждого джава дева) там есть и определение неизменяемого класса и объяснения почему это важно

                        в частности, без final на полях Вы не получите от JVM тех же гарантий видимости для многопоточного кода
                          0
                          Не могу понять о чем Вы говорите. Можете меня ткнуть носом в нужный Item из книги?
                            –6
                            без final на полях Вы не получите от JVM тех же гарантий видимости для многопоточного кода

                            Модификатор final используется только на этапе компиляции. В байткоде его нет и JVM про него ничего не знает. Какие «гарантии видимости» вы имеете ввиду?
                              +4
                              Вот ответ на stackoverflow Коротко: компилятор может менять инструкции местами и для другого треда все будет выглядеть так, что объект сначала создали, а только потом выставили значения полей. final явно говорит компилятору, что так делать не стоит.
                                0
                                А, теперь ясно. Спасибо.

                                Эта проблема связана с тем, что в байткоде создание объекта это две инструкции — NEW и INVOKESPECIAL.

                                Тем не менее Билдера эта проблема не касается. Вызов конструктора происходит в отдельном методе build() и только потом возвращается ссылка на объект. Даже если компилятор поменяет порядок внутри метода, ни один конкурентный поток не увидит ссылку на недостоенный объект. Разве что компилятор решит сделать inline методу, хотя я уверен, что на публичный метод у него рука не поднимется.
                                  +1
                                  Разве что компилятор решит сделать inline методу, хотя я уверен, что на публичный метод у него рука не поднимется.

                                  А почему нет, JIT-компилятор может заинлайнить код вашего метода build(), и уже после этого переставить инициализацию полей в произвольном порядке, в результате чего другие потоки увидят ваш объект еще до того, как будут записаны какие-то поля.

                                  ну т.е.

                                  Account acc = Account.newBuildler().setToken(a).setUserId(b).build();
                                  this.account = acc;
                                  doSomeAction(acc);
                                  


                                  JIT-компилятор может превратить в:

                                  Account acc = new Account();
                                  this.acc = acc;
                                  <вот тут из другого потока происходит чтение token, и он получает null...>
                                  acc.token = a;
                                  acc.userId = b;
                                  doSomeAction(acc);
                                  
                                    +3
                                    Дело не только в JIT-компиляции, а ещё в реордеринге в CPU и в протоколах когерентности кэшей.
                                      0
                                      Ну при наличии правильно расставленных барьеров реордеринг не будет проблемой. Беда в том, что если поле не-final, то JIT-компилятор не будет ставить лишние барьеры.
                                        +1
                                        О том и речь!
                                    +3
                                    Не сочтите за грубость, но мне искренне интересно, почему вы считаете, что:

                                    Модификатор final используется только на этапе компиляции. В байткоде его нет и JVM про него ничего не знает.


                                    Даже если компилятор поменяет порядок внутри метода, ни один конкурентный поток не увидит ссылку на недостоенный объект.


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


                                    Серьёзно, основываясь на чём вы делаете такие утверждения с такой уверенностью?
                                      0
                                      Про модификатор сказал ниже — перепутал с локальными переменными.
                                      После прочтения Java memory model в JLS я в этом уже не далеко не уверен.

                                      К сожалению очевидно ошибочный комментарий удалить уже нельзя.
                                        0
                                        Ок, первое вижу. А остальное? Исходя из каких убеждений вы сделали эти утверждения?
                                          0
                                          Исходя из каких убеждений вы сделали эти утверждения?


                                          Я имел ввиду Java компилятор, не JIT.

                                          Если бы Java компилятор мог самовольно инлайнить публичные методы, мы бы остались без библиотек.
                                          С JIT ситуация другая. Он работает уже с окончательным кодом, который нужно выполнить и волен решать сам как это делать.

                                          Тем не менее проблема билдера и многопоточности несколько надумана. Точнее она ничем не отличается от обычного вызова конструктора в многпоточном коде. Очевидно, это задача клиентского кода синхронизировать доступ к общим ресурсам. Как, например, при использовании StringBuilder'а.
                                            0
                                            Понял ваш ход мыслей, спасибо.
                                              +1
                                              действительно, речь же о всего лишь «элегантном» билдере а не потокобезопасном, чего все так накинулись )
                                    +2
                                    С чего вы взяли, что в байткоде модификатора final нет?
                                    docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.5
                                      0
                                      Спутал с локальными переменными и параметрами
                              +2
                              Ну какая же это иммутабельность, если
                              Account.Builder builder = Account.newBuilder();
                                      
                              Account account = builder
                                          .setToken("hello")
                                          .setUserId("habr")
                                          .build();
                              
                              assert account.getToken().equals("hello"); // ok
                              
                              builder.setToken("blablabla");
                                      
                              assert account.getToken().equals("hello"); // ой
                              
                                +2
                                Это ж уже осуждалось. Ctrl-F «Важные замечания».

                                Кроме того, программирование — это всегда поиск компромисов. Между производительностью, количеством кода и безопасностью использования. Если типичное использование билдера выглядит так:

                                Account account = Account.newBuilder().setToken("hello").setUserId("habr").build();
                                


                                То можно пренебречь возможностью менять объект через билдер. Вконце концов даже значение final поля можно изменить через рефлекшн, если SecurityManager это позволяет.
                                  +1
                                  Есть такое правило: если ошибку можно совершить, кто-то её обязательно совершит. Если вы этот код написали только для себя, то и никаких вопросов. Если им будет пользоваться вся команда, то я бы выбрал решение, где такую ошибку совершить нельзя. Скорее всего это был бы просто бин, которому в конструктор можно передать значения полей. Без флюэнтов и билдеров. Для таких бинов это очень эффективный способ и вполне удобный.
                            +13
                            Все уже было придумано до нас (а так же доведено до ума и отшлифовано до блеска):

                            @Getter
                            @Builder
                            public class Account {
                                
                                private String userId;
                                private String token;     
                            }
                            


                            Для параноиков (типа меня) есть вариант без использования публичного конструктора и c финальными полями:

                            @Getter
                            public class Account {
                                
                                private final String userId;
                                private final String token;
                                
                                @Builder
                                private Account(String userId, String token) {
                                    this.userId = userId;
                                    this.token = token;
                                }
                             }   
                              


                            Использование в обоих случаях одинаковое:
                            Account.builder().userId("u12").token("t12").build(); 


                            Подробности на projectlombok.org.
                              +3
                              А если нет желания тащить весь lombok, можно использовать github.com/google/auto
                              билдера пока нет, но должен когда-нибудь появиться — было бы логичным развитием
                                0
                                Как раз там есть в issues вопрос, будет ли билдер, и авторы ответили, что, скорее всего, нет.
                                  0
                                  github.com/google/auto/issues/111 и много других вопросов-клонов.
                                  +1
                                  Спасибо за ссылку, с первого взгляда рвет шаблон в клочья. Буду изучать.
                                    +1
                                    К сожалению плагин lombok для idea не поспевает за новыми версиями ide и глючит, по этой причине когда то отказался от использования его в проекте.
                                      +3
                                      Передали мне как-то проект, использующий lombok. Сперва было очень неочевидно, как работает вся эта магия, плюс надо куда-то лазить в настройки, чтобы идейка научилась процессить эти аннотации.

                                      Первым делом избавился от зависимости от lombok.
                                        0
                                        Возможно, вы поторопились. Не стоит становиться рабом лампы, даже если это замечательная Идея. lombok работает с эклипс, нетбинс и чудесно билдется в мавен. В конце концов, javaagent API появилась не вчера, а в jdk5. И потому уже давно не магия, а инструмент, который должен удобно настраиваться в любой современной IDE.
                                          0
                                          Оно жеж работает через annotation processing (легко увидеть здесь), а не через instrumentation.
                                            0
                                            Сейчас точно не помню, но кажется, с использованием lombok у меня были проблемы, что некуда поставить breakpoint в setter, т.к. этого самого setter-а в коде нет.

                                            Также, я стараюсь руководствоваться принципом наименьшего удивления, т.к. любой новый человек, который придет на проект, очень озадачится, как работает код, когда нет геттеров/сеттеров, хотя они вызываются.
                                          +1
                                          Для явы самым простым и рабочим решением стал бы плагин для ИДЕ, который генерирует код билдера для конкретного объекта (как toString и тп.).

                                            +4
                                            дык, хватает уже плагинов-то… в частности, например InnerBuilder
                                              +1
                                              Выглядит вполне ожидаемо, думаю обсуждение можно закрывать :)
                                            0
                                            Уж лучше Scala. Она хоть идеей нормально поддерживается.
                                            А вообще, идея отлично умеет генерить подобные билдеры.
                                              +1
                                              Только плагинами, из коробки в 14 этого нет.
                                                0
                                                Криво выразился. Я имел ввиду, что плагин Scala отлично работает, в отличие от плагина lombok.
                                                  +2
                                                  Я тоже некорректно выразил мысль. Имел ввиду, что идея без плагинов не имеет генерации билдеров.
                                                    0
                                                    Действительно, стандартно только Fluent Interface генерит.
                                            +1
                                            Вариант со static inner class можно реализовать и без копий полей оригинала. Достаточно хранить собираемый объект. Естественно, что это не работает для immutable классов.

                                            example
                                            public class Account {
                                              private String userId;
                                              private String token;
                                            
                                              private Account() {}
                                            
                                              // getters
                                            
                                              // builder fabric method
                                              public static Builder builder() {
                                                return new Builder();
                                              }
                                            
                                              public static class Builder {
                                                private Account account = new Account();
                                                
                                                public Account build() {
                                                  Account result = account;
                                                  account = new Account();
                                                  return result;
                                                }
                                            
                                                public Builder userId(String userId) {
                                                  account.userId = userId;
                                                  return this;
                                                }
                                            
                                                public Builder token(String token) {
                                                  account.token = token;
                                                  return this;
                                                }
                                              }
                                            }
                                            
                                            
                                            // usage
                                            
                                            Account account = Account.builder().userId(someId).token(someToken).build();
                                            
                                            Account.Builder builder = Account.builder();
                                            Account accountOne = builder.userId("1").token("t1").build();
                                            Account accountTwo = builder.userId("2").build(); // token will be null
                                            
                                              –5
                                              Идите к нам, у нас печеньки. В том числе именованные параметры, из-за отсутствия которых нужен очередной паттерн.

                                              C# 5.0 (текущая версия):

                                              public class Person
                                              {
                                                  public string LastName { get; private set; }
                                                  public string FirstName { get; private set; }
                                                  public string MiddleName { get; private set; }
                                                  public string Salutation { get; private set; }
                                                  public string Suffix { get; private set; }
                                                  public string StreetAddress { get; private set; }
                                                  public string City { get; private set; }
                                                  public string State { get; private set; }
                                                  public bool IsFemale { get; private set; }
                                                  public bool IsEmployed { get; private set; }
                                                  public bool IsHomeOwner { get; private set; }
                                              
                                                  public Person (string lastName = "",
                                                      string firstName = "",
                                                      string middleName = "",
                                                      string salutation = "",
                                                      string suffix = "",
                                                      string streetAddress = "",
                                                      string city = "",
                                                      string state = "",
                                                      bool isFemale = false,
                                                      bool isEmployed = false,
                                                      bool isHomeOwner = false)
                                                  {
                                                      LastName = lastName;
                                                      FirstName = firstName;
                                                      MiddleName = middleName;
                                                      Salutation = salutation;
                                                      Suffix = suffix;
                                                      StreetAddress = streetAddress;
                                                      City = city;
                                                      State = state;
                                                      IsFemale = isFemale;
                                                      IsEmployed = isEmployed;
                                                      IsHomeOwner = isHomeOwner;
                                                  }
                                              }

                                              C# 6.0 (следующая версия):

                                              public class Person
                                              {
                                                  public readonly string LastName { get; }
                                                  public readonly string FirstName { get; }
                                                  public readonly string MiddleName { get; }
                                                  public readonly string Salutation { get; }
                                                  public readonly string Suffix { get; }
                                                  public readonly string StreetAddress { get; }
                                                  public readonly string City { get; }
                                                  public readonly string State { get; }
                                                  public readonly bool IsFemale { get; }
                                                  public readonly bool IsEmployed { get; }
                                                  public readonly bool IsHomeOwner { get; }
                                              
                                                  public Person (string lastName = "",
                                                      string firstName = "",
                                                      string middleName = "",
                                                      string salutation = "",
                                                      string suffix = "",
                                                      string streetAddress = "",
                                                      string city = "",
                                                      string state = "",
                                                      bool isFemale = false,
                                                      bool isEmployed = false,
                                                      bool isHomeOwner = false)
                                                  {
                                                      LastName = lastName;
                                                      FirstName = firstName;
                                                      MiddleName = middleName;
                                                      Salutation = salutation;
                                                      Suffix = suffix;
                                                      StreetAddress = streetAddress;
                                                      City = city;
                                                      State = state;
                                                      IsFemale = isFemale;
                                                      IsEmployed = isEmployed;
                                                      IsHomeOwner = isHomeOwner;
                                                  }
                                              }

                                              C# 7.0 (планируемый синтаксис, раньше планировалось в 6.0):

                                              public class Person(
                                                  string lastName = null,
                                                  string firstName = null,
                                                  string middleName = null,
                                                  string salutation = null,
                                                  string suffix = null,
                                                  string streetAddress = null,
                                                  string city = null,
                                                  string state = null,
                                                  bool isFemale = false,
                                                  bool isEmployed = false,
                                                  bool isHomeOwner = false)
                                              {
                                                  public readonly string LastName { get; } = lastName;
                                                  public readonly string FirstName { get; } = firstName;
                                                  public readonly string MiddleName { get; } = middleName;
                                                  public readonly string Salutation { get; } = salutation;
                                                  public readonly string Suffix { get; } = suffix;
                                                  public readonly string StreetAddress { get; } = streetAddress;
                                                  public readonly string City { get; } = city;
                                                  public readonly string State { get; } = state;
                                                  public readonly bool IsFemale { get; } = isFemale;
                                                  public readonly bool IsEmployed { get; } = isEmployed;
                                                  public readonly bool IsHomeOwner { get; } = isHomeOwner;
                                              }

                                              Использование:

                                              var person = new Person(
                                                  lastName: "John",
                                                  firstName: "Smith",
                                                  city: "New York",
                                                  state: "New Tork");

                                              Ну вы поняли. trollface.jpg.to
                                                +7
                                                Да нафиг нам в вашу обитель зла, у нас есть Scala!
                                                  0
                                                  Скала — это хорошо, но найти работу с ней — это та ещё задачка, в отличие от мейнстрима. Ну не любит её энтерпрайз.

                                                  Дотнет с шарпом со следующего релиза — FOSS, так что традиционный аргумент про обитель зла перестаёт работать. :-)
                                                    +4
                                                    Но тут еще вопрос — а нужна ли контора, закрытая для новых технологий? Как бы на поиск работы можно смотреть совсем по-разному: или вот хочу в эту контору, нужно знать то-то и то-то, или хочу делать вот то-то на вот этом, вот конторы где это можно. Что касается интерпрайза, то есть уже вакансии в том же Luxoft на Scala, так что тут на месте дело не стоит.

                                                    Что касается открытости дотнета, формального открытия недостаточно, пройдут годы, прежде чем появится реальное oss комьюнити. А потом официальной версии под *nix пока нет. Очень странная технологий «добра» выходит, мы типа открытые, но работаем только на win, зашибись. Пока это все появится, глядишь и Scala станет мейнстимом :)
                                                      +1
                                                      Вообще там и офф версия под *nix ожидается. Да и комьюнити есть. Не такой большой, но весьма бодрый)

                                                      Вот выдержка из статьи:
                                                      We are building a .NET Core CLR for Windows, Mac and Linux and it will be both open source and it will be supported by Microsoft. It'll all happen at github.com/dotnet.

                                                      Announcing .NET 2015 — .NET as Open Source, .NET on Mac and Linux, and Visual Studio Community
                                                        +1
                                                        Ожидается и в продакшне — две разные разницы :)
                                                          +1
                                                          Нет оснований не верить, что не будет офф версии. Особенно имея уже рабочий k runtime )
                                                            +1
                                                            Дык а я и верю. Просто вот как появится, так и поговорим :)
                                                        +1
                                                        Скала одновременно к сожалению и к счастью регулярно ломает обратную и прямую совместимость, хотя и обещает чуток стабилизироваться.
                                                          0
                                                          Вообще-то они обещают все еще сильней поломать :) И в этом ее сила. Может я не очень понятно выразился, я и не утверждаю что скалу примет интерпрайз, скорее стоит говорить о каком-то ограниченном применении. Я лишь говорю, что на интерпайзе свет клином не сошелся, мне он что со скалой, что без скалы не нужен. Есть куча клевых и популярных технологий, которые там не используют, и это лишь довод для меня там не работать. Но повод работать там, где все это используют :)
                                                            0
                                                            Потому и написал, что и к сожалению, и к счастью)

                                                            Они так часто ломают совместимость, т. к. живут на стандартной JVM. Это хорошо в плане скорости работы (т. к. хороший gc и jit из коробки) и динамичного развития самого языка.
                                                          +1
                                                          Популярность языка — к сожалению, очень и очень важный фактор. Во-первых, не все живут в столицах, поэтому на весь город запросто может не быть ни одной компании, использующей ваши любимые технологии. Во-вторых, выбор — это сила. Из пяти компаний я выбираю или из сотни — это две большие разницы, как говорится.

                                                          Скалу в мейнстрим уже многие годы обещают, но прогресс незаметен. Так что в чудеса не верю. :)

                                                          С OSS комьюнити в дотнете всё в порядке. Уход дотнета в опен-сорс — это не резкий непродуманный рывок, а закономерный результат: мелкомягкие уже несколько лет к этому идут. Некоторые мелкомягкие библиотеки уже ушли в опен-сорс, некоторые популярные библиотеки от комьюнити получают поддержку мелкомягкими. Стеснения в выборе библиотек обычно не чувствую. Ну да, библиотек для парсинга командной строки — всего лишь 30, а не 130, как для джавы, но всё нужное есть.
                                                            0
                                                            Ну дык про популярность я же писал, тут две стратегии. Нет в одном городе, для меня это повод уехать из этого города, а не учить то, что в этом городе есть :) Тут смотря как к этому относиться.
                                                              0
                                                              Что касается комьюнити, то оно все-таки не в порядке, если говорить про не-интерпрайз. Если вы работате только в этой области, то вас ждет дивный новый мир :) Посмотрите на такие штуки, как reactive stack, typelevel stack. Посмотрите на такие платформы обработки данных, как spark. Потом интеграция с *nix это не мелочь, смотрите на такие штуки, как docker, vagrant, ansible, salt. Пока дотнет нормально не работает под никсы, вам эти технологии не доступны.
                                                                0
                                                                Энтерпрайз — это с любой стороны самый большой кусок разработки. Есть он — есть всё остальное. Соответственно, для меня это мера успеха платформы-технологии-языка.

                                                                Вы по части big data что ли? «Не-энтепрайз» — это для вас что?

                                                                Apache Spark — 1.0 только-только релизнулся, рано говорить про ограничения.
                                                                Docker — вижу статьи, как использовать с дотнетом.
                                                                Vagrant — аналогично.

                                                                Пока дотнет нормально не работает под никсы, вам эти технологии не доступны.

                                                                Моно уже давно работает нормально. Не слишком распространён — это да.
                                                                  0
                                                                  >Соответственно, для меня это мера успеха платформы-технологии-языка.

                                                                  Ну вот видите, а для меня нет. Меня инерпрайз сам по себе не интересует, не смотря на распространенность.
                                                                  0
                                                                  удалено.
                                                              +2
                                                              Scala для энтерпрайза действительно не лучшая идея. Сложно и не однозначно во многих вопросах. Всегда считал энтерпрайз похожим чем-то на армию. Те же требования к коду, как в армии к технике: простота и однозначность в эксплуатации, надежность и результативность. Остальное (цена и расходы) не важно. Лично я очень жду Kotlin.
                                                                +1
                                                                Так и есть. Просто мы начали разговор про удобство, закончили за упокой интепрайз :) Но опять же, есть дивный новый мир вне инерпрайза, где востребованы advanced технологии.
                                                                • UFO just landed and posted this here
                                                                    0
                                                                    А почему не kotlin, кстати?
                                                                    • UFO just landed and posted this here
                                                                  0
                                                                  На дотнете намного больше вакансии?
                                                                    0
                                                                    Э… Вакансий и на джаве, и на шарпе на порядок больше, чем на скале. HH (msk): scala — 33, java — 777, c# — 470, python — 378, ruby — 109.
                                                                    Какую страну, город ни выбирай, на каком сайте не ищи — скалы везде будет, как минимум, раз в 10 меньше, чем мейнстримовых языков. (Забавно, что моя оценка пальцем в небо «5 или 100» была настолько точна. :-D )

                                                                    Искренне ваш, К.О.
                                                              0
                                                              Если не ставить вопрос об эффективности (==количестве мусора), то лучшая реализация билдера, которую я видел — в joda-time. Там сами доменные объекты (и изменяемые реализации, и неизменяемые) предоставляют методы типа A.withXXX(newValue) -> A. При этом изменяемые реализации возвращают this, а неизменяемые — новый экземпляр объекта с измененным полем. Отличный подход, кмк. Единственный его недостаток в том, что на каждое свойство создается новый объект (для неизменяемых реализаций), но и то, в горячем коде если нормально отработает inliner и escape analyzer, то бОльшая часть этих объектов должна быть скаляризована.
                                                                0
                                                                Этот подход используется во всех иммутабельных типах, например, в BigDecimal. И это очень хороший подход, но это не билдер. Смысл паттерна «билдер» в том, чтобы состояние необходимое для конструкции объекта вынести в отдельный объект билдера, который не будет использоваться в реальной работе. Это так же позволяет вынести какую-то логику конструирования. Вроде такого:
                                                                public class Person {
                                                                
                                                                    private final String name;
                                                                    private final int age;
                                                                
                                                                    public Person(String name, int age) {
                                                                        this.name = name;
                                                                        this.age = age;
                                                                    }
                                                                
                                                                    public String getName() {
                                                                        return name;
                                                                    }
                                                                
                                                                    public int getAge() {
                                                                        return age;
                                                                    }
                                                                
                                                                    public static Builder builder() {
                                                                        return new Builder();
                                                                    }
                                                                
                                                                    public static class Builder {
                                                                        private NameGenerator nameGenerator = new RandomNameGenerator();
                                                                        private Integer age;
                                                                
                                                                        private Builder age(int age) {
                                                                            this.age = age;
                                                                            return this;
                                                                        }
                                                                
                                                                        public Builder birthDate(Calendar date) {
                                                                            return age(Calendar.getInstance().get(Calendar.YEAR) - date.get(Calendar.YEAR));
                                                                        }
                                                                
                                                                        public Builder name(String name) {
                                                                            return nameGenerator(new FixedNameGenerator(name));
                                                                        }
                                                                
                                                                        public Builder nameGenerator(NameGenerator generator) {
                                                                            this.nameGenerator = generator;
                                                                            return this;
                                                                        }
                                                                
                                                                        public Person build() {
                                                                            if (age == null) {
                                                                                throw new IllegalArgumentException();
                                                                            }
                                                                
                                                                            return new Person(nameGenerator.generate(), age);
                                                                        }
                                                                    }
                                                                
                                                                    public static void main(String[] args) {
                                                                        Person person = Person.builder().age(10).build();
                                                                    }
                                                                }
                                                                
                                                                  0
                                                                  В целом да, вы правы. Но тогда задача, которую ставит автор (не дублировать само описание состояния — поля) — не решаема, потому что билдер принципиально имеет другое состояние. Но на практике-то мы все знаем, что в половине случаев состояние вообще совпадает полностью, и все отличие в мутабельности/иммутабельности, еще в половине от оставшейся половины, опять же, состояние совпадает, и разница в дополнительных проверках, которые делает билдер, да более удобном API — fluent, специальные группы методов под конкретные паттерны использования, и т.п. И остается не так уж много случаев, когда билдер реально какую-то нетривиальную логику реализует, и нетривиальное состояние имеет. Поэтому мысль как-то упростить работу с наиболее частым случаем недурна — хотя, увы, я для джавы не вижу хорошей реализации

                                                                  >Смысл паттерна «билдер» в том, чтобы состояние необходимое для конструкции объекта вынести в отдельный объект билдера, который не будет использоваться в реальной работе. Это так же позволяет вынести какую-то логику конструирования.

                                                                  Рассуждения об истинном смысле каких-то шаблонов мне кажутся не очень осмысленными, потому что шаблон — это только идея.
                                                                  Например, по вашему же описанию «какая-то дополнительная логика создания» вполне себе вписывается в тот метод, что я описываю. «Отдельное состояние» — нет, но это нужно реже. Если я навешу на свой иммутабельный объект два интерфейса — Builder, в который вынесу все fluent withXXX() методы, и ReadableXXX где только на чтение доступ, то я смогу передавать свой объект в методы, которые принимают только Builder, и знать не знают, как он внутри устроен. С точки зрения таких методов — мой объект будет натуральным построителем, так ведь?
                                                                    0
                                                                    проблема в том, интерфейсы у билдера и объекта разные, и придется заниматся дублированием методов, рефакторить тоже вручную. Можно только сделать динамическую реализацию интерфейса билдера, но тогда ошибки будут уже не во время компиляции, а при запуске.

                                                                    т.е. написать что-то вроде:
                                                                    public interface IBuilder<T> {
                                                                        public T build();
                                                                    }
                                                                    
                                                                    public interface IPersonBuilder extends IBuilder<Person>
                                                                    {
                                                                        public IPersonBuilder age(int value);
                                                                    }
                                                                    
                                                                    Builder.newBuilder(IPersonBuilder.class).age(10).build();
                                                                    


                                                                    реализация которого будет сгенерена динамически, c проверкой существования полей.
                                                                      +2
                                                                      Задач buider решает много, лично я создаю его для следующих целей
                                                                      • На вход конструктору подается большое количество объектов, порядок которых легко перепутать
                                                                      • Я хочу сделать все поля класса неизменяемыми, чтобы немного облегчить себе жизнь в многопоточный среде (в идеале это все поля final)
                                                                      • В конструкторе происходят какие то действия, которые могут вызвать исключения ( в builder это вынесется в set метод поля) — например на вход подается json и не факт что там есть нужное поле, да и вообще парсить его в конструкторе я считаю некрасиво
                                                                      • Еще можно придумать случай когда внутри конструктора меняются стратегиии инициализации полей, тогда можно это вынести в set методы builder'а и еще задавать стратегии


                                                                      И пример автора не одну из этих задач не решает

                                                              Only users with full accounts can post comments. Log in, please.