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

Про що це?

При реалізації прикутий 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<T, RetBuilder> {
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<T extends Gen1, RetBuilder extends InnerBuilder1<? extends T, ?>> 
extends BuilderImpl<T, RetBuilder> {

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

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

Звичайно, рекурсивна конструкція class InnerBuilder1<T extends Gen1, RetBuilder extends InnerBuilder1<? extends T, ?>> трохи напружує, але зате реально працює.

Ну а FinalBuilder досить простий:
private static class FinalBuilder1 extends InnerBuilder1<Gen1, FinalBuilder1> {

private FinalBuilder1() {
super(new Gen1()); // сюди не можна this
injectReturnBuilder(this);
}
}


Залишилося додати статичну функцію по створенню білдера

public static InnerBuilder1<? extends Gen1, ?> builder() {
return new FinalBuilder1();
}


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

public static InnerBuilder2<? extends Gen2, ?> builder() {
return new FinalBuilder2();
}


public static class InnerBuilder2<T extends Gen2, RetBuilder extends InnerBuilder2<? extends T,?>> extends InnerBuilder1<T, RetBuilder> {

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

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

private static class FinalBuilder2 extends InnerBuilder2<Gen2, FinalBuilder2> {

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<? extends Gen1, ?> builder) {
builder.val1("set value from Gen1 builder");
return builder.build();
}
...
// а отримує білдер нащадка Gen2
buildGen1Final(
Gen2.builder().val2("set value from Gen2 builder")
);

Все теж працює. Аналогічно реалізується білдер клас для Gen3, за подробицями можна звернутися в гитхаб
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.