Один из часто рассматриваемых паттернов — паттерн Builder. В основном рассматриваются варианты реализации «классического» варианта этого паттерна:

MyClass my = MyClass.builder().first(1).second(2.0).third("3").build();

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

Итак, расссмотрим их:

Минимальный builder или Реабилитация double brace initialization


Сначала рассмотрим минимальный builder, про который часто забывают — double brace initialization (
http://stackoverflow.com/questions/1958636/what-is-double-brace-initialization-in-java, http://c2.com/cgi/wiki?DoubleBraceInitialization). Используя double brace initialization мы можем делать следующее:

new MyClass() {{ first = 1; second = 2.0; third = "3"; }}

Что мы тут видим?
  1. Нарушение совместимости equals
    Что такое «совместимость equals»? Дело в том что стандартный equals примерно такой:

    @Override public boolean equals(Object obj) {
        if(this == obj) return true;
        if(!super.equals(obj)) return false;
        if(getClass() != obj.getClass()) return false;
        ...
    }
    

    И при сравнении с унаследованным классом equals будет возвращать false. Но мы создаём анонимный унаследованный класс и вмешиваемся в цепочку наследования.
  2. Возможная утечка памяти, т.к. анонимный класс будет держать ссылку на контекст создания.
  3. Инициализация полей без проверок.

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

В результате обычно double brace initialization используют для инициализации составных структур. Например:

new TreeMap<String, Object>() {{ put("first", 1); put(second, 2.0); put("third", "3"); }}

Тут используются методы, а не прямой доступ к полям и совместимость по equals обычно не требуется. Так как же мы можем использовать такой ненадёжный хакоподобный метод? Да очень просто — выделив для double brace initialization отдельный класс билдера.

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

public static class Builder {
    public int    first  = -1        ;
    public double second = Double.NaN;
    public String third  = null      ;
    
    public MyClass create() {
        return new MyClass(
            first ,
            second,
            third
            );
    }
}

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

new MyClass.Builder(){{ first = 1; third = "3"; }}.create()

Что мы получаем?
  1. Builder не вмешивается в цепочку наследования — это отдельный класс.
  2. Builder не течёт — его использование прекращается после создания объекта.
  3. Builder может контролировать параметры — в методе создания объекта.

Voila! Double brace initialization реабилитирована.

Для использовании наследования, Builder разделяется на две части (один с полями, другой — с методом создания) следующим образом:

public class MyBaseClass {

    protected static class BuilderImpl {
        public int    first  = -1        ;
        public double second = Double.NaN;
        public String third  = null      ;
    }
    
    public static class Builder extends BuilderImpl {
        
        public MyBaseClass create() {
            return new MyBaseClass(
                first ,
                second,
                third
                );
        }
        
    }
    ...
}
public class MyChildClass extends MyBaseClass {

    protected static class BuilderImpl extends MyBaseClass.BuilderImpl {
        public Object fourth = null;
    }
    
    public static class Builder extends BuilderImpl {
        public MyChildClass create() {
            return new MyChildClass(
                first ,
                second,
                third ,
                fourth
                );
        }
        
    }
    ...
}

Если нужны обязательные параметры, они будут выглядеть так:

public static class Builder {
    public double second = Double.NaN;
    public String third  = null      ;
    
    public MyClass create(int first) {
        return new MyClass(
            first ,
            second,
            third
            );
    }
}

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

new MyClass.Builder(){{ third = "3"; }}.create(1)

Это настолько просто, что можно использовать хоть как построитель параметров функций, например:

String fn = new fn(){{ first = 1; third = "3"; }}.invoke();

Полный код на github.

Перейдём к сложному.

Максимально сложный Mega Builder


А что, собственно, можно усложнить? А вот что! Сделаем Builder, который в compile-time будет:
  1. не позволять использовать недопустимые комбинации параметров
  2. не позволять строить объект если не заполнены обязательные параметров
  3. не допускать повторной инициализации параметров

Что нам понадобится для этого? Для этого нам понадобится создать интерфейсы со всеми вариантами сочетаний параметров, для чего сначала сделем декомпозицию объекта на отдельные интерфейсы соответствующие каждому параметру.

Нам понадобится интерфейс для присвоения каждого параметра и возврата нового билдера. Он должен выглядеть как-то так:

public interface TransitionNAME<T> { T NAME(TYPE v); }

При этом NAME должен быть разным для каждого интерфейса — ведь их потом надо будет объединять.

Также понадобится и getter, чтобы мы могли получить значение после такого присвоения:

public interface GetterNAME { TYPE NAME(); }

Поскольку нам понадобится связка transition-getter, определим transition-интерфейс следующим образом:

public interface TransitionNAME<T extends GetterNAME> { T NAME(TYPE v); }

Это также добавит статического контроля в описаниях.

Примерно понятно, наборы каких интерфейсов мы собираемся перебирать. Определимся теперь, как это сделать.

Возьмём такой же как в предыдущем примере 1-2-3 класс и распишем для начала все сочетания параметров. Получим знакомое бинарное представление:

first second third
-     -      -
-     -      +
-     +      -
-     +      +
+     -      -
+     -      +
+     +      -
+     +      +

Для удобства представим это в виде дерева следующим образом:

first second third
-     -      -    /
+     -      -    /+
+     +      -    /+/+
+     +      +    /+/+/+
+     -      +    /+/-/+
-     +      -    /-/+
-     +      +    /-/+/+
-     -      +    /-/-/+

Промаркируем допустимые сочетания, например так:

first second third
-     -      -    /       *
+     -      -    /+      *
+     +      -    /+/+    * 
+     +      +    /+/+/+
+     -      +    /+/-/+  *
-     +      -    /-/+
-     +      +    /-/+/+  *
-     -      +    /-/-/+  *

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

first second third
-     -      -    /       *
+     -      -    /+      *
+     +      -    /+/+    * 
+     -      +    /+/-/+  *
-     +      -    /-/+
-     +      +    /-/+/+  *
-     -      +    /-/-/+  *

Как же реализовать это?

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

Нарисуем интерфейсы:

public interface Get_first  { int    first (); }
public interface Get_second { double second(); }
public interface Get_third  { String third (); }

public interface Trans_first <T extends Get_first > { T first (int    first ); }
public interface Trans_second<T extends Get_second> { T second(double second); }
public interface Trans_third <T extends Get_third > { T third (String third ); }

Табличку с этим рисовать неудобно, сократим идентификаторы:

public interface G_1 extends Get_first {}
public interface G_2 extends Get_second{}
public interface G_3 extends Get_third {}

public interface T_1<T extends G_1> extends Trans_first <T> {}
public interface T_2<T extends G_2> extends Trans_second<T> {}
public interface T_3<T extends G_3> extends Trans_third <T> {}

Нарисуем табличку переходов:

public interface B     extends T_1<B_1  >, T_2<B_2  >, T_3<B_3  > {} // - - -    /       *
public interface B_1   extends             T_2<B_1_2>, T_3<B_1_3> {} // + - -    /+      *
public interface B_1_2 extends                                    {} // + + -    /+/+    *
public interface B_1_3 extends                                    {} // + - +    /+/-/+  *
public interface B_2   extends T_1<B_1_2>,             T_3<B_2_3> {} //          /-/+     
public interface B_2_3 extends                                    {} // - + +    /-/+/+  *
public interface B_3   extends T_1<B_1_3>, T_2<B_2_3>             {} // - - +    /-/-/+  *

Определим Built интерфейс:

public interface Built { MyClass build(); }

Промаркируем интерфейсы, где уже можно построить класс интерфейсом Built, добавим getter-ы и определим получившийся Builder-интерфейс:

//                                                  транзит
//                                                    |                           можем строить
//                                 геттеры            |                             |
//                                   |                |                             |
//                             -------------  ----------------------------------  -----
//
//                             first          first                                           first 
//                             |    second    |           second                              | second
//                             |    |    third|           |           third                   | | third
//                             |    |    |    |           |           |                       | | |
public interface B     extends                T_1<B_1  >, T_2<B_2  >, T_3<B_3  >, Built {} // - - -    /       *
public interface B_1   extends G_1,                       T_2<B_1_2>, T_3<B_1_3>, Built {} // + - -    /+      *
public interface B_1_2 extends G_1, G_2,                                          Built {} // + + -    /+/+    * 
public interface B_1_3 extends G_1,      G_3,                                     Built {} // + - +    /+/-/+  *
public interface B_2   extends      G_2,      T_1<B_1_2>,             T_3<B_2_3>        {} //          /-/+     
public interface B_2_3 extends      G_2, G_3,                                     Built {} // - + +    /-/+/+  *
public interface B_3   extends           G_3, T_1<B_1_3>, T_2<B_2_3>,             Built {} // - - +    /-/-/+  *

public interface Builder extends B {}

Этих описаний достаточно, чтобы по ним можно было в run-time соорудить proxy, надо только подправить получившиеся определения, добавив в них маркерные интерфейсы:

public interface Built extends BuiltBase<MyClass> {}

public interface Get_first  extends GetBase { int    first (); }
public interface Get_second extends GetBase { double second(); }
public interface Get_third  extends GetBase { String third (); }

public interface Trans_first <T extends Get_first > extends TransBase { T first (int    first ); }
public interface Trans_second<T extends Get_second> extends TransBase { T second(double second); }
public interface Trans_third <T extends Get_third > extends TransBase { T third (String third ); }

Теперь надо получить из Builder-классов значения чтобы создать реальный класс. Тут возможно два варианта — или создавать методы для каждого билдера и статически-типизированно получать параметры из каждого builder-а:

public MyClass build(B     builder) { return new MyClass(-1             , Double.NaN      , null); }
public MyClass build(B_1   builder) { return new MyClass(builder.first(), Double.NaN      , null); }
public MyClass build(B_1_2 builder) { return new MyClass(builder.first(), builder.second(), null); }
...

или воспользоваться обобщённым методом, определённым примерно следующим образом:

public MyClass build(BuiltValues values) {
    return new MyClass(
        // значения из values
        );
}

Но как получить значения?

Во-первых у нас есть по-прежнему есть набор builder-классов у которых есть нужные getter-ы. Соответственно надо проверять есть ли реализация нужного getter и если есть — приводить тип к нему и получать значение:

(values instanceof Get_first) ? ((Get_first) values).first() : -1

Конечно, можно добавить метод получения значения, но оно будет нетипизированным, так как мы не сможем получить тип значения из существующих типов:

Object getValue(final Class< ? extends GetBase> key);

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

(Integer) values.getValue(Get_first.class)

Для того чтобы получить тип, пришлось бы создавать дополнительные классы и связки наподобие:

public interface TypedGetter<T, GETTER> { Class<GETTER> getterClass(); };

public static final Classed<T> GET_FIRST = new Classed<Integer>(Get_first.class);

Тогда метод получения значения мог бы быть определён следующим образом:

public <T, GETTER> T get(TypedGetter<T, GETTER> typedGetter);

Но мы попытаемся обойтись тем что есть — getter и transition интерфейсами. Тогда, без приведений типов, вернуть значение можно только вернув getter-интерфейс или null, если такой интерфейс не определён для данного builder:

<T extends GetBase> T get(Class<T> key);

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

(null == values.get(Get_second.class)) ? Double.NaN: values.get(Get_second.class).second()

Это уже лучше. Но можно ли добавить значение по-умолчанию в случае отсутствия интерфейса, сохранив тип? Конечно, возможно возвращать типизированный getter-интерфейс, но всё равно придётся передавать нетипизированное значение по умолчанию:

<T extends GetBase> T get(Class<T> key, Object defaultValue);

Но мы можем воспользоваться для установки значения по умолчанию transition-интерфейсом:

<T extends TransBase> T getDefault(Class< ? super T> key);

И использовать это следующим образом:

values.getDefault(Get_third.class).third("1").third()

Это всё что можно типобезопасно соорудить с существующими интерфейсами. Создадим обобщённый метод инициализации иллюстрирующий перечисленные варианты использования и проинициализируем результирующий билдер:

protected static final Builder __builder = MegaBuilder.newBuilder(
    Builder.class, null,
    new ClassBuilder<Object, MyClass>() {
        @Override public MyClass build(Object context, BuiltValues values) {
            return new MyClass(
                (values instanceof Get_first) ? ((Get_first) values).first() : -1,
                (null == values.get(Get_second.class)) ? Double.NaN: values.get(Get_second.class).second(),
                values.getDefault(Get_third.class).third(null).third()
                );
        }
    }
);

public static Builder builder() { return __builder; }

Теперь можно его вызывать:

builder()                              .build();
builder().first(1)                     .build();
builder().first(1).second(2)           .build(); builder().second(2  ).first (1).build();
builder().first(1)          .third("3").build(); builder().third ("3").first (1).build(); 
builder()         .second(2).third("3").build(); builder().third ("3").second(2).build();
builder()                   .third("3").build();

Скачать код и посмотреть на работу context assist можно отсюда.

В частности:
Код рассматриваемого примера: MyClass.java
Пример с generic-типами: MyParameterizedClass.java
Пример не-статического builder: MyLocalClass.java.

Итого


  • Double brace initialization не будет хаком или антипаттерном, если добавить немного билдера.
  • Гораздо проще пользоваться динамическими объектами + типизированными дескрипторами доступа (см. в тексте пример с TypedGetter) чем использовать сборки интерфейсов или другие варианты статически-типизированных объектов, поскольку это влечёт за собой необходимость работы с reflection со всеми вытекающими.
  • С использованием аннотаций возможно удалось бы упростить код proxy-генератора, но это усложнило бы объявления и, вероятно, ухудшило бы выявление несоответствий в compile-time.
  • Ну и наконец, в данной статье мы окончательно и бесповоротно определили минимальную и максимальную границу сложности паттерна Builder — все остальные варианты находятся где-то между ними.

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


  1. yaneblog
    04.09.2015 15:48

    Вызывают ли одобрение нижеперечисленные подходы:
    projectlombok.org/features/Builder.html
    projectlombok.org/features/Builder.html#singular
    ?


    1. speakingfish
      04.09.2015 17:09

      Идея обвязывать заодно первый уровень коллекций интересная.
      По идее, если уж так делать, то можно было бы и следующие уровни обвязывать и не только коллекций но и просто вложенных классов.
      Почему коллекции инициализируются только или элементами ли коллекциями? Что если я использую ленивые итераторы?
      Непонятно в каком месте реализовывать валидацию.
      Посмотрел сгенерированный код projectlombok.org/features/Singular-snippet.html
      Метод public SingularExample build() как-то переусложнён — всё это можно было бы спокойно вынести в хелперы.
      Почему списки билдера инициализируются ArrayList а не LinkedList?
      Непонятно как организовать обязательные поля и как соорудить контекстно-зависимый билдер.


      1. vedenin1980
        04.09.2015 17:13
        +3

        Почему списки билдера инициализируются ArrayList а не LinkedList?

        Странный вопрос, на практике LinkedList почти всегда проигрывает ArrayList, как по скорости работы, так и по потребляемой памяти.


        1. speakingfish
          04.09.2015 17:51

          И действительно.
          habrahabr.ru/post/233797
          ---Add elements ( 6kk )
          LinkedList: 2264 ms
          ArrayList: 493 ms
          ArrayList is faster


          1. withoutuniverse
            05.09.2015 14:45
            -2

            с помощью итератора (когда не нужно ходить по списку каждую итерацию для поиска последнего элемента) LinkedList быстрее.
            пруф


            1. vedenin1980
              05.09.2015 15:05
              +3

              Смотрите, такие тесты (вставить тысячу элементов в одно место) мало полезны, так как
              1) Если нужно вставить тысячу элементов в начало или середину — создаем новый ArrayList на тысячу элементов и вставляем его как addAll — получится быстрее чем у LinkedList,
              2) Если нужно вставлять элементы в произвольные места списка, то сложность итерирования LinkedList съест всякую пользу от скорости вставки по сравнению с ArrayList,
              3) Если нужно часто удалять элементы, проще в ArrayList записывать null, а потом null'ы игнорировать — ArrayList будет куда быстрее,
              4) Если нужно часто вставлять элементы в произвольные места списка — лучше использовать SortedMap'у c числовым ключом (элементы вставляются быстро по ключу и итерирование будет тоже быстрым).
              5) Надо не забывать что в реальных случаях, List надо создать, обработать и куда-то записать результат. Создание и запись результата у LinkedList настолько дорогие, что как правило любая польза от алгоритма это не перевешивает,
              6) Не стоит забывать что КАЖДЫЙ элемент LinkedList это отдельный элемент Node с ссылкой на значение и первый и последний элемент, поэтому во-первых, каждый элемент ест минимум в 8 раз больше памяти, создание нового объекта весьма дорого, так и сборка мусора после LinkedList в разы дороже, если ArrayList на миллион элементов собирается как один объект, то в LinkedList как миллион и один объект.
              7) плюс LinkedList хранится в памяти в разных блоках, а ArrayList в одном, что ускоряет обработку,

              Поэтому такие пруфы в реальных задачах смысла не имеют, использование LinkedList крайне крайне редко оправдано.


              1. vedenin1980
                05.09.2015 15:10

                В 6 пункте я имел в виду, конечно, что каждый элемент LinkedList это отдельный объект класса Node.


              1. withoutuniverse
                05.09.2015 16:39
                -4

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

                Потому пройдемся по списку:

                1) Пример надуман, да и не представляю что создать миллионный массив + 1000 быстрее, чем просто добавить 1000
                2) Приведите пример? В чем сложность итерирования вообще чего либо?
                3) Это заставит приложение разрастить просто нереально, о эффективном использовании памяти речь не будет идти вовсе. (Не забываем про аллокацию при достижении лимита выделенной памяти под массив)
                4) Вода, нужен пруф и тесты
                5) Снова вода и снова нужны пруф и тесты
                6) Каждый элемент массива — указатель. Конечно памяти ест меньше, но ваше сравнение некорректное, ведь для каждой структуры свои цели — советую почитать что и где лучше использовать.
                7) При добавлении неизвестного количества элементов LinkedList даст фору именно по этой причине.

                Вы написал «пруфы смысла не имеют» — не надо так. Я сам использую почти всегда ArrayList, но лишь по той причине, что скорость работы и количество аллокаций мне не важны. В BigData разговор был бы совсем другой.


                1. vedenin1980
                  05.09.2015 23:43
                  +2

                  нужен пруф и тесты

                  Пруф? Официальная документация вам будет достаточным пруфом?

                  There are two general-purpose List implementations — ArrayList and LinkedList. Most of the time, you'll probably use ArrayList, which offers constant-time positional access and is just plain fast. It does not have to allocate a node object for each element in the List, and it can take advantage of System.arraycopy when it has to move multiple elements at the same time. Think of ArrayList as Vector without the synchronization overhead.

                  If you frequently add elements to the beginning of the List or iterate over the List to delete elements from its interior, you should consider using LinkedList. These operations require constant-time in a LinkedList and linear-time in an ArrayList. But you pay a big price in performance. Positional access requires linear-time in a LinkedList and constant-time in an ArrayList. Furthermore, the constant factor for LinkedList is much worse. If you think you want to use a LinkedList, measure the performance of your application with both LinkedList and ArrayList before making your choice; ArrayList is usually faster.

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

                  >> 2) Приведите пример? В чем сложность итерирования вообще чего либо?
                  Тем что сложность итерирования в LinkedList O(n), а добавление O(1), сложность итерирования в ArrayList — O(1), а добавления O(n). То есть добавить элемент в случайное место сложность будет одинаковая O(n), но как сказано в официальной документации у LinkedList это O(n) будет намного хуже, чем O(n) ArrayList.

                  >> 3) Это заставит приложение разрастить просто нереально, о эффективном использовании памяти речь не будет идти вовсе. (Не забываем про аллокацию при достижении лимита выделенной памяти под массив)
                  Вовсе нет, если кол-во null будет больше половины. можно скопировать не null элементы в новый массив это O(n), но все n удаления c перемещениями будут O(n), то есть O(2*n). В то время как в LinkedList перемещения с ударениями будут O(n^2). Учитывая намного большие затраты памяти в LinkedList, даже заранее выделение достаточного лимита и трата лишней памяти в ArrayList будет экономнее

                  >>4) Вода, нужен пруф и тесты
                  >>5) Снова вода и снова нужны пруф и тесты
                  Достаточно просто посчитать сложность алгоритмов (вставка и итерирование Map'ы это O(1), что куда лучше чем O(n) у LinkedList) и посмотреть что написано в офф.документации про LinkedList.

                  >>7) При добавлении неизвестного количества элементов LinkedList даст фору именно по этой причине.
                  Не даст, достаточно выделить достаточно памяти под ожидаемый алгоритм если нужно оптимизировать скорость или ограничить память, если небольшая потеря производительности при переаллокации допустима.

                  >>скорость работы и количество аллокаций мне не важны. В BigData разговор был бы совсем другой.
                  Вы серьезно? Когда я оптимизировал работу с BigData мне приходилось отказываться не только от LinkedList, но даже от HashMap и ArrayList и работать тупо с массивами.


                  1. withoutuniverse
                    06.09.2015 03:00

                    Про выдержку на английском я тоже согласен, о чем и написал в конце предыдущего сообщения. «тупо массив» использовать не выйдет при неизвестном количестве элементов, и придется писать заново велосипед для расширения массива, который уже внутри ArrayList. Хотя в случае с забиванием null пожалуй соглашусь с вами, потери тут будут минимальны.

                    А вот по сложности алгоритмов — вы путаете произвольный доступ к случайному элементу и итерирование. Было бы странным при практически равных показателях на операции с чтением всех элементов иметь в одном случае o(n) а в другом o(1).

                    Также хотел бы ОБЯЗАТЕЛЬНО обратить внимание на пункт с последовательностью в памяти к элементам, что должно было бы упростить доступ к ним — в java в памяти последовательно лежит только массив указателей на указатели, сами же указатели на объекты и объекты будут непоследовательно лежать в памяти, в разных местах.


                  1. speakingfish
                    07.09.2015 14:19

                    Когда я оптимизировал работу с BigData мне приходилось отказываться не только от LinkedList, но даже от HashMap и ArrayList и работать тупо с массивами.

                    Интересно. ArrayList же просто обёртка над массивом. Что в нём было неэффективно и возможна ли другая более эффективная обёртка над массивом?
                    И чем вы заменили HashMap? Параллельными сортированными массивами? И возможна ли эффективная обёртка?


                    1. vedenin1980
                      07.09.2015 14:33

                      ArrayList же просто обёртка над массивом. Что в нём было неэффективно и возможна ли другая более эффективная обёртка над массивом?

                      ArrayList неэффективен если надо работать с примитивными типами, так он всегда использует обертки примитивов, что ест память и производительность. Конечно, существуют trove коллекции, но зачем вводить лишнею обертку над массивами с неизвестным overhead'ом (да и не для всего они подходят), если массива хватало за глаза? Тем более что при больших данных проблема была не в том что неизвестно сколько элементов в массивах, а в том чтобы засунуть работу с миллиардами записей в несчастные сто гигабайт памяти, не убив производительность.

                      И чем вы заменили HashMap? Параллельными сортированными массивами? И возможна ли эффективная обёртка?

                      Массивом с похожим функционалов как у HashMap'ы (большую часть тупо скопипастили) но с клонированием ключей и значений вместо создания новых объектов и одновременным поиском/вставкой. Это позволило ускорить работу в пару раз (что в результате позволило сэкономить часы работы сервера ежедневно).


                      1. speakingfish
                        07.09.2015 17:56

                        Т.е. эти оптимизации в основном были связаны с неэффективной работой Java с массивами примитивных типов?


                        1. vedenin1980
                          07.09.2015 18:16

                          Нет, главные оптимизации были с катомными HashMap — клонирование объектов ключа и значения вместо дорого создания объектов и реализация метода getOrCreate, когда объект находится или создается одной итерацией, а не двумя. Примитивные типы это уже поскольку постольку.


  1. kokorins
    04.09.2015 15:56
    +1

    У первого подхода есть еще одна неприятность, что поля должны быть инициализированы финальными значениями, а значит что-нибудь простое

    public class HelloWorld{
        static class A {
            int a=0;
        }
    
         public static void main(String []args){
            List<A> as = new ArrayList<>();
            for(int i=0; i<10; ++i)
                as.add(new A(){{a=i;}});
         }
    }
    

    не скомпилируется, так как i не final.


    1. speakingfish
      04.09.2015 17:36

      Да вы правы, такое не прокатит.
      Но, в общем, все уже привыкли не только к особенностям анонимных классов, но и вовсю используют лямбды Java 8 и знают про final/effectively final.
      Я например чаще использую for each, тогда в аналогичном коде для Java 8+ ничего не надо будет менять, только для Java < 8 надо будет добавить final:

              for(final int i : new int[] {1,2,3}) {
                  new MyClass.Builder(){{ first = i; third = "3"; }}.create();
              }
      


  1. Athari
    04.09.2015 21:46
    +2

    Чего только не придумают, чтобы не писать на C#. ;-D

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

    1. Спец, который может одолеть весь этот матан.
    2. Нубы в большой концентрации, которых нужно ограничивать.
    3. Более-менее стабильный интерфейс, потому что проделывать это каждый раз невесело.
    4. Малое количество вариантов, потому что код растёт экспоненциально (?).


    1. gurinderu
      04.09.2015 22:22
      +1

      а как вам поможет c# при реализации паттерна builder?


      1. Athari
        04.09.2015 22:51
        +3

        В случае самого простого «билдера» отдельный класс не нужен:

        а) Можно задавать read-write свойства в блоке инициализации. Если какие-то свойства обязательны, добавить их в конструктор.

        new Foo(1) { Bar = 2, Baz = 3 }
        В отличие от Java, отдельный класс для каждого случая генерироваться не будет, никакой контекст захватываться тоже не будет, это просто запись свойств.

        б) Можно сделать конструктор или статический метод со всеми доступными свойствами, для всех опциональных задать значения по умолчанию, вызывать с именованными аргументами:
        new Foo(foo: 1, bar: 2, baz: 3)
        Foo.CreateFromBar(2, baz: 3)
        Этот способ даёт возможность пропускать имена аргументов, но можно прописать в конвенции (а также прикрутить проверку в IDE и при коммите по вкусу), что в таких случаях использование именованных аргументов обязательно.

        Инициализация коллекций и словарей поддерживается для конструкторов:
        new Foo { Bars = { 1, 2, 3 }, Bazs = { [1] = 2, [3] = 4 } }
        Кажется, обсуждали поддержку блока инициализации ещё и для статических методов, но не помню, к чему там пришли.

        Чуть больше кода в этом комментарии: habrahabr.ru/post/244521/#comment_8154859 (что характерно, тоже заминусованном).


        1. speakingfish
          05.09.2015 00:39
          +1

          По поводу варианта А — в этом случае наверное не может быть final полей и не могут выполнятся проверки.
          Вот вариант Б — да, именованные параметры со значениями по умолчанию сделали бы многие простые билдеры ненужными, однако…
          Сейчас я добавил сокращённые версии классов с минимальным билдером. Там по новым ссылкам можно их увидеть github.com/speaking-fish/java-sf-builder-simple-example — классы New*
          В них в конструктор класса передаются не все параметры, а только билдер. Т.е. теперь не надо перечислять все параметры в функциях.
          Вы же согласны что лучше иметь сложные проверки отдельно от конструктора, в билдере.
          Т.е. тогда в C# билдер останется, но не отдельным классом а отдельной функцией, так?
          Тогда возникает вопрос, а как будет работать наследование?
          Если у меня наследуется класс билдера — то он наследует все свои поля и их потом не надо опять все перечислять.
          А что с параметрами функции? Их можно унаследовать или в унаследованных классах придётся как перечислять всё больше и больше параметров и делать какие-то проверки чтобы их случайно не упустить, и в каждом классе потом искать и исправлять дефолтные значения или определять еще для них константы и использовать константы?
          Поясните этот момент.
          Это вообще скорее не C# касается, а всех языков с возможностью использования именованных параметров функций.
          Т.е. в результате, отдельный класс билдера будет проще использовать и в таких языках.
          PS. А инициализация коллекций и словарей многоуровневая или только первый уровень поддерживается?


          1. Athari
            05.09.2015 07:40

            По поводу варианта А — в этом случае наверное не может быть final полей и не могут выполнятся проверки.

            Read-only поля и свойства задать так не получится, им можно задавать значение только в конструкторе, а вот проверки можно положить в сеттер свойств, если они read-write.

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

            Почему? Это банальная проверка аргументов в конструкторе.

            Тогда возникает вопрос, а как будет работать наследование?

            Если честно, я нечасто вижу глубокие иерархии сложных объектов с билдерами на десятки свойств, поэтому вопросом, что делать в общем случае, я не задавался. :) Если количество свойств идёт на десятки, а глубина иерархии дальше одинарной, то я бы не занимался сомнительной копипастой фабричных методов с именованными аргументами, а занялся бы поиском более вменяемого интерфейса, вероятно, с отдельными билдерами, у которых задание свойств собрано в осмысленные выражения. Это тот момент, когда нужно отвлечься от механического кодинга и включить мозг.

            Всё-таки мы говорим про простые билдеры. В их рамках копипаста нескольких аргументов займёт меньше места, чем разведение иерархии билдеров. Набивать код проблемы не составит, решарпер всё сам скопипастит. Хотя, конечно, нехорошо, что значения будут дублироваться. Можно в константы вынести, если есть опасения, что значения изменятся.

            Скажем, вот код, эквивалентный вашим NewBaseClass.java и NewChildClass.java (где-то в два раза короче):

            using static System.Console;
            
            class CsBaseClass
            {
                public int First { get; }
                public double Second { get; }
                public string Third { get; }
            
                public CsBaseClass (int first = -1, double second = double.NaN, string third = null)
                {
                    First = first;
                    Second = second;
                    Third = third;
                }
            
                public override string ToString () => $"{GetType().Name} - First: {First}, Second: {Second}, Third: {Third}";
            }
            
            class CsChildClass : CsBaseClass
            {
                public object Fourth { get; }
            
                public CsChildClass (int first = -1, double second = double.NaN, string third = null, object fourth = null)
                    : base(first, second, third)
                {
                    Fourth = fourth;
                }
            
                public override string ToString () => $"{base.ToString()}, Fourth: {Fourth}";
            }
            
            class Program
            {
                static void Main ()
                {
                    WriteLine(new CsBaseClass(first: 1, third: "3"));
                    WriteLine(new CsChildClass(first: 1, third: "3", fourth: 4));
                }
            }

            Если количество свойств доберётся до десяти я так, конечно, делать не буду. :)

            В C# 7 планируется сделать что-то вроде этого (не следил за последними обновлениями, могу соврать; там были заморочки с именованием аргументов и свойств):

            using static System.Console;
            
            class CsBaseClass (int First = -1, double Second = double.NaN, string Third = null);
            
            class CsChildClass : CsBaseClass (int First = -1, double Second = double.NaN, string Third = null, object Fourth = null);
            
            class Program
            {
                static void Main ()
                {
                    WriteLine(new CsBaseClass(First: 1, Third: "3"));
                    WriteLine(new CsChildClass(First: 1, Third: "3", Fourth: 4));
                }
            }

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

            PS. А инициализация коллекций и словарей многоуровневая или только первый уровень поддерживается?

            Многоуровневая.


            1. speakingfish
              07.09.2015 01:07

              Пример билдера который никак нельзя воткнуть ни в конструктор ни в основной билдер:

              public class LocalBuilderExample {
                  
                  protected final double base;
                  
                  public LocalBuilderExample(double base) {
                      super();
                      this.base = base;
                  }
              
                  public class BaseBuilder extends NewBaseClass.BuilderImpl {
                      public NewBaseClass create() {
                          if(!Double.isNaN(base)) {
                              first  = (int) Math.round(base) + first;
                              if(!Double.isNaN(base)) {
                                  second = base + second;
                              } else {
                                  second = base;
                              }
                          }
                          return new NewBaseClass(this);
                      };
                  }
              }
              

              Вы предлагаете в таком случае дублировать все описания параметров вместе со всеми дефолтными значениями.

              Скажем, вот код, эквивалентный вашим NewBaseClass.java и NewChildClass.java (где-то в два раза короче):

              В вашем коде не хватает констант для дефолтных значений — это соответственно увеличит код.
              Добавим их и сравним построчно код игнорируя пустые строки (я убрал модификаторы доступа чтобы влезло):
              C#                                              Java
              class BaseClass {                           =   class BaseClass {                                       
                                                           
                                                                                                                                     
                                                           +  static class BuilderImpl {                                                                                                          
                                                           +      int    first  = -1        ;                                                                                                        
                                                           +      double second = Double.NaN;                                                                                                        
                                                           +      String third  = null      ;                                                                                                        
                                                           +  }                                                                                                                                             
                                                           +5
                                                           +  static class Builder extends BuilderImpl {                                                                                                                                                                
                                                           +      BaseClass create() { return new BaseClass(this); }                                                                                                                                              
                                                           +  }                                                                                                                                                                                                                
                                                           +3                                                                                                                                               
              const int    DEFAULT_First  = -1        ; +       
              const double DEFAULT_Second = double.NaN; +       
              const string DEFAULT_Third  = null      ; +       
                                                        +3                                                                         
              const int    First  { get; }                =   final int    first ;                                           
              const double Second { get; }                =   final double second;                                       
              const string Third  { get; }                =   final String third ;                                       
                                                                                                                                   
                                                           +  int    first () { return first ; }                            
                                                           +  double second() { return second; }                            
                                                           +  String third () { return third ; }                            
                                                           +3                                                                      
              BaseClass(                                  =   BaseClass(                        
                  int    first  = DEFAULT_First ,         =       BuilderImpl builder                  
                  double second = DEFAULT_Second,       +                                       
                  string third  = DEFAULT_Third ,       +
              ) {                                       +2=   ) {                                       
                  First  = first;                         =       this.first = builder.first ;                                     
                  Second = second;                        =       this.second= builder.second;                                     
                  Third  = third;                         =       this.third = builder.third ;                                     
              }                                           =   }                               
                                                                
              }                                           =   }                                  
                                                                                                
                                                          =12                                                                                                                                 
                                                        +5 +11
                                                           +6
                                                        17  23    
                                                            
              class ChildClass : BaseClass {              =   class ChildClass extends BaseClass {                                      
              
                                                           +  static class BuilderImpl extends BaseClass.BuilderImpl {                                                                    
                                                           +      Object fourth = null;                                                                                                         
                                                           +3 }                                                                                                                                        
                                                                                                                                                                                                       
                                                           +  static class Builder extends BuilderImpl {                                                                                        
                                                           +      ChildClass create() { return new ChildClass(this); }                                                                    
                                                           +3 }
                                                                                                                                                                                                      
              const object DEFAULT_Fourth = null;       +          
                                                        +1    
              const object Fourth { get; }                =   final Object fourth;
              
                                                           +  Object fourth() { return fourth; }
                                                           +1 
              ChildClass (                                =   ChildClass(
                  int    first  = DEFAULT_First ,         =       BuilderImp


              1. Athari
                07.09.2015 02:47

                Вы предлагаете в таком случае дублировать все описания параметров вместе со всеми дефолтными значениями.

                Что-то мешает вынести в отдельную функцию?

                В вашем коде не хватает констант для дефолтных значений — это соответственно увеличит код.

                Тут есть нюанс. Изменение значений по умолчанию — это катаклизм, потому что ломает обратную совместимость. Куда эти значения ни прячь, подобное изменение потенциально ломает весь код. Поэтому значения по умолчанию следует использовать тогда, когда ты уверен, что они не изменятся и ничто не сломают.

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

                Учитывая такую ситуацию, в случае использования трёх-пяти значений я ограничусь копипастой, даже если это не самый красивый и идеологически верный способ. Потому что, если мне захочется менять значения по умолчанию, чего я делать не должен, я и так всё поломаю к чертям, так зачем же платить больше? Мне всё равно нужно проверить абсолютно все использования конструкторов во всём коде, изменение самого конструктора — это сущая мелочь на фоне этого.

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

                Знаете, ваше форматирование в виде

                ChildClass (
                    int    first  = DEFAULT_First ,
                    double second = DEFAULT_Second,
                    string third  = DEFAULT_Third ,
                    object fourth = DEFAULT_Fourth
                ) : base(
                    first ,
                    second,
                    third
                ) {
                    Fourth = fourth;
                }

                Мне сильно напомнило документацию по Win32 в MSDN. Там тоже аргументы в столбик всегда рисуют. :)

                HWND hwnd = CreateWindowEx(
                    0,
                    CLASS_NAME,
                    L"Learn to Program Windows",
                    WS_OVERLAPPEDWINDOW,
                    CW_USEDEFAULT,
                    CW_USEDEFAULT,
                    CW_USEDEFAULT,
                    CW_USEDEFAULT,
                    NULL,
                    NULL,
                    hInstance,
                    NULL
                    );

                Давайте уж тогда исключим форматирование из уравнения и коллапсируем whitespace ("\s+" => " "). Мерять только строчками некорректно, потому что сложность восприятия строчек разной длины разная.

                Сравниваемый код
                C#:

                using static System.Console;
                
                public class CsBaseClass
                {
                    public const int DefaultFirst = -1;
                    public const double DefaultSecond = double.NaN;
                    public const string DefaultThird = null;
                
                    public int First { get; }
                    public double Second { get; }
                    public string Third { get; }
                
                    public CsBaseClass (int first = DefaultFirst, double second = DefaultSecond, string third = DefaultThird)
                    {
                        First = first;
                        Second = second;
                        Third = third;
                    }
                
                    public override string ToString () => $"{GetType().Name} - First: {First}, Second: {Second}, Third: {Third}";
                }
                
                public class CsChildClass : CsBaseClass
                {
                    public const object DefaultFourth = null;
                
                    public object Fourth { get; }
                
                    public CsChildClass (int first = DefaultFirst, double second = DefaultSecond, string third = DefaultThird,
                        object fourth = DefaultFourth)
                        : base(first, second, third)
                    {
                        Fourth = fourth;
                    }
                
                    public override string ToString () => $"{base.ToString()}, Fourth: {Fourth}";
                
                    static void Main ()
                    {
                        WriteLine(new CsBaseClass(first: 1, third: "3"));
                        WriteLine(new CsChildClass(first: 1, third: "3", fourth: 4));
                    }
                }

                Java:

                public class NewBaseClass {
                    protected static class BuilderImpl {
                        public int    first  = -1        ;
                        public double second = Double.NaN;
                        public String third  = null      ;
                    }
                    
                    public static class Builder extends BuilderImpl {
                        public NewBaseClass create() { return new NewBaseClass(this); }
                    }
                    
                    protected final int    first ;
                    protected final double second;
                    protected final String third ;
                    
                    protected NewBaseClass(BuilderImpl builder) {
                        super();
                        this.first = builder.first ;
                        this.second= builder.second;
                        this.third = builder.third ;
                    }
                
                    public int    first () { return first ; }
                    public double second() { return second; }
                    public String third () { return third ; }
                    
                    @Override public String toString() {
                        return "MyClass [first=" + first + ", second=" + second + ", third=" + third + "]";
                    }
                }
                
                public class NewChildClass extends NewBaseClass {
                    protected static class BuilderImpl extends NewBaseClass.BuilderImpl {
                        public Object fourth = null;
                    }
                    
                    public static class Builder extends BuilderImpl {
                        public NewChildClass create() { return new NewChildClass(this); }
                    }
                    
                    protected final Object fourth;
                    
                    protected NewChildClass(BuilderImpl builder) {
                        super(builder);
                        this.fourth = builder.fourth;
                    }
                
                    public Object fourth() { return fourth; }
                
                    @Override public String toString() {
                        return "MyChildClass [first=" + first + ", second=" + second + ", third=" + third + ", fourth=" + fourth + "]";
                    }
                
                    public static void main(String[] args) {
                        System.out.println(new NewBaseClass.Builder(){{ first = 1; third = "3"; }}.create());
                        System.out.println(new NewChildClass.Builder(){{ first = 1; third = "3"; fourth = 4; }}.create());
                    }
                }


                1. speakingfish
                  07.09.2015 17:37

                  Вы предлагаете в таком случае дублировать все описания параметров вместе со всеми дефолтными значениями.

                  Что-то мешает вынести в отдельную функцию?

                  Создание новой функции приведёт к дублированию всего описания параметров. А в случае билдера — нет.

                  Сделал одинаковое форматирование и добавил на github: github.com/speaking-fish/java-sf-builder-simple-example/tree/master/comparison
                  Получилось следующее:
                  Полный код:
                  C#: 41 lines (28 sloc) 1.217 kB
                  Java: 51 lines (35 sloc) 1.616 kB
                  Java +24% lines +25% sloc +32% байт

                  Без toString и main:
                  C#: 30 lines (21 sloc) 0.813 kB
                  Java: 42 lines (29 sloc) 1.13 kB
                  Java +40% lines +38% sloc +39% байт

                  Без полей и getter-ов — рассматриваем только разницу именованные параметры / билдер:
                  C#: 24 lines (17 sloc) 0.674 kB
                  Java: 32 lines (22 sloc) 0.875 kB
                  Java +33% lines +29% sloc +30% байт

                  Выводы: В худшем синтетическом случае кода больше на 40% и то это сокращение в основном из-за getter-ов. В реальном коде увеличение кода даже в таком минимальном варианте (только два класса) не будет более чем на треть.

                  Как я уже сказал, этот код вручную писать необязательно. «Написание» конструктора для класса-потомка у меня свелось к аккорду

                  Обычно надо не только сгенерить код но и поддерживать его.
                  Что будет в случае следующих рефакторингов?:
                  Изменение типа поля.
                  Изменение названия поля.
                  Изменение значения по умолчанию.
                  Перенос поля из одного класса в другой.
                  В случае выделенного объекта билдер это простые контролируемые компилятором операции.
                  А как с параметрами?


                  1. Athari
                    09.09.2015 00:09

                    Создание новой функции приведёт к дублированию всего описания параметров. А в случае билдера — нет.

                    Вообще-то дублирование параметров — неизбежное зло. Вам никогда не приходилось прокидывать аргументы через несколько вызовов по цепочке? Конкретно в этом случае можно выделить обработку конкретно этих двух аргументов в отдельную функцию. Это будет осмысленная функция с нормальным названием и ясным функционалом, поэтому это совершенно нормальное явление.

                    Вы так говорите про дублирование, как будто слово «first» повторяется в джавовом коде реже, чем в шарповом. Пока что во всех версиях на обоих языках дублирования хоть попой кушай. Хоть как-то приближается к избавлению от дублирования только планируемый синтаксис C# 7, и то там одним упоминанием названия невозможно обойтись.

                    Выводы: В худшем синтетическом случае кода больше на 40% и то это сокращение в основном из-за getter-ов. В реальном коде увеличение кода даже в таком минимальном варианте (только два класса) не будет более чем на треть.

                    Для меня 30–50% — это очень даже ощутимое число. И, как я уже сказал, различия в количестве классов даже более важны, чем слоки, байты и непробельные символы.

                    Что будет в случае следующих рефакторингов?

                    Ну, будет работа не на часик, а на два. Вроде, даже решарпер не очень поможет, потому что полноценная обработка цепочек конструкторов не реализована. Фиче-реквест был, но, вроде, до него ещё не добрались.

                    В случае выделенного объекта билдер это простые контролируемые компилятором операции.

                    Я ж уже говорил: это весьма эфемерный бонус. Тип и имя поля-то вы измените, даже сэкономите 50% времени относительно грязной версии с копипастой, но ведь всё равно основное время будет убито на поиск и исправление всех использований во всём коде. Все перечисленные вами изменения ломают обратную совместимость, не получится изменить только одно место.


                    1. speakingfish
                      09.09.2015 19:28

                      Вообще-то дублирование параметров — неизбежное зло. Вам никогда не приходилось прокидывать аргументы через несколько вызовов по цепочке?

                      Нет никакого неизбежного зла — при необходимости часть или весь список параметров превращается в один объект.
                      Зависит от того насколько то или иное удобно.
                      Вы так говорите про дублирование, как будто слово «first» повторяется в джавовом коде реже, чем в шарповом.

                      Слово first встречается только при певром упоминании — в первом объекте и билдере.
                      Хоть как-то приближается к избавлению от дублирования только планируемый синтаксис C# 7

                      Этот сахар опять только для первого объявления а не для всех последующих.
                      Для меня 30–50% — это очень даже ощутимое число. И, как я уже сказал, различия в количестве классов даже более важны, чем слоки, байты и непробельные символы.

                      Там не было 50% — максимум 40%. Количество классов само по себе ничему не вредит — например мы пользуемся в больших количествах анонимными классами просто потому что так удобнее.

                      Что будет в случае следующих рефакторингов?

                      Ну, будет работа не на часик, а на два. Вроде, даже решарпер не очень поможет, потому что полноценная обработка цепочек конструкторов не реализована. Фиче-реквест был, но, вроде, до него ещё не добрались.

                      В случае выделенного объекта билдер это простые контролируемые компилятором операции.

                      Я ж уже говорил: это весьма эфемерный бонус. Тип и имя поля-то вы измените, даже сэкономите 50% времени относительно грязной версии с копипастой, но ведь всё равно основное время будет убито на поиск и исправление всех использований во всём коде. Все перечисленные вами изменения ломают обратную совместимость, не получится изменить только одно место.

                      Т.е. вы отвечаете примерно так: «Да, рефакторинга никакого быть не может. Значит и вам он не нужен.» или что «раз придётся копировать код, то пусть так и будет».
                      Я всё же не столь категоричен, т.к. выбор зависит от ситуации — надо только описать как преимущества так и недостатки обоих подходов.


                      1. Athari
                        09.09.2015 23:32

                        Нет никакого неизбежного зла — при необходимости часть или весь список параметров превращается в один объект.
                        Зависит от того насколько то или иное удобно.

                        Вы будете ради двух-трёх аргументов заводить отдельный класс ради использования один раз?

                        Слово first встречается только при певром упоминании — в первом объекте и билдере.

                        Откройте файл, нажмите Ctrl+F, введите «first» и посчитайте. Вот это всё — дублирование.

                        Количество классов само по себе ничему не вредит

                        Это усложняет восприятие кода. Может быть, вы супермен, и для вас разницы между 2000 и 6000 классов нет, а вот для меня есть.


                        1. speakingfish
                          10.09.2015 00:56

                          Вы будете ради двух-трёх аргументов заводить отдельный класс ради использования один раз?

                          Это усложняет восприятие кода.

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

                          Откройте файл, нажмите Ctrl+F, введите «first» и посчитайте. Вот это всё — дублирование.

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


                          1. Athari
                            10.09.2015 02:45

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

                            Сразу видно, что не последовали совету. :) Посмотрите самую первую версию классов, которую я привел на C# (там, где ни констант, ни билдеров). Слово «first» повторяется 8 раз, да даже слово «fourth» 6 раз (не считая Main). В версии с константами — 11 и 8. С джавой и билдерами — 10 и 8 (без геттеров — 8 и 6). А вот в планируемой версии C# 7 — 2 и 1, и это при том, что функциональности даже больше реализовано (GetHashCode, в частности). Вот это — реально избавление от дублирования. Всё остальное — так, игрушки.

                            Что я хочу сказать: вы видите только одно дублирование — дублирование аргументов. А вообще-то в обоих языках дублирования дофига, и оно совсем в другом месте.

                            Это всё равно что оптимизировать в 10 раз скорость выполнения кода, который отнимает меньше 1% времени. Звучит круто, но бесполезно.


                            1. speakingfish
                              11.09.2015 19:28

                              Без констант будет не эквивалентный код так как дефолтные значения буду размазаны по всем классам.
                              Тогда и без сгенерированных геттеров, сравним — мы же сравниваем только именованные параметры vs builder?:

                              1: public BaseClass(int first = -1...
                              2: First =
                              3:  first;
                              
                              против
                              1: public int first = -1
                              2: protected int first;
                              3: first =
                              4:  src.first;
                              

                              builder проигрывает на одно упоминание в базовом классе
                              1: public ChildClass(int first = -1...
                              2: base(first...
                              
                              против
                              0:
                              

                              в производном builder выигрывает на два упоминания
                              итого builder выигрывает на одно упоминание даже у кода с размазанными по всем классам константами

                              Это всё равно что оптимизировать в 10 раз скорость выполнения кода, который отнимает меньше 1% времени.

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


              1. Athari
                07.09.2015 03:09

                Можно пример?

                class Foo
                {
                    public Foo Foo { get; set; }
                    public List<Foo> FooList { get; } = new List<Foo>();
                    public Dictionary<Foo, Foo> FooDic { get; } = new Dictionary<Foo, Foo>();
                
                    static void Main ()
                    {
                        new Foo {
                            Foo = new Foo {
                                FooList = {
                                    new Foo(),
                                    new Foo()
                                },
                                FooDic = {
                                    [new Foo()] = new Foo(),
                                    [new Foo()] = new Foo()
                                },
                                Foo = new Foo {
                                    FooList = {
                                        new Foo {
                                            FooList = {
                                                new Foo {
                                                    FooList = {
                                                        new Foo()
                                                    }
                                                }
                                            }
                                        }
                                    },
                                    FooDic = {
                                        [new Foo {
                                            Foo = new Foo()
                                        }] = new Foo {
                                            Foo = new Foo()
                                        },
                                        [new Foo {
                                            FooDic = {
                                                [new Foo()] = new Foo()
                                            }
                                        }] = new Foo {
                                            FooDic = {
                                                [new Foo()] = new Foo()
                                            }
                                        }
                                    }
                                }
                            }
                        };
                    }
                }

                Есть ещё возможность к любой коллекции добавить метод Add с произвольными аргументами и добавлять элементы в фигурных скобках, но это в основном используется для словарей, а для них в C# 6 добавили отдельный синтаксис с квадратными скобками. Но вообще фича осталась, иногда полезна.

                Ещё есть нюанс, что в зависимости от того, нужно ли создать объект или просто изменить его, либо используется new, либо не используется. В примере выше объект всегда создаётся в иницилизаторе, а коллекции всегда создаются сами. Но можно и наоборот. new Foo { Foo = { Foo = new Foo() } } тоже будет работать, если свойство не будет null после вызова конструктора (но конкретно в этом случае будет stack overflow).


                1. speakingfish
                  07.09.2015 17:47

                  Конструкция конечно вложенная но инициализации тут одноуровневые.
                  Вот например сначала отдельно инициализируется класс, который потом используется при инициализации Foo
                  new Foo {
                  Foo = new Foo {...}
                  }
                  А это уже похоже:
                  new Foo { Foo = { Foo =… } }
                  А как инициализировать List<List> или Dictionary<Foo, List>?


                  1. Athari
                    09.09.2015 00:21

                    А как инициализировать List<List> или Dictionary<Foo, List>?

                    Как-то так:

                    using System.Collections.Generic;
                    
                    class Foo
                    {
                        static void Main ()
                        {
                            var foos = new List<Dictionary<List<Dictionary<Foo, Foo>>, List<List<Foo>>>> {
                                new Dictionary<List<Dictionary<Foo, Foo>>, List<List<Foo>>> {
                                    [new List<Dictionary<Foo, Foo>> {
                                        new Dictionary<Foo, Foo> {
                                            [new Foo()] = new Foo()
                                        }
                                    }] = new List<List<Foo>> {
                                        new List<Foo> {
                                            new Foo()
                                        }
                                    }
                                }
                            };
                        }
                    }


                    1. speakingfish
                      09.09.2015 20:07

                      Понятно. Но все так же не синтаксис инициализации двумерного массива.


                      1. Athari
                        09.09.2015 23:36

                        Как вы себе это представляете? У коллекций могут быть наследники, поэтому без указания классов не выйдет.


                        1. speakingfish
                          10.09.2015 01:11

                          Да я понял что многоуровневого аналога как для двумерного массива нет
                          Я предполагал что это могло бы выглядеть наподобие:

                          var foos = new List<Dictionary<List<Dictionary<Foo, Foo>>, List<List<Foo>>>> {
                              [
                                  [
                                      [[new Foo() = new Foo()], [new Foo() = new Foo()]] = [[new Foo(), new Foo()], [new Foo(), new Foo()]],
                                      [[], [new Foo() = new Foo()]] = [[], []]
                                  ],
                                  [
                                      [] = [[new Foo(), new Foo()], []],
                                  ];
                              ]
                          };
                          


                          1. Athari
                            10.09.2015 03:01

                            Выглядит ужасающе, чёрт ногу сломит. :) Но от полноценных литералов для списков и словарей я бы не отказался.


                            1. speakingfish
                              11.09.2015 19:29

                              Если компактный код выглядит ужасающие, можно разбавить его комментариями :)


        1. gurinderu
          05.09.2015 10:15

          Я если честно, не совсем понял как приведенный код относится к паттерну builder? Это же вроде обычная инициализация объекта. В Java тоже можно сделать конструкторы и сеттере без дополнительного класса. Паттерн builder нужен же для конструирования сложных объектов, чего нету в вашем примере. Отличным примером является написание query builder'а. Если вы напишите пример и он будет выглядеть много красивее, чем на Java, то я скажу вам огромное спасибо.


          1. Athari
            05.09.2015 12:32
            +1

            Автор под «простым билдером» подразумевает билдер, который просто задаёт значения некоторых свойств, без каких-либо украшений. Такой тривиальный билдер в шарпе «встроенный» за счёт именованных параметров и значений аргументов по умолчанию, в джаве тривиальная «встроенная» версия будет приводить к созданию классов и заглатыванию контекста, поэтому нужен более сложный код.

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


    1. Regis
      04.09.2015 23:52

      На самом деле для использования даже сложных билдеров знание «матана» не нужно, так как это обычно всё вызывается в fluent-стиле и промежуточные шаги нигде явно не пристутствуют.

      Яркий пример использования подобных билдеров — библиотека JOOQ:

      Result<...> query = create.select(BOOK.AUTHOR_ID, BOOK.TITLE)
            .from(BOOK)
            .groupBy(AUTHOR_ID)
            .fetch();
      


  1. DemetrNieA
    05.09.2015 05:18

    >> Для использовании наследования, Builder разделяется на две части (один с полями, другой — с методом создания) следующим образом:
    Так делать не стоит, сейчас объясню почему:

    1. Если Вы используете билдеры и наследование, то оно Вам для чего-либо нужно. И обычно не просто для экономии места при написании переменных, а для того, чтобы переиспользовать билдер:

    List<? extends BaseEntity> entities;
    
    public void addEntity(BaseEntity.Builder builder) {
      entities.add(builder.build());
    }
    

    2. После присвоения базового параметра при такой реализации будет возвращена ссылка на базовый билдер, т.е. нельзя будет сделать такое:
    ExtendedEntity.newBuilder().baseParameter(value).extendedParameter(value2).build();
    

    Это можно обойти хитрым использованием дженериков при наследовании. Могу написать код, если он кому-либо интересен.

    По поводу «Mega Builder» — можете привести пример реализации и использования?


    1. speakingfish
      07.09.2015 01:47

      >> Для использовании наследования, Builder разделяется на две части (один с полями, другой — с методом создания) следующим образом:
      Так делать не стоит, сейчас объясню почему

      Это написано про минимальный билдер, который сооружается из double brace initialization, а он не является fluent-интерфейсом.
      Mega Builder является fluent-интерфейсом, но он не может поддерживать такого сокращённого наследования с помощью пареадресации унаследованному билдеру, поскольку в унаследованном классе могут быть вообще совершенно другие валидные сочетания параметров, и поэтому их надо полностью описывать заново.
      В статье есть все ссылки на описываемый код: github.com/speaking-fish/java-sf-builder-mega github.com/speaking-fish/java-sf-builder-simple-example


  1. Throwable
    06.09.2015 02:59

    В тему: есть прекрасный проект Joda-Beans, который решает многие задачи и исправляет косяки спецификации Java Beans: генерирует автоматически boilerplate code для properties (в том числе и билдеры), добавляет метаданные, equals/hashcode, immutability, etc… Другая хорошая альтернатива — Groovy, которая при необходимости также может генерировать билдеры для объектов.


    1. nehaev
      06.09.2015 07:37
      +3

      > Другая хорошая альтернатива — Groovy, которая при необходимости также может генерировать билдеры для объектов.

      А еще лучше Scala с кейс-классами и статической типизацией.


      1. Throwable
        07.09.2015 11:46

        Scala — это все-таки отдельная область, с собственной системой типов, стандартной библиотекой, экосистемой, и парадигмами, которые имеют лишь определенную совместимость с Java. Смешивать в решении одновременно Java и Scala, довольно нецелесообразно, хотя и можно.

        Groovy же натурально интегрируется с Java, не выходя из ее парадигмы. В данном случае я имел ввиду такой мощный инструмент Groovy как AST. В стандартной библиотеке уже есть гибкие трансформации для создания properties и билдеров. При указании @CompileStatic Groovy AST генерирует код, который 100% интероперабелен с Java и не требует присутствия groovy в рантайме.


        1. grossws
          09.09.2015 00:40
          +1

          Есть ещё project lombok, который используется в compile-time и удобен для описания POJO (умеет генерировать геттеры, сеттеры, toString, hashCode/equals, билдеры). Просто POJO-класс аннотируется и lombok.jar добавляется в classpath.