2015-07-09 11 views
11

Ho una classe builder che restituisce se stessa dalla maggior parte dei metodi per consentire il collegamento a catena. Per fare in modo che questo funzioni con le classi figlio, voglio che i metodi padre restituiscano le istanze del figlio in modo che i metodi figlio siano disponibili a catena fino alla fine.Restituisce classe figlio dalla classe padre

public class BaseBuilder<T extends BaseBuilder<T>> { 
    public T buildSomething() { 
     doSomeWork(); 
     /* OPTION #1: */ return this;  // "Type mismatch: cannot convert from BaseBuilder<T> to T" 
     /* OPTION #2: */ return T.this; // "Type mismatch: cannot convert from BaseBuilder<T> to T" 
     /* OPTION #3: */ return (T) this; // "Type safety: Unchecked cast from SqlBuilder<T> to T" 
    } 
} 

public class ChildBuilder extends BaseBuilder<ChildBuilder> {} 

Opzioni # 1 e # 2 risultato errori di compilazione, e l'opzione # 3 un avvertimento (anche se uno che può essere soppresso con @SuppressWarnings("unchecked")). C'è un approccio migliore qui? Come posso tranquillamente eseguire il downcast di Basebuilder su Childbuilder?

+0

Non dovrebbe essere di classe pubblica BaseBuilder perché il tipo generico si riferisce alla stessa classe? => ChildBuilder estende BaseBuilder 6ton

+0

@ 6ton, perché i soli tipi generici validi sono i tipi che ereditano BaseBuilder. Ad esempio, "BaseBuilder " non avrebbe senso. –

+0

Ancora non sono sicuro del motivo per cui hai bisogno della firma - ho pubblicato una risposta con giustificazione – 6ton

risposta

6

La dichiarazione ChildBuilder extends BaseBuilder<ChildBuilder> indica in qualche modo un odore di codice e sembra violare DRY. In questo esempio BaseBuilder può essere parametrizzato solo con ChildBuilder e nient'altro, quindi dovrebbe essere ridondante.

Preferirei ripensare al fatto se davvero voglio fare un'architettura eccessiva e proverei a mettere tutti i metodi dei costruttori di bambini nello BaseBuilder. Quindi posso semplicemente restituire this da tutti i metodi che supportano il concatenamento.

Se continuo a pensare che trarrai vantaggio separando gruppi specifici di metodi builder nelle proprie classi, quindi darei preferenza alla composizione, perché non è consigliabile applicare l'ereditarietà solo per il riutilizzo del codice.

Supponiamo di avere due sottoclassi della BaseBuilder:

class BuilderA extends BaseBuilder<BuilderA> { 
    BuilderA buildSomethingA() { return this; } 
} 

class BuilderB extends BaseBuilder<BuilderB> { 
    BuilderB buildSomethingB() { return this; } 
} 

E se sorge la necessità di catena buildSomethingA e buildSomethingB come:

builder.buildSomething().buildSomethingA().buildSomethingB(); 

Non saremo in grado di farlo senza spostare il metodi di sottoclasse allo BaseBuilder; ma immagina che ci sia anche BuilderC per cui quei metodi non hanno senso e non dovrebbero essere ereditati dallo BaseBuilder.

Se spostiamo questi due metodi sulla superclasse e la prossima volta con altri tre metodi e la prossima volta ... ci ritroveremo con una superclasse responsabile del 90% dei compiti dell'intera gerarchia con un sacco di codice come:

if ((this instanceof BuilderB) && !flag1 && flag2) { 
    ... 
} else if ((this instanceof BuilderC) && flag1 && !flag2 && thing != null) { 
    ... 
} else if ... 

la soluzione che mi piace di più è un DSL come:

builder.buildSomething1().buildSomething2() 
    .builderA() 
     .buildSomethingA1().buildSomethingA2() 
    .end() 
    .buildSomething3() 
    .builderB() 
     .buildSomethingB() 
    .end(); 

Qui end() restituisce l'istanza builder in modo da poter catena più dei suoi metodi o di iniziare una nuova sotto-builder.

In questo modo i (sub) builder possono ereditare da qualsiasi cosa abbiano bisogno (altrimenti devono estendere solo lo BaseBuilder) e possono avere le loro gerarchie o composizioni significative.

