Pull to refresh

Самый простой и самый сложный Builder на Java

Reading time9 min
Views38K


Один из часто рассматриваемых паттернов — паттерн 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 — все остальные варианты находятся где-то между ними.
Tags:
Hubs:
Total votes 21: ↑20 and ↓1+19
Comments47

Articles