2016-04-21 3 views
5

Я знаю, что были похожие вопросы. Однако я не видел ответа на свой вопрос.Generic fluent Builder в Java

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

public static class SomeObject<T, S> { 
    public int number; 
    public T singleGeneric; 
    public List<S> listGeneric; 

    public SomeObject(int number, T singleGeneric, List<S> listGeneric) { 
     this.number = number; 
     this.singleGeneric = singleGeneric; 
     this.listGeneric = listGeneric; 
    } 
} 

Я хотел бы построить его с беглым синтаксиса Builder. Я бы хотел сделать это элегантно. Я хочу это работало так:

SomeObject<String, Integer> works = new Builder() // not generic yet! 
    .withNumber(4) 

    // and only here we get "lifted"; 
    // since now it's set on the Integer type for the list 
    .withList(new ArrayList<Integer>()) 

    // and the decision to go with String type for the single value 
    // is made here: 
    .withTyped("something") 

    // we've gathered all the type info along the way 
    .create(); 

Нет небезопасного предупреждения литого и нет необходимости заранее указать общие типы (в верхней части, где строятся Builder).

Вместо этого мы предоставляем информацию о типе в явном виде, далее вниз по цепочке - вместе с вызовами withList и withTyped.

Теперь, что было бы самым элегантным способом его достижения?

Мне известны самые распространенные трюки, такие как использование recursive generics, но я немного поиграл с ним и не мог понять, как это относится к этому прецеденту.

Ниже мирское многословное решение, которое работает в смысле удовлетворения всех требований, но за счет большого многословия - это вводит четыре строителей (не связанные с точкой зрения наследования), представляющее четыре возможных комбинациями T и S типов, являющимися определяется или нет.

Это действительно работает, но это вряд ли версия, которой можно гордиться, и недостижимо, если бы мы ожидали более общих параметров, чем два.

public static class Builder { 
    private int number; 

    public Builder withNumber(int number) { 
     this.number = number; 
     return this; 
    } 

    public <T> TypedBuilder<T> withTyped(T t) { 
     return new TypedBuilder<T>() 
       .withNumber(this.number) 
       .withTyped(t); 
    } 

    public <S> TypedListBuilder<S> withList(List<S> list) { 
     return new TypedListBuilder<S>() 
       .withNumber(number) 
       .withList(list); 
    } 
} 

public static class TypedListBuilder<S> { 
    private int number; 
    private List<S> list; 

    public TypedListBuilder<S> withList(List<S> list) { 
     this.list = list; 
     return this; 
    } 

    public <T> TypedBothBuilder<T, S> withTyped(T t) { 
     return new TypedBothBuilder<T, S>() 
       .withList(list) 
       .withNumber(number) 
       .withTyped(t); 
    } 

    public TypedListBuilder<S> withNumber(int number) { 
     this.number = number; 
     return this; 
    } 
} 

public static class TypedBothBuilder<T, S> { 
    private int number; 
    private List<S> list; 
    private T typed; 

    public TypedBothBuilder<T, S> withList(List<S> list) { 
     this.list = list; 
     return this; 
    } 

    public TypedBothBuilder<T, S> withTyped(T t) { 
     this.typed = t; 
     return this; 
    } 

    public TypedBothBuilder<T, S> withNumber(int number) { 
     this.number = number; 
     return this; 
    } 

    public SomeObject<T, S> create() { 
     return new SomeObject<>(number, typed, list); 
    } 
} 

public static class TypedBuilder<T> { 
    private int number; 
    private T typed; 

    private Builder builder = new Builder(); 

    public TypedBuilder<T> withNumber(int value) { 
     this.number = value; 
     return this; 
    } 

    public TypedBuilder<T> withTyped(T t) { 
     typed = t; 
     return this; 
    } 

    public <S> TypedBothBuilder<T, S> withList(List<S> list) { 
     return new TypedBothBuilder<T, S>() 
       .withNumber(number) 
       .withTyped(typed) 
       .withList(list); 
    } 
} 

Есть ли более умная техника, которую я мог бы применить?

+0