+0

Grazie, fai dei buoni punti. Il suggerimento 'end()' è una grande idea! –

0

Invece di dichiarare il metodo per restituire T - dichiarare per tornare BaseBuilder:

public BaseBuilder buildSomething() { 
... 

Dal T estende BaseBuilder - ma non è ancora noto in fase di compilazione, credo che questo è il miglior compromesso è possibile fare.

Se sai (durante la fase di compilazione) esattamente quale tipo stai restituendo puoi semplicemente restituirlo, ma in caso contrario - e dovrai downcast - continuerai a ricevere "Type safety: Unchecked cast e se puoi dimostrare che il downcast è valido che va perfettamente bene a SuppressWarnings.

Vedere Josh Bloch's wise words in riguardo.

1

Una possibilità è utilizzare il fatto che Java supporta tipi di ritorno covarianti. Ad esempio, questo codice è legale:

class BaseBuilder { 
    BaseBuilder buildSomething() { (...) return this; } 
} 

class ChildBuilder extends BaseBuilder { 
    @Override // Notice the more specific return type 
    ChildBuilder buildSomething() { (...) return this; } 
} 

void main() { 
    BaseBuilder x = new BaseBuilder().buildSomething().anotherOperation(); 
    ChildBuilder y = new ChildBuilder().buildSomething().anotherOperation(); 
} 

In caso contrario, l'opzione # 3 è l'unico modo per ottenere davvero ciò che si desidera. Permette metodi della superclasse di tornare direttamente un tipo di sottoclasse in modo che è possibile richiamare metodi delle sottoclassi:

@SuppressWarnings("unchecked") // Ugly. 
class Base<T extends Base<T>> { // Ugly. 
    public T alpha() { return (T)this; } 
    public T delta() { return (T)this; } 
} 

class Child extends Base<Child> { // Clean. 
    // No need to override/redefine alpha() and delta() in child. 
    public Child gamma() { return this; } 
} 

void main(String[] args) { 
    Child x = new Child(); 
    x.alpha().gamma(); // This works because alpha() returns Child. 
} 
-1

IMO si firma costruttore di base BaseBuilder<T extends BaseBuilder<T>> deve essere cambiato.

Immagino T per riferirsi al tipo in fase di costruzione e BaseBuilder<T extends ComplexObject>ChildBuilder extends BaseBuilder<MoreComplexObject>

Sostituzione di metodi in ChildBuilder funziona ancora - si torna this. Dal metodo di compilazione ritorni T

public class BaseBuilder<T extends ComplexObject> { 
    public BaseBuilder<T> withComplexThing() { 
     return this; 
    } 

    public T build() { 
    } 

} 

public class ChildBuilder extends BaseBuilder<MoreComplexObject> { 
    public ChildBuilder withComplexThing() { 
     return this; 
    } 

    public MoreComplexObject build() { 
    } 
} 
-1

è ok, basta eliminare l'avviso.

Se si deve essere un purista, ecco una soluzione:

abstract public class BaseBuilder<T...> 
{ 
    abstract protected T getThis(); 

    public T buildSomething() 
     ... 
      return getThis(); 


... 

public class ChildBuilder extends BaseBuilder<ChildBuilder> 
{ 
    @Override 
    protected ChildBuilder getThis(){ return this; } 
} 

mi consiglia di abbandonare il limite ricorsiva; è quasi inutile. Basta nominare la variabile di tipo This.

public class BaseBuilder<This> 
1

Cast in opzione # 3 non è sicuro in quanto la seguente classe avrebbe compilato (è la responsabilità sviluppatore):

public class ChildBuilder extends BaseBuilder<FakeBuilder> {} 
               ^^^^^^^^^^^ 

Una soluzione comune è quella di chiedere le sottoclassi per la loro this:

public abstract class BaseBuilder<T extends BaseBuilder<T>> { 
    protected abstract T getThis(); 
    public T buildSomething() { 
    return getThis(); 
    } 
} 

public class ChildBuilder extends BaseBuilder<ChildBuilder> { 
    @Override 
    protected ChildBuilder getThis() { 
    return this; 
    } 
}