2016-04-21 14 views
5

Sono consapevole che ci sono state domande simili. Non ho visto una risposta alla mia domanda però.Generatore fluente generico in Java

Presenterò quello che voglio con un codice semplificato. Dire che ho un oggetto complesso, alcuni dei suoi valori sono generici:

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; 
    } 
} 

vorrei costruirlo con la sintassi Builder fluente. Mi piacerebbe renderlo elegante però. Vorrei che ha funzionato così:

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(); 

Nessun allarme del cast non sicuri, e non c'è bisogno di specificare i tipi generici in anticipo (in alto, in cui è costruito Builder).

Invece, lasciamo che le informazioni sul tipo scorrano in modo esplicito, più in basso lungo la catena - insieme alle chiamate withList e withTyped.

Ora, quale sarebbe il modo più elegante per raggiungerlo?

Sono a conoscenza dei trucchi più comuni, come l'uso di recursive generics, ma l'ho usato per un po 'e non riuscivo a capire come si applica a questo caso d'uso.

Qui di seguito è una soluzione verbose mondano che opera nel senso di soddisfare tutte le esigenze, ma a costo di grandi verbosità - introduce quattro costruttori (non correlato in termini di eredità), che rappresentano quattro possibili combinazioni di T e S tipo, in quanto definito o no.

Funziona, ma non è una versione di cui essere orgogliosi e non mantenibile se ci aspettassimo più parametri generici di due.

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); 
    } 
} 

Esiste una tecnica più intelligente che potrei applicare?

+0

* "impossibile da mantenere se ci si aspettassero parametri più generici di solo due" * Se si desidera mantenere l'ordinamento arbitrario (nel proprio esempio, è possibile eseguire sia con withTyped (...). WithList (...) '* e * 'withList (...). withTyped (...)') allora il problema diventa davvero difficile perché si finisce con qualcosa come 'n!' classi, dove 'n' è il numero di parametri di tipo. Se si segue un approccio passo-passo più tradizionale, allora è un po 'più semplice. – Radiodef

+1

@Radiodef Ho pensato alle classi 2^n: in qualsiasi momento, ogni tipo generico può essere in uno dei due stati: già definito o non ancora definito. Ma sì, questo è un grave inconveniente di quell'implementazione "manuale". Ecco perché mi chiedo se esista una soluzione migliore; forse sfruttando i vincoli generici in modo intelligente. –

risposta

5

Ok, quindi l'approccio più tradizionale di step-builder sarebbe qualcosa del genere.

Sfortunatamente, poiché stiamo mescolando metodi generici e non generici, dobbiamo ridichiarare molti metodi. Io non rispondo allo allo.

L'idea di base è la seguente: definire ogni passaggio su un'interfaccia, quindi implementarli tutti nella classe privata. Possiamo farlo con interfacce generiche ereditando dai loro tipi non elaborati. È brutto, ma funziona.

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); 
} 

Dal momento che non vogliamo che la gente l'accesso alla brutta implementazione, renderlo privato e rendere tutti passare attraverso un metodo static.

In caso contrario, funziona più o meno lo stesso come la propria idea:

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

cosa si tratta? Va bene, quindi non c'è un problema in generale qui, ed è in questo modo:

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(); 

Se non abbiamo creato copie, avremmo ora abbiamo 123 mascherato in giro come un String.

(Se si sta utilizzando solo il costruttore come l'insieme fluente di chiamate, questo non può accadere.)

Anche se non abbiamo bisogno di fare una copia per withNumber, ho appena andato il passo in più e ha reso il costruttore immutabile. Stiamo creando più oggetti di quelli che dobbiamo, ma non c'è davvero un'altra buona soluzione. Se tutti useranno il builder nel modo corretto, potremmo renderlo mutabile e return this.


Poiché siamo interessati a nuove soluzioni generiche, ecco un'implementazione di builder in una singola classe.

La differenza qui è che non manteniamo i tipi di typed e list se invochiamo uno dei loro setter una seconda volta. Questo non è davvero un inconveniente di per sé, è solo diverso credo. Ciò significa che siamo in grado di fare questo:

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); 
} 

stessa cosa circa la creazione di copie e l'inquinamento mucchio.


Ora stiamo ottenendo davvero romanzo. L'idea qui è che possiamo "disabilitare" ogni metodo provocando un errore di conversione di cattura.

E 'un po' complicato da spiegare, ma l'idea di base è:

  • Ogni metodo dipende in qualche modo su una variabile di tipo che viene dichiarato sulla classe.
  • "Disabilita" questo metodo impostando il tipo restituito tale variabile di tipo su ?.
  • Ciò provoca un errore di conversione di acquisizione se si tenta di richiamare il metodo su tale valore di ritorno.

La differenza tra questo esempio e l'esempio precedente è che se cerchiamo di chiamare un setter una seconda volta, avremo un errore di compilatore:

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

Quindi, possiamo solo chiamare ogni setter una volta.

Le due principali aspetti negativi qui sono che:

  • Non è possibile chiamare setter una seconda volta per legittimi motivi
  • e can chiamata setter una seconda volta con il null letterale.

Penso che sia un proof-of-concept piuttosto interessante, anche se un po 'poco pratico.

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); 
} 

Anche in questo caso, stessa cosa per la creazione di copie e l'inquinamento mucchio.


In ogni caso, spero che questo ti dia alcune idee per affondare i denti. :)

Se si è generalmente interessati a questo genere di cose, mi consiglia di imparare code generation with annotation processing, perché è possibile generare cose come questa molto più facile rispetto a scriverle a mano. Come abbiamo detto nei commenti, scrivere cose come questa a mano diventa irrealistico abbastanza rapidamente.

+0

Disabilitare setter è abbastanza bello, mai visto prima! – AdamSkywalker

+0

Questa è una risposta più completa di quanto mi aspettassi! Molto perspicace. Molte grazie. –