Sottoclasse di una classe Java Builder


133

Fornisci questo articolo del dottor Dobbs , e in particolare il modello del costruttore, come possiamo gestire il caso di sottoclasse di un costruttore? Prendendo una versione ridotta dell'esempio in cui vogliamo sottoclassare per aggiungere l'etichettatura OGM, un'implementazione ingenua sarebbe:

public class NutritionFacts {                                                                                                    

    private final int calories;                                                                                                  

    public static class Builder {                                                                                                
        private int calories = 0;                                                                                                

        public Builder() {}                                                                                                      

        public Builder calories(int val) { calories = val; return this; }                                                                                                                        

        public NutritionFacts build() { return new NutritionFacts(this); }                                                       
    }                                                                                                                            

    protected NutritionFacts(Builder builder) {                                                                                  
        calories = builder.calories;                                                                                             
    }                                                                                                                            
}

sottoclasse:

public class GMOFacts extends NutritionFacts {                                                                                   

    private final boolean hasGMO;                                                                                                

    public static class Builder extends NutritionFacts.Builder {                                                                 

        private boolean hasGMO = false;                                                                                          

        public Builder() {}                                                                                                      

        public Builder GMO(boolean val) { hasGMO = val; return this; }                                                           

        public GMOFacts build() { return new GMOFacts(this); }                                                                   
    }                                                                                                                            

    protected GMOFacts(Builder builder) {                                                                                        
        super(builder);                                                                                                          
        hasGMO = builder.hasGMO;                                                                                                 
    }                                                                                                                            
}

Ora possiamo scrivere codice in questo modo:

GMOFacts.Builder b = new GMOFacts.Builder();
b.GMO(true).calories(100);

Ma, se sbagliamo l'ordine, tutto fallisce:

GMOFacts.Builder b = new GMOFacts.Builder();
b.calories(100).GMO(true);

Il problema è ovviamente che NutritionFacts.Builderrestituisce un NutritionFacts.Builder, non un GMOFacts.Builder, quindi come possiamo risolvere questo problema o esiste un modello migliore da usare?

Nota: questa risposta a una domanda simile offre le classi che ho sopra; la mia domanda riguarda il problema di garantire che le chiamate del builder siano nell'ordine corretto.


1
Penso che il seguente link descriva un buon approccio: egalluzzo.blogspot.co.at/2010/06/…
stuXnet

1
Ma come si fa a build()produrre b.GMO(true).calories(100)?
Sridhar Sarnobat,

Risposte:


170

Puoi risolverlo usando i generici. Penso che questo si chiama "modelli generici ricuramente ricorrenti"

Trasforma il tipo restituito dei metodi del builder di classe base come argomento generico.

public class NutritionFacts {

    private final int calories;

    public static class Builder<T extends Builder<T>> {

        private int calories = 0;

        public Builder() {}

        public T calories(int val) {
            calories = val;
            return (T) this;
        }

        public NutritionFacts build() { return new NutritionFacts(this); }
    }

    protected NutritionFacts(Builder<?> builder) {
        calories = builder.calories;
    }
}

Ora crea un'istanza del builder di base con il builder di classi derivato come argomento generico.

public class GMOFacts extends NutritionFacts {

    private final boolean hasGMO;

    public static class Builder extends NutritionFacts.Builder<Builder> {

        private boolean hasGMO = false;

        public Builder() {}

        public Builder GMO(boolean val) {
            hasGMO = val;
            return this;
        }

        public GMOFacts build() { return new GMOFacts(this); }
    }

    protected GMOFacts(Builder builder) {
        super(builder);
        hasGMO = builder.hasGMO;
    }
}

2
Hmm, penso che dovrò (a) pubblicare una nuova domanda, (b) riprogettare con implementsinvece di extends, o (c) buttare via tutto. Ora ho uno strano errore di compilazione in cui leafBuilder.leaf().leaf()ed leafBuilder.mid().leaf()è OK, ma leafBuilder.leaf().mid().leaf()non riesce ...
Ken YN

11
@gkamal return (T) this;genera un unchecked or unsafe operationsavviso. Questo è impossibile da evitare, giusto?
Dmitry Minkovsky,

