Builder Factory возвращает разные субинтерфейсы

Я знаю, что здесь есть много вариантов и связанных тем, связанных с переполнением стека, но я не нашел убедительных ответов, поэтому я сам попробую.

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

Обратите внимание, что меня не интересует тип возвращаемого значения метода 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 в абстрактном классе.


person Johan Tidén    schedule 29.09.2015    source источник
comment
Это не очень уродливо :) T намеревается быть типом this, так что (T)this прекрасно. Я бы предпочел использовать This в качестве имени переменной типа, поэтому (This)this выглядит еще разумнее. Однако есть выход, если вы действительно ненавидите актерский состав; но я не думаю, что это стоит усилий.   -  person ZhongYu    schedule 29.09.2015
comment
Если все реализации имеют одни и те же свойства, почему вы не можете просто создать класс BuilderParameters (конкретный неуниверсальный неокончательный класс), который вы затем вводите в свой конструктор? Код клиента будет примерно таким: Fruit grapes = fruitBuilderFactory.grapes().build(GrapesBuilderParams.newInstance().weight(...).color(...).clusterSize(...)). Сделав это, вы устранили бы всю универсальность из параметров (чтобы больше не было приведений) и очень мало потеряли бы со стороны настраиваемости (т.е. построители не могут зависеть от порядка инициализации параметра, потому что они этого не знают)   -  person Giulio Franco    schedule 29.09.2015
comment
Может быть, уловка с getThis () сработает?   -  person user158037    schedule 16.10.2015


Ответы (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 комментарий. Я сам использовал это, но никогда не понимал, что это известная идиома. :-)

person shmosel    schedule 10.07.2016
comment
Спасибо, это может быть чище (и, надеюсь, немного безопаснее), чем литье. Я согласен с другими комментариями, что это (немного) затруднит расширение строителей, особенно подклассов уже конкретных строителей. - person Johan Tidén; 11.07.2016

Вы используете то, что обычно называют самотипами, но, кажется, несколько сбивает с толку то, что означает 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();

Для большинства шаблонов строителей подклассов это все, что вам нужно. Даже если вам нужно определить дополнительные методы построения для определенных типов, вы все равно можете использовать эту структуру. Рассмотрим _11 _ и ImmutableMultiset.Builder<E>, расширяющий его и предоставляющий дополнительные методы. Также обратите внимание, что они связывают построитель напрямую с типом, а не с общим классом построителя-фабрики. Подумайте о том, чтобы воспроизвести эту структуру самостоятельно (например, Grape.Builder, Apple.Builder и т. Д.).


В редких случаях вам действительно нужно использовать собственные типы и также представлять построитель как универсальный тип. Это создает сложную структуру типов, но на практике проявляется в определенных местах, таких как Truth _ 15_ (который имеет значительно более сложную семантику, чем большинство строители). Обратите внимание, что у него есть два универсальных шаблона; S представляет себя, а T представляет тип, с которым действительно работают. Если ваш FruitBuilder должен быть таким сложным, вы должны использовать аналогичный шаблон:

interface FruitBuilder<B extends FruitBuilder<B, F>, F> {
  B taste(...);
  B shape(...);
  B weight(...);
  F build();
}
person dimo414    schedule 10.07.2016
comment
Guava должен переопределить каждый метод в каждом подклассе, чтобы вернуть текущий тип. Не очень элегантно. - person shmosel; 10.07.2016
comment
@shmosel - это дизайнерское решение. Поскольку существует ограниченное количество реализаций, а построители предназначены для упрощения работы вызывающим абонентам, он поможет скрыть неэлегантность реализации. Использование self-type переложит некоторую нагрузку на вызывающих абонентов, что, на мой взгляд, было бы гораздо менее элегантно. Обратите внимание, например, на подпись типа, необходимую для создания _ 1_. Это работает для варианта использования Truth, но со сборщиками жизнь вызывающим абонентам будет тяжелее. - person dimo414; 10.07.2016
comment
Каким образом селф-тип усложняет вызов вызывающим абонентам? - person shmosel; 10.07.2016
comment
@shmosel, если самотипизированный экземпляр должен быть назначен переменной, вызывающий должен правильно объявить этот тип, что в худшем случае сложно, а в лучшем случае просто лишний тип. Это не проблема для IterableSubject, поскольку он предназначен для плавного использования (поэтому назначается редко), но принуждение вызывающих абонентов к объявлению типа, подобного IterableSubject<? extends IterableSubject<?,T,C>,T,C>, было бы проблемой. Как я уже сказал, это дизайнерское решение, поэтому вы можете привести веские аргументы в пользу любого варианта, но на практике вам, пользователю, не мешает решение Guava избегать самонабора. - person dimo414; 10.07.2016
comment
Большинство построителей предназначены для плавного использования, и их обычно можно назначить по их конкретному типу (в противном случае они не смогли бы добавлять методы). - person shmosel; 10.07.2016
comment
@shmosel да, но есть много случаев, когда построители удерживаются дольше одного выражения, например, если вам нужно добавить значения в цикл. Я не уверен, о чем вы говорите; вы можете использовать собственные типы, но часто в этом не нуждаются, а подписи типов, которые избегают самотипов, проще рассуждать. Что-нибудь из этого вам не нравится? Вы всегда можете напрямую спросить у разработчиков Guava, почему они считают себя типами были нежелательны в данном конкретном случае. - person dimo414; 10.07.2016
comment
Вместо этого вы, кажется, используете его для представления типа самого строителя, что, вероятно, не является необходимым. Вся предпосылка вопроса заключается в том, что тип построителя необходим, а тип возвращаемого значения - нет. - person Johan Tidén; 11.07.2016
comment
И в конце своего ответа я описываю, как правильно структурировать самотипизированный конструктор. - person dimo414; 11.07.2016