Фабрика Строителей, возвращающая различные подчиненные интерфейсы
Я знаю, что есть много вариантов и связанных с этим тем здесь, связанных с переполнением стека, но я не нашел убедительных ответов, поэтому я сам попробую.
Я пытаюсь спроектировать фабрику компоновщиков, которая возвращает разные подклассы общего интерфейса компоновщика. Я хочу разрешить всем реализациям использовать общий абстрактный класс для повторного использования кода.
Обратите внимание, что меня не интересует тип возвращаемого значения build()
метод, только то, что типы строителей.
Это то, что я до сих пор:
Интерфейс Builder с универсальным для подчиненных интерфейсов:
interface FruitBuilder<T extends FruitBuilder<T>> {
T taste(String taste);
T shape(String shape);
T weight(String weight);
Fruit build();
}
У некоторых строителей есть дополнительные методы:
interface GrapesBuilder extends FruitBuilder<GrapeBuilder> {
GrapesBuilder clusterSize(int clusterSize);
}
Далее необходимо указать фабрику, которая возвращает конкретных сборщиков:
interface FruitBuilderFactory {
GrapesBuilder grapes();
AppleBuilder apple();
LemonBuilder lemon();
}
Пользователь этих интерфейсов должен иметь возможность использовать его следующим образом:
Fruit grapes = fruitBuilderFactory
.grapes()
.weight(4)
.color("Purple")
.clusterSize(4) // Note that the GrapesBuilder type must be accessible here!
.build();
Большая часть логики перейдет в абстрактный класс, включая расширенную логику сборки:
abstract class BaseFruitBuilder<T extends FruitBuilder<T>> implements FruitBuilder<T> {
String taste;
T taste(String taste) {
this.taste = taste;
return (T)this; // Ugly cast!!!!!
}
...
Fruit build() {
Fruit fruit = createSpecificInstance();
// Do a lot of stuff on the fruit instance.
return fruit;
}
protected abstract Fruit createSpecificInstance();
}
Учитывая базовый класс, реализовать новые компоновщики очень просто:
class GrapseBuilderImpl extends BaseFruitBuilder<GrapesBuilder> {
int clusterSize;
GrapesBuilder clusterSize(int clusterSize) {
this.clusterSize = clusterSize;
}
protected Fruit createSpecificInstance() {
return new Grape(clusterSize);
}
}
Это все компилируется и хорошо (по крайней мере, мой настоящий код). Вопрос, могу ли я убрать уродливое приведение к T в абстрактном классе.
2 ответа
Одним из способов избежать приведения является определение одного абстрактного метода, возвращающего T
:
abstract class BaseFruitBuilder<T extends FruitBuilder<T>> implements FruitBuilder<T> {
String taste;
T taste(String taste) {
this.taste = taste;
return returnThis();
}
protected abstract T returnThis();
//...
}
class GrapseBuilderImpl extends BaseFruitBuilder<GrapesBuilder> {
//...
@Override
protected T returnThis() {
return this;
}
}
Недостатком является то, что вы должны доверять каждому подклассу для правильной реализации метода. Опять же, с вашим подходом ничто не мешает объявить подкласс GrapesBuilder extends BaseFruitBuilder<AppleBuilder>
, так что вам нужно до некоторой степени доверять подклассам.
РЕДАКТИРОВАТЬ Только что понял, что на это решение ссылается комментарий@user158037. Я использовал это сам, но так и не понял, что это известная идиома.:-)
Вы используете то, что обычно называют само-типами, но кажется, что T
относится к. У тебя есть FruitBuilder
интерфейс, который имеет универсальный тип T
, который должен представлять тип, который вернет строитель. Вместо этого вы, похоже, используете его для представления типа самого компоновщика, что, скорее всего, не нужно (если это так, см. Ниже более сложное предложение).
Будьте осторожны и намеренны с дженериками; тот факт, что они являются абстрактными понятиями, позволяет легко их спутать. В вашем случае я бы предложил следующий интерфейс:
interface FruitBuilder<F extends Fruit> {
FruitBuilder<F> taste(...);
FruitBuilder<F> shape(...);
FruitBuilder<F> weight(...);
F build();
}
Отдельные сборщики теперь объявляют тип, который они будут в конечном итоге строить, а не свой собственный тип:
interface FruitBuilderFactory {
GrapesBuilder grapes(); // define a concrete subtype to add methods
FruitBuilder<Apple> apple();
}
А теперь каждый FruitBuilder
явно является строителем F
экземпляры, и каждый из ваших методов конструктора может чисто вернуть this
и ваш build()
Метод вернет объект ожидаемого универсального типа, что позволит вам написать что-то вроде:
Grape grape = FruitBuilderFactory.grape()....build();
Для большинства шаблонов подклассов это все, что вам нужно. Даже если вам нужно определить дополнительные методы построения для определенных типов, вы все равно можете использовать эту структуру. Рассмотрим гуавы ImmutableCollection.Builder<E>
и ImmutableMultiset.Builder<E>
подтип, который расширяет его и предоставляет дополнительные методы. Также обратите внимание, что они связывают компоновщик напрямую с типом, а не с общим классом компоновки компоновщика. Попробуйте воспроизвести эту структуру самостоятельно (например, Grape.Builder
, Apple.Builder
, так далее.).
В редких случаях вам нужно использовать собственные типы и представлять конструктор как общий тип. Это создает сложную структуру типа, но на практике обнаруживается в некоторых местах, таких как Истина Subject<S extends Subject<S,T>,T>
(который имеет значительно более сложную семантику, чем большинство строителей). Обратите внимание, что у него есть два дженерика; S
представляет себя в то время как T
представляет тип, с которым фактически работает. Если твой FruitBuilder
Нужно быть таким комплексом, вы бы использовали аналогичный шаблон:
interface FruitBuilder<B extends FruitBuilder<B, F>, F> {
B taste(...);
B shape(...);
B weight(...);
F build();
}