5
Per risolvere l' unchecked castavviso, vedere la soluzione suggerita di seguito tra le altre risposte: stackoverflow.com/a/34741836/3114959
Stepan Vavra,

8
Nota che in Builder<T extends Builder>realtà è un tipo raw - questo dovrebbe essere Builder<T extends Builder<T>>.
Boris the Spider

2
@ user2957378 the Builderfor GMOFactsdeve anche essere generico Builder<B extends Builder<B>> extends NutritionFacts.Builder<Builder>- e questo modello può continuare fino al livello richiesto. Se si dichiara un builder non generico, non è possibile estendere il modello.
Boris the Spider

44

Solo per la cronaca, per sbarazzarsi di

unchecked or unsafe operations avvertimento

per la return (T) this;dichiarazione di cui parlano @dimadima e @Thomas N., in alcuni casi si applica la seguente soluzione.

Crea abstractil builder che dichiara il tipo generico ( T extends Builderin questo caso) e dichiara il protected abstract T getThis()metodo astratto come segue:

public abstract static class Builder<T extends Builder<T>> {

    private int calories = 0;

    public Builder() {}

    /** The solution for the unchecked cast warning. */
    public abstract T getThis();

    public T calories(int val) {
        calories = val;

        // no cast needed
        return getThis();
    }

    public NutritionFacts build() { return new NutritionFacts(this); }
}

Fare riferimento a http://www.angelikalanger.com/GenericsFAQ/FAQSections/ProgrammingIdioms.html#FAQ205 per ulteriori dettagli.


Perché il build()metodo restituisce qui i Valori nutrizionali?
mvd,

@mvd Perché questa è una risposta alla domanda? Nei sottotipi, lo sostituirai comepublic GMOFacts build() { return new GMOFacts(this); }
Stepan Vavra,

Il problema si verifica quando vogliamo aggiungere il secondo figlio BuilderC extends BuilderBe BuilderB extends BuilderAquando BuilderBnon lo èabstract
sosite,

1
Questa non è una risposta alla domanda, perché la classe base potrebbe non essere astratta!
Roland,

"Rendi astratto il costruttore che dichiara il tipo generico" - e se volessi usare direttamente quel costruttore?
Margherita

21

Basato su un post sul blog , questo approccio richiede che tutte le classi non foglia siano astratte e tutte le classi foglia devono essere definitive.

public abstract class TopLevel {
    protected int foo;
    protected TopLevel() {
    }
    protected static abstract class Builder
        <T extends TopLevel, B extends Builder<T, B>> {
        protected T object;
        protected B thisObject;
        protected abstract T createObject();
        protected abstract B thisObject();
        public Builder() {
            object = createObject();
            thisObject = thisObject();
        }
        public B foo(int foo) {
            object.foo = foo;
            return thisObject;
        }
        public T build() {
            return object;
        }
    }
}

Quindi, hai qualche classe intermedia che estende questa classe e il suo builder, e tanti altri di cui hai bisogno:

public abstract class SecondLevel extends TopLevel {
    protected int bar;
    protected static abstract class Builder
        <T extends SecondLevel, B extends Builder<T, B>> extends TopLevel.Builder<T, B> {
        public B bar(int bar) {
            object.bar = bar;
            return thisObject;
        }
    }
}

E, infine, una classe foglia concreta che può chiamare tutti i metodi di costruzione su uno dei suoi genitori in qualsiasi ordine:

public final class LeafClass extends SecondLevel {
    private int baz;
    public static final class Builder extends SecondLevel.Builder<LeafClass,Builder> {
        protected LeafClass createObject() {
            return new LeafClass();
        }
        protected Builder thisObject() {
            return this;
        }
        public Builder baz(int baz) {
            object.baz = baz;
            return thisObject;
        }
    }
}

Quindi, puoi chiamare i metodi in qualsiasi ordine, da qualsiasi classe nella gerarchia:

public class Demo {
    LeafClass leaf = new LeafClass.Builder().baz(2).foo(1).bar(3).build();
}

Sai perché le classi fogliari devono essere definitive? Vorrei che le mie classi concrete fossero sottoclassabili, ma non ho trovato il modo di far capire al compilatore il tipo di B, risulta sempre come la classe base.
David Ganster,

Nota come la classe Builder in LeafClass non segua lo stesso <T extends SomeClass, B extends SomeClass.Builder<T,B>> extends SomeClassParent.Builder<T,B>modello della classe SecondLevel intermedia, ma dichiara tipi specifici. Non puoi installare una classe fino a quando non arrivi alla foglia usando i tipi specifici, ma una volta che lo fai, non puoi estenderla ulteriormente perché stai usando i tipi specifici e hai abbandonato il modello di modello curiosamente ricorrente. Questo link potrebbe essere d'aiuto: angelikalanger.com/GenericsFAQ/FAQSections/…
Q23

7

È possibile sovrascrivere anche il calories()metodo e lasciarlo restituire il builder estendente. Questo viene compilato perché Java supporta tipi di ritorno covarianti .

public class GMOFacts extends NutritionFacts {
    private final boolean hasGMO;
    public static class Builder extends NutritionFacts.Builder {
        private boolean hasGMO = false;
        public Builder() {
        }
        public Builder GMO(boolean val)
        { hasGMO = val; return this; }
        public Builder calories(int val)
        { super.calories(val); return this; }
        public GMOFacts build() {
            return new GMOFacts(this);
        }
    }
    [...]
}

Ah, non lo sapevo, poiché provengo da uno sfondo C ++. Questo è un approccio utile per questo piccolo esempio, ma con una lezione in piena regola che ripete tutti i metodi diventa un dolore e un dolore soggetto a errori. +1 per avermi insegnato qualcosa di nuovo, comunque!
Ken YN,

Mi sembra che questo non risolva nulla. Il motivo (IMO) per la sottoclasse dei genitori è di riutilizzare i metodi dei genitori senza sovrascriverli. Se le classi sono semplicemente oggetti valore senza una vera logica nei metodi del builder, tranne per impostare un valore semplice, allora chiamare il metodo genitore nel metodo di sostituzione ha poco o nessun valore.
Dude sviluppatore

La risposta risolve il problema descritto nella domanda: il codice che utilizza il builder viene compilato con entrambi gli ordini. Dato che un modo si compila e l'altro no, suppongo che ci debba essere un valore dopo tutto.
Flavio,

3

Esiste anche un altro modo per creare classi in base al Buildermodello, che è conforme a "Preferisci composizione su ereditarietà".

Definisci un'interfaccia, quella classe genitrice Buildererediterà:

public interface FactsBuilder<T> {

    public T calories(int val);
}

L'implementazione di NutritionFactsè quasi la stessa (tranne per l' Builderimplementazione dell'interfaccia 'FactsBuilder'):

public class NutritionFacts {

    private final int calories;

    public static class Builder implements FactsBuilder<Builder> {
        private int calories = 0;

        public Builder() {
        }

        @Override
        public Builder calories(int val) {
            return this;
        }

        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
    }

    protected NutritionFacts(Builder builder) {
        calories = builder.calories;
    }
}

La Builderclasse di un figlio dovrebbe estendere la stessa interfaccia (tranne diversa implementazione generica):

public static class Builder implements FactsBuilder<Builder> {
    NutritionFacts.Builder baseBuilder;

    private boolean hasGMO = false;

    public Builder() {
        baseBuilder = new NutritionFacts.Builder();
    }

    public Builder GMO(boolean val) {
        hasGMO = val;
        return this;
    }

    public GMOFacts build() {
        return new GMOFacts(this);
    }

    @Override
    public Builder calories(int val) {
        baseBuilder.calories(val);
        return this;
    }
}

Si noti, questo NutritionFacts.Builderè un campo all'interno GMOFacts.Builder(chiamato baseBuilder). Il metodo implementato FactsBuilderdall'interfaccia chiama baseBuilderlo stesso metodo:

@Override
public Builder calories(int val) {
    baseBuilder.calories(val);
    return this;
}

C'è anche un grande cambiamento nel costruttore di GMOFacts(Builder builder). La prima chiamata dal costruttore al costruttore della classe genitore dovrebbe passare appropriata NutritionFacts.Builder:

protected GMOFacts(Builder builder) {
    super(builder.baseBuilder);
    hasGMO = builder.hasGMO;
}