* «недопустимый, если бы мы ожидали более общих параметров, чем только два». * Если вы хотите сохранить произвольный порядок (в вашем примере вы можете делать как с помощью typed (...). WithList (...) '* и * 'withList (...). withTyped (...)'), тогда проблема становится очень трудной, потому что вы получаете что-то вроде классов 'n!', где 'n' - количество параметров типа. Если вы примете более традиционный подход к созданию шага, то это немного проще. – Radiodef

+1

@Radiodef Я думал, что 2^n классов: в любой точке каждый родовой тип может находиться в одном из двух состояний: уже определен или еще не определен. Но да, это серьезный недостаток этой «ручной» реализации. Вот почему мне интересно, существует ли лучшее решение; возможно, используя общие ограничения каким-то умным способом. –

ответ

5

Хорошо, поэтому более традиционный подход к построению шага будет таким.

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

Основная идея: определить каждый шаг на интерфейсе, а затем реализовать их все в частном классе. Мы можем сделать это с помощью общих интерфейсов, наследуя их исходные типы. Это уродливо, но оно работает.

public interface NumberStep { 
    NumberStep withNumber(int number); 
} 
public interface NeitherDoneStep extends NumberStep { 
    @Override NeitherDoneStep withNumber(int number); 
    <T> TypeDoneStep<T> withTyped(T type); 
    <S> ListDoneStep<S> withList(List<S> list); 
} 
public interface TypeDoneStep<T> extends NumberStep { 
    @Override TypeDoneStep<T> withNumber(int number); 
    TypeDoneStep<T> withTyped(T type); 
    <S> BothDoneStep<T, S> withList(List<S> list); 
} 
public interface ListDoneStep<S> extends NumberStep { 
    @Override ListDoneStep<S> withNumber(int number); 
    <T> BothDoneStep<T, S> withTyped(T type); 
    ListDoneStep<S> withList(List<S> list); 
} 
public interface BothDoneStep<T, S> extends NumberStep { 
    @Override BothDoneStep<T, S> withNumber(int number); 
    BothDoneStep<T, S> withTyped(T type); 
    BothDoneStep<T, S> withList(List<S> list); 
    SomeObject<T, S> create(); 
} 
@SuppressWarnings({"rawtypes","unchecked"}) 
private static final class BuilderImpl implements NeitherDoneStep, TypeDoneStep, ListDoneStep, BothDoneStep { 
    private final int number; 
    private final Object typed; 
    private final List list; 

    private BuilderImpl(int number, Object typed, List list) { 
     this.number = number; 
     this.typed = typed; 
     this.list = list; 
    } 

    @Override 
    public BuilderImpl withNumber(int number) { 
     return new BuilderImpl(number, this.typed, this.list); 
    } 

    @Override 
    public BuilderImpl withTyped(Object typed) { 
     // we could return 'this' at the risk of heap pollution 
     return new BuilderImpl(this.number, typed, this.list); 
    } 

    @Override 
    public BuilderImpl withList(List list) { 
     // we could return 'this' at the risk of heap pollution 
     return new BuilderImpl(this.number, this.typed, list); 
    } 

    @Override 
    public SomeObject create() { 
     return new SomeObject(number, typed, list); 
    } 
} 

// static factory 
public static NeitherDoneStep builder() { 
    return new BuilderImpl(0, null, null); 
} 

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

В противном случае он работает почти так же, как ваша собственная идея:

SomeObject<String, Integer> works = 
    SomeObject.builder() 
     .withNumber(4) 
     .withList(new ArrayList<Integer>()) 
     .withTyped("something") 
     .create(); 

// we could return 'this' at the risk of heap pollution

Что это? Хорошо, так что проблема вообще здесь, и это так:

NeitherDoneStep step = SomeObject.builder(); 
BothDoneStep<String, Integer> both = 
    step.withTyped("abc") 
     .withList(Arrays.asList(123)); 
// setting 'typed' to an Integer when 
// we already set it to a String 
step.withTyped(123); 
SomeObject<String, Integer> oops = both.create(); 

Если мы не создавали копии, мы теперь имеем 123 маскировку вокруг как String.

(Если вы используете только строитель, как беглый набор вызовов, это не может произойти.)

Хотя нам не нужно, чтобы сделать копию для withNumber, я просто пошел дополнительный шаг и сделал строителя неизменным. Мы создаем больше объектов, чем нужно, но на самом деле нет другого хорошего решения. Если все будут правильно использовать строитель, мы можем сделать его изменчивым и return this.


Поскольку нас интересуют новые родовые решения, здесь реализована реализация строителя в одном классе.

Разница заключается в том, что мы не сохраняем типы typed и list, если мы будем ссылаться на их установочные установки во второй раз. На самом деле это не недостаток, это просто разные, я думаю. Это означает, что мы можем сделать это:

SomeObject<Long, String> = 
    SomeObject.builder() 
     .withType(new Integer(1)) 
     .withList(Arrays.asList("abc","def")) 
     .withType(new Long(1L)) // <-- changing T here 
     .create(); 
public static class OneBuilder<T, S> { 
    private final int number; 
    private final T typed; 
    private final List<S> list; 

    private OneBuilder(int number, T typed, List<S> list) { 
     this.number = number; 
     this.typed = typed; 
     this.list = list; 
    } 

    public OneBuilder<T, S> withNumber(int number) { 
     return new OneBuilder<T, S>(number, this.typed, this.list); 
    } 

    public <TR> OneBuilder<TR, S> withTyped(TR typed) { 
     // we could return 'this' at the risk of heap pollution 
     return new OneBuilder<TR, S>(this.number, typed, this.list); 
    } 

    public <SR> OneBuilder<T, SR> withList(List<SR> list) { 
     // we could return 'this' at the risk of heap pollution 
     return new OneBuilder<T, SR>(this.number, this.typed, list); 
    } 

    public SomeObject<T, S> create() { 
     return new SomeObject<T, S>(number, typed, list); 
    } 
} 

// As a side note, 
// we could return e.g. <?, ?> here if we wanted to restrict 
// the return type of create() in the case that somebody 
// calls it immediately. 
// The type arguments we specify here are just whatever 
// we want create() to return before withTyped(...) and 
// withList(...) are each called at least once. 
public static OneBuilder<Object, Object> builder() { 
    return new OneBuilder<Object, Object>(0, null, null); 
} 

То же самое и о создании копий и загрязнения кучи.


Теперь мы получаем действительно роман. Идея здесь заключается в том, что мы можем «отключить» каждый метод, вызывая ошибку преобразования захвата.

Это немного сложно объяснить, но основная идея:

  • Каждый метод каким-то образом зависит от переменной типа, который объявлен в классе.
  • «Отключить» этот метод, указав тип возвращаемого значения, чтобы этот тип изменялся до ?.
  • Это приводит к ошибке преобразования захвата, если мы попытаемся вызвать метод для этого возвращаемого значения.

Разница между этим примером и предыдущим примером является то, что если мы будем пытаться называть сеттер во второй раз, мы получим ошибку компиляции:

SomeObject<Long, String> = 
    SomeObject.builder() 
     .withType(new Integer(1)) 
     .withList(Arrays.asList("abc","def")) 
     .withType(new Long(1L)) // <-- compiler error here 
     .create(); 

Таким образом, мы можем назвать только каждый сеттер один раз.

Две основные недостатки здесь, что вы:

  • нельзя назвать сеттеров во второй раз для законных причин
  • и можете сеттеры вызова второй раз с null буквальным.

Я думаю, что это довольно интересное доказательство концепции, даже если это немного непрактично.

public static class OneBuilder<T, S, TCAP, SCAP> { 
    private final int number; 
    private final T typed; 
    private final List<S> list; 

    private OneBuilder(int number, T typed, List<S> list) { 
     this.number = number; 
     this.typed = typed; 
     this.list = list; 
    } 

    public OneBuilder<T, S, TCAP, SCAP> withNumber(int number) { 
     return new OneBuilder<T, S, TCAP, SCAP>(number, this.typed, this.list); 
    } 

    public <TR extends TCAP> OneBuilder<TR, S, ?, SCAP> withTyped(TR typed) { 
     // we could return 'this' at the risk of heap pollution 
     return new OneBuilder<TR, S, TCAP, SCAP>(this.number, typed, this.list); 
    } 

    public <SR extends SCAP> OneBuilder<T, SR, TCAP, ?> withList(List<SR> list) { 
     // we could return 'this' at the risk of heap pollution 
     return new OneBuilder<T, SR, TCAP, SCAP>(this.number, this.typed, list); 
    } 

    public SomeObject<T, S> create() { 
     return new SomeObject<T, S>(number, typed, list); 
    } 
} 

// Same thing as the previous example, 
// we could return <?, ?, Object, Object> if we wanted 
// to restrict the return type of create() in the case 
// that someone called it immediately. 
// (The type arguments to TCAP and SCAP should stay 
// Object because they are the initial bound of TR and SR.) 
public static OneBuilder<Object, Object, Object, Object> builder() { 
    return new OneBuilder<Object, Object, Object, Object>(0, null, null); 
} 

Опять же о создании копий и загрязнения кучи.


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

Если вас вообще интересует такая вещь, я рекомендую учиться code generation with annotation processing, потому что вы можете создавать такие вещи намного проще, чем писать их вручную. Как мы говорили в комментариях, запись таких вещей вручную становится нереалистичной довольно быстро.

+0

Отключение сеттеров довольно круто, никогда не видел этого раньше! – AdamSkywalker

+0

Это более полный ответ, чем я ожидал! Очень проницательный. Большое спасибо. –

Смежные вопросы