Правильный полиморфный билдер на Java

О чем все это?


При реализации chained builder на Java все прекрасно, пока не понадобится добавить наследование. Сразу же возникают две проблемы — как сделать, чтобы методы родительского билдера возвращали объект дочернего билдера и как передавать дочерний билдер в функции, принимающие родительский. Предлагается реализация паттерна, которая позволяет решить обе проблемы. Исходники можно посмотреть здесь на гитхабе


Upd. Реальная проблема


В приложении есть dto-объекты для отображения результата, которые строятся следующим образом:
1) создается билдер нужного dto-объекта
2) билдер передается в различные классы по цепочке, каждый класс использует билдер для установки нужных ему полей
В один прекрасный день решили ввести новую версию API, dto-объект был расширен с помощью наследования и тут оказалось, что его билдер не получается засунуть в существующую цепочку классов для достройки.

Постановка задачи


Тут должно быть 100500 слов о важности паттерна билдер, дабы не утомлять читателя этой лабудой, сразу перейдем к делу. Пусть есть 3 класса с понятными именами Gen1, Gen2 и Gen3. Они образуют линейную иерархию Gen3 → Gen2 → Gen1. Каждый их них содержит ровно один очень важный метод с именем setValX (где X цифра из имени класса). Мы хотим получить билдеры Builder1, Builder2, Builder3 каждый из которых содержит соответствующий метод valX, который реализуется только у одного класса (не хотим копипастить).
Так же должны работать цепочки:
Gen1 gen1 = builder1.val1("val1").build();
Gen2 gen2 = builder2.val1("val1").val2("val2").build();
Gen3 gen3 = builder3.val1("val1").val2("val2").val3("val3").build();

и возможность использовать дочерние билдеры вместо родительских:
Gen1 someFunction(Builder1 builder1) {
    return builder1.val1("val1111");
}
...
someFunction1(builder3.val2("val222").val3("val333"));

Что и как получилось


Билдер предполагается сделать по следующей схеме — создать объект в самом начале, потом заполнить его поля, а в функции build () вернуть клиенту. В этом случае нам необходим класс, который будет делать простую вещь — хранить ссылку на достраиваемый объект и хранить ссылку на тот билдер нужного типа, который будут возвращать все методы-установщики значений. Следующий класс решает проблему:
public class BuilderImpl  {
    protected T nested;
    RetBuilder returnBuilder;

    protected BuilderImpl(T child) {
        nested = child;
    }

    protected T getNested() {
        return nested;
    }

    protected void injectReturnBuilder(RetBuilder builder) {
        returnBuilder = builder;
    }

    protected RetBuilder self() {
        return returnBuilder;
    }

    public T build() {
        return nested;
    }
}

конечно, от метода injectReturnBuilder лучше было бы избавиться, передавая нужные данные в конструктор, но увы, туда будет передаваться this дочернего билдера, который нельзя использовать до окончания родительского конструктора super (). Метод getNested () на любителя, можно обращаться к полю nested напрямую. Метод self () сделан, чтобы не путать поле со словом this.

Теперь задумаемся вот над какой проблемой. Если у нас есть некий генерик Builder1<> который реализуется все что нам нужно для класса Gen1 (с какими-то параметрами Gen1, Builder1), на нужно будет унаследовать от него генерик Builder2 для Gen2 (с какими-то параметрами Gen1, Builder1), а от того Builder3 для Gen3 то получится, что у Builder3 в предках две реализации исходного Builder1 с разными параметрами, что, увы прямо запрещено Java.

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

Отдельную трудность представляет написание InnerBuilderX с правильной комбинацией wildcard. Путем долгих проб и ошибок (читать спецификации не наш путь) был найдет приемлемый вариант. Но пока он был найден — были перепробованы комбинации, на который инспектор Idea помирал или ошибался, что несколько затормозило разработку. И так, вот код для InnerBuilder1 класса Gen1. Параметр T это тип хранящегося объекта, RetBuilder — тип билдера, который возращается из функции установки val1.

public static class InnerBuilder1> 
extends BuilderImpl {

        protected InnerBuilder1(T created) {
            super(created);
        }

        public RetBuilder val1(String val) {
            getNested().setVal1(val);
            return self();
        }
    }

Конечно, рекурсивная конструкция class InnerBuilder1> немного напрягает, но зато реально работает.

Ну, а FinalBuilder довольно простой:

private static class FinalBuilder1 extends InnerBuilder1 {

        private FinalBuilder1() {
            super(new Gen1()); // сюда нельзя this
            injectReturnBuilder(this);
        }
    }

Осталось добавить статическую функцию по созданию билдера

public static InnerBuilder1 builder() {
        return new FinalBuilder1();
}

Теперь перейдем к дочернему билдеру. Нам унаследовать реализацию для внутреннего билдера и сделать создание объекта в финальном:

public static InnerBuilder2 builder() {
        return new FinalBuilder2();
    }


    public static class InnerBuilder2> extends InnerBuilder1 {

        protected InnerBuilder2(T created) {
            super(created);
        }

        public RetBuilder val2(String val) {
            getNested().setVal2(val);
            return self();
        }
    }

    private static class FinalBuilder2 extends InnerBuilder2 {

        private FinalBuilder2() {
            super(new Gen2());
            injectReturnBuilder(this);
        }
    }

можно попробовать скомпилировать тестовый код:


Gen2.builder().val1("111").val1("111").val1("111").val1("111").val2("222").build();

Получилось! А что там с полиморфизмом?

 //  принимает билдер передкового Gen1
 Gen1 buildGen1Final(Gen1.InnerBuilder1 builder) {
        builder.val1("set value from Gen1 builder");
        return builder.build();
    }
...
// а получает билдер потомка Gen2
 buildGen1Final(
        Gen2.builder().val2("set value from Gen2 builder")
 );

Все тоже работает. Аналогично реализуется билдер для класс Gen3, за подробностями можно обратится в гитхаб

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

  • 13 ноября 2016 в 19:26

    0

    Слишком абстрактно и похоже на overengineering. Не помешал бы пример из реальной жизни. Зачем это нужно.
    • 13 ноября 2016 в 19:53

      0

      добавил секцию про реальную задачу

© Habrahabr.ru