La piena attuazione della GMOFactsclasse:

public class GMOFacts extends NutritionFacts {

    private final boolean hasGMO;

    public static class Builder implements FactsBuilder<Builder> {
        NutritionFacts.Builder baseBuilder;

        private boolean hasGMO = false;

        public Builder() {
        }

        public Builder GMO(boolean val) {
            hasGMO = val;
            return this;
        }

        public GMOFacts build() {
            return new GMOFacts(this);
        }

        @Override
        public Builder calories(int val) {
            baseBuilder.calories(val);
            return this;
        }
    }

    protected GMOFacts(Builder builder) {
        super(builder.baseBuilder);
        hasGMO = builder.hasGMO;
    }
}

3

Un esempio completo a 3 livelli dell'ereditarietà multi-generatore sarebbe simile al seguente :

(Per la versione con un costruttore di copie per il builder, vedere il secondo esempio di seguito)

Primo livello - genitore (potenzialmente astratto)

import lombok.ToString;

@ToString
@SuppressWarnings("unchecked")
public abstract class Class1 {
    protected int f1;

    public static class Builder<C extends Class1, B extends Builder<C, B>> {
        C obj;

        protected Builder(C constructedObj) {
            this.obj = constructedObj;
        }

        B f1(int f1) {
            obj.f1 = f1;
            return (B)this;
        }

        C build() {
            return obj;
        }
    }
}

Secondo livello

import lombok.ToString;

@ToString(callSuper=true)
@SuppressWarnings("unchecked")
public class Class2 extends Class1 {
    protected int f2;

    public static class Builder<C extends Class2, B extends Builder<C, B>> extends Class1.Builder<C, B> {
        public Builder() {
            this((C) new Class2());
        }

        protected Builder(C obj) {
            super(obj);
        }

        B f2(int f2) {
            obj.f2 = f2;
            return (B)this;
        }
    }
}

Terzo livello

import lombok.ToString;

@ToString(callSuper=true)
@SuppressWarnings("unchecked")
public class Class3 extends Class2 {
    protected int f3;

    public static class Builder<C extends Class3, B extends Builder<C, B>> extends Class2.Builder<C, B> {
        public Builder() {
            this((C) new Class3());
        }

        protected Builder(C obj) {
            super(obj);
        }

        B f3(int f3) {
            obj.f3 = f3;
            return (B)this;
        }
    }
}

E un esempio di utilizzo

public class Test {
    public static void main(String[] args) {
        Class2 b1 = new Class2.Builder<>().f1(1).f2(2).build();
        System.out.println(b1);
        Class2 b2 = new Class2.Builder<>().f2(2).f1(1).build();
        System.out.println(b2);

        Class3 c1 = new Class3.Builder<>().f1(1).f2(2).f3(3).build();
        System.out.println(c1);
        Class3 c2 = new Class3.Builder<>().f3(3).f1(1).f2(2).build();
        System.out.println(c2);
        Class3 c3 = new Class3.Builder<>().f3(3).f2(2).f1(1).build();
        System.out.println(c3);
        Class3 c4 = new Class3.Builder<>().f2(2).f3(3).f1(1).build();
        System.out.println(c4);
    }
}


Una versione un po 'più lunga con un costruttore di copie per il costruttore:

Primo livello - genitore (potenzialmente astratto)

import lombok.ToString;

@ToString
@SuppressWarnings("unchecked")
public abstract class Class1 {
    protected int f1;

    public static class Builder<C extends Class1, B extends Builder<C, B>> {
        C obj;

        protected void setObj(C obj) {
            this.obj = obj;
        }

        protected void copy(C obj) {
            this.f1(obj.f1);
        }

        B f1(int f1) {
            obj.f1 = f1;
            return (B)this;
        }

        C build() {
            return obj;
        }
    }
}

Secondo livello

import lombok.ToString;

@ToString(callSuper=true)
@SuppressWarnings("unchecked")
public class Class2 extends Class1 {
    protected int f2;

    public static class Builder<C extends Class2, B extends Builder<C, B>> extends Class1.Builder<C, B> {
        public Builder() {
            setObj((C) new Class2());
        }

        public Builder(C obj) {
            this();
            copy(obj);
        }

        @Override
        protected void copy(C obj) {
            super.copy(obj);
            this.f2(obj.f2);
        }

        B f2(int f2) {
            obj.f2 = f2;
            return (B)this;
        }
    }
}

Terzo livello

import lombok.ToString;

@ToString(callSuper=true)
@SuppressWarnings("unchecked")
public class Class3 extends Class2 {
    protected int f3;

    public static class Builder<C extends Class3, B extends Builder<C, B>> extends Class2.Builder<C, B> {
        public Builder() {
            setObj((C) new Class3());
        }

        public Builder(C obj) {
            this();
            copy(obj);
        }

        @Override
        protected void copy(C obj) {
            super.copy(obj);
            this.f3(obj.f3);
        }

        B f3(int f3) {
            obj.f3 = f3;
            return (B)this;
        }
    }
}

E un esempio di utilizzo

public class Test {
    public static void main(String[] args) {
        Class3 c4 = new Class3.Builder<>().f2(2).f3(3).f1(1).build();
        System.out.println(c4);

        // Class3 builder copy
        Class3 c42 = new Class3.Builder<>(c4).f2(12).build();
        System.out.println(c42);
        Class3 c43 = new Class3.Builder<>(c42).f2(22).f1(11).build();
        System.out.println(c43);
        Class3 c44 = new Class3.Builder<>(c43).f3(13).f1(21).build();
        System.out.println(c44);
    }
}

2

Se non vuoi sporgere l'occhio su una parentesi angolare o tre, o forse non ti senti ... umm ... voglio dire ... tosse ... il resto della tua squadra comprenderà rapidamente curiosamente schema di generici ricorrenti, puoi farlo:

public class TestInheritanceBuilder {
  public static void main(String[] args) {
    SubType.Builder builder = new SubType.Builder();
    builder.withFoo("FOO").withBar("BAR").withBaz("BAZ");
    SubType st = builder.build();
    System.out.println(st.toString());
    builder.withFoo("BOOM!").withBar("not getting here").withBaz("or here");
  }
}

sostenuto da

public class SubType extends ParentType {
  String baz;
  protected SubType() {}

  public static class Builder extends ParentType.Builder {
    private SubType object = new SubType();

    public Builder withBaz(String baz) {
      getObject().baz = baz;
      return this;
    }

    public Builder withBar(String bar) {
      super.withBar(bar);
      return this;
    }

    public Builder withFoo(String foo) {
      super.withFoo(foo);
      return this;
    }

    public SubType build() {
      // or clone or copy constructor if you want to stamp out multiple instances...
      SubType tmp = getObject();
      setObject(new SubType());
      return tmp;
    }

    protected SubType getObject() {
      return object;
    }

    private void setObject(SubType object) {
      this.object = object;
    }
  }

  public String toString() {
    return "SubType2{" +
        "baz='" + baz + '\'' +
        "} " + super.toString();
  }
}

e il tipo principale:

public class ParentType {
  String foo;
  String bar;

  protected ParentType() {}

  public static class Builder {
    private ParentType object = new ParentType();

    public ParentType object() {
      return getObject();
    }

    public Builder withFoo(String foo) {
      if (!"foo".equalsIgnoreCase(foo)) throw new IllegalArgumentException();
      getObject().foo = foo;
      return this;
    }

    public Builder withBar(String bar) {
      getObject().bar = bar;
      return this;
    }

    protected ParentType getObject() {
      return object;
    }

    private void setObject(ParentType object) {
      this.object = object;
    }

    public ParentType build() {
      // or clone or copy constructor if you want to stamp out multiple instances...
      ParentType tmp = getObject();
      setObject(new ParentType());
      return tmp;
    }
  }

  public String toString() {
    return "ParentType2{" +
        "foo='" + foo + '\'' +
        ", bar='" + bar + '\'' +
        '}';
  }
}

Punti chiave:

  • Incapsula l'oggetto nel builder in modo che l'ereditarietà ti impedisca di impostare il campo sull'oggetto trattenuto nel tipo genitore
  • Le chiamate a super assicurano che la logica (se presente) aggiunta ai metodi di creazione del tipo super venga mantenuta nei sottotipi.
  • Il lato negativo è la creazione di oggetti spuri nelle classi genitore ... Ma vedi sotto per un modo per ripulirlo
  • Il lato positivo è molto più facile da capire a colpo d'occhio, e nessun costruttore dettagliato che trasferisce le proprietà.
  • Se hai più thread che accedono ai tuoi oggetti builder ... Immagino di essere contento di non essere tu :).

MODIFICARE:

Ho trovato un modo per aggirare la creazione di oggetti spuri. Per prima cosa aggiungi questo a ciascun costruttore:

private Class whoAmI() {
  return new Object(){}.getClass().getEnclosingMethod().getDeclaringClass();
}

Quindi nel costruttore per ciascun costruttore:

  if (whoAmI() == this.getClass()) {
    this.obj = new ObjectToBuild();
  }

Il costo è un file di classe extra per la new Object(){}classe interna anonima


1

Una cosa che potresti fare è creare un metodo factory statico in ciascuna delle tue classi:

NutritionFacts.newBuilder()
GMOFacts.newBuilder()

Questo metodo statico di fabbrica restituisce quindi il builder appropriato. Puoi avere l' GMOFacts.Builderestensione a NutritionFacts.Builder, non è un problema. Il problema qui sarà gestire la visibilità ...


0

Il seguente contributo IEEE Raffinato Fluent Builder in Java offre una soluzione completa al problema.

Divide la domanda originale in due sotto-problemi di carenza ereditaria e quasi invarianza e mostra come una soluzione a questi due sotto-problemi si apre per il supporto dell'ereditarietà con il riutilizzo del codice nel modello di builder classico in Java.


Questa risposta non contiene alcuna informazione per essere utile, non contiene almeno un riepilogo della risposta fornita nel collegamento e conduce a un collegamento che richiede l'accesso.
Sonata,

Questa risposta si collega a una pubblicazione della conferenza sottoposta a revisione paritaria con un'autorità di pubblicazione ufficiale e una procedura di pubblicazione e condivisione ufficiale.
mc00x1

0

Ho creato una classe generatrice generica astratta generica che accetta due parametri di tipo formali. Il primo è per il tipo di oggetto restituito da build (), il secondo è il tipo restituito da ciascun settatore di parametri opzionale. Di seguito sono riportate le classi genitore e figlio a scopo illustrativo:

// **Parent**
public abstract static class Builder<T, U extends Builder<T, U>> {
    // Required parameters
    private final String name;

    // Optional parameters
    private List<String> outputFields = null;


    public Builder(String pName) {
        name = pName;
    }

    public U outputFields(List<String> pOutFlds) {
        outputFields = new ArrayList<>(pOutFlds);
        return getThis();
    }


    /**
     * This helps avoid "unchecked warning", which would forces to cast to "T" in each of the optional
     * parameter setters..
     * @return
     */
    abstract U getThis();

    public abstract T build();



    /*
     * Getters
     */
    public String getName() {
        return name;
    }
}

 // **Child**
 public static class Builder extends AbstractRule.Builder<ContextAugmentingRule, ContextAugmentingRule.Builder> {
    // Required parameters
    private final Map<String, Object> nameValuePairsToAdd;

    // Optional parameters
    private String fooBar;


    Builder(String pName, Map<String, String> pNameValPairs) {
        super(pName);
        /**
         * Must do this, in case client code (I.e. JavaScript) is re-using
         * the passed in for multiple purposes. Doing {@link Collections#unmodifiableMap(Map)}
         * won't caught it, because the backing Map passed by client prior to wrapping in
         * unmodifiable Map can still be modified.
         */
        nameValuePairsToAdd = new HashMap<>(pNameValPairs);
    }

    public Builder fooBar(String pStr) {
        fooBar = pStr;
        return this;
    }


    @Override
    public ContextAugmentingRule build() {
        try {
            Rule r = new ContextAugmentingRule(this);
            storeInRuleByNameCache(r);
            return (ContextAugmentingRule) r;
        } catch (RuleException e) {
            throw new IllegalArgumentException(e);
        }
    }

    @Override
    Builder getThis() {
        return this;
    }
}

Questo ha soddisfatto le mie esigenze di soddisfazione.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.