web-dev-qa-db-fra.com

Sous-classement d'une classe Java Builder

Donnez cet article de Dr Dobbs , et le motif Builder en particulier, comment traitons-nous le cas du sous-classement d'un Builder? En prenant une version simplifiée de l'exemple dans lequel nous souhaitons ajouter une étiquette pour les OGM à une sous-classe, une implémentation naïve serait:

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

Sous-classe:

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

Maintenant, nous pouvons écrire du code comme ceci:

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

Mais, si nous nous trompons, tout échoue:

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

Le problème est bien sûr que NutritionFacts.Builder renvoie un NutritionFacts.Builder, pas un GMOFacts.Builder, alors comment pouvons-nous résoudre ce problème ou existe-t-il un meilleur modèle à utiliser?

Remarque: cette réponse à une question similaire offre les cours que j'ai ci-dessus; Ma question concerne le problème de s'assurer que les appels des constructeurs sont dans le bon ordre.

110
Ken Y-N

Vous pouvez le résoudre en utilisant des génériques. Je pense que cela s’appelle le "Modèles génériques curieusement récurrents"

Faites du type de retour des méthodes du générateur de classe de base un argument générique.

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

Instanciez maintenant le générateur de base avec le générateur de classe dérivée en tant qu'argument générique.

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

Pour mémoire, pour se débarrasser de la 

unchecked or unsafe operations avertissement

pour l'instruction return (T) this; telle que @dimadima et @Thomas N., la solution suivante s'applique dans certains cas.

Définissez abstract sur le générateur qui déclare le type générique (T extends Builder dans ce cas) et déclarez la méthode abstraite protected abstract T getThis() comme suit:

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

Reportez-vous à http://www.angelikalanger.com/GenericsFAQ/FAQSections/ProgrammingIdioms.html#FAQ205 pour plus de détails.

32
Stepan Vavra

Basée sur un article de blog , cette approche requiert que toutes les classes non-feuille soient abstraites et que toutes les classes feuille soient finales.

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

Ensuite, vous avez une classe intermédiaire qui étend cette classe et son constructeur, et autant d’autres selon vos besoins:

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

Et enfin, une classe feuille concrète pouvant appeler toutes les méthodes du constructeur sur n’importe quel de ses parents, dans n’importe quel ordre:

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

Ensuite, vous pouvez appeler les méthodes dans n'importe quel ordre, à partir de n'importe laquelle des classes de la hiérarchie:

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

Vous pouvez également remplacer la méthode calories() et la laisser renvoyer le générateur extensible. Cela compile car Java prend en charge les types de retour covariants .

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);
        }
    }
    [...]
}
6
Flavio

Il existe également un autre moyen de créer des classes selon le modèle Builder, qui est conforme à "Préférer la composition à l'héritage".

Définissez une interface, cette classe parente Builder héritera:

public interface FactsBuilder<T> {

    public T calories(int val);
}

L'implémentation de NutritionFacts est presque identique (à l'exception de Builder qui implémente l'interface '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 Builder d'une classe enfant devrait étendre la même interface (sauf une implémentation générique différente):

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

Notez que NutritionFacts.Builder est un champ dans GMOFacts.Builder (appelé baseBuilder). La méthode implémentée à partir de l'interface FactsBuilder appelle la méthode baseBuilder du même nom:

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

Il y a aussi un grand changement dans le constructeur de GMOFacts(Builder builder). Le premier appel du constructeur à un constructeur de classe parent doit transmettre le NutritionFacts.Builder approprié:

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

L'implémentation complète de la classe GMOFacts:

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;
    }
}
2
R. Zagórski

Un exemple complet à 3 niveaux d'héritage de plusieurs constructeurs ressemblerait à ceci :

(Pour la version avec un constructeur de copie pour le générateur, voir le deuxième exemple ci-dessous)

Premier niveau - parent (potentiellement abstrait)

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

Deuxième niveau

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

Troisième niveau

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

Et un exemple d'utilisation

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


Version un peu plus longue avec un constructeur de copie pour le constructeur:

Premier niveau - parent (potentiellement abstrait)

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

Deuxième niveau

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

Troisième niveau

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

Et un exemple d'utilisation

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
v0rin

Si vous ne voulez pas attirer votre attention sur un angle ou trois, ou peut-être que vous ne vous sentez pas ... hum ... je veux dire ...toux... le Le reste de votre équipe comprendra rapidement le modèle de génériques curieusement récurrent. Vous pouvez le faire:

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

supporté par

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

et le type de parent:

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 + '\'' +
        '}';
  }
}

Points clés: 

  • Encapsulez l'objet dans le générateur afin que l'héritage vous empêche de définir le champ sur l'objet contenu dans le type parent
  • Les appels à vérifier que la logique (le cas échéant) ajoutée aux méthodes du générateur de type super sont conservés dans les sous-types.
  • Inconvénient: création d’objets parasites dans la ou les classes parentes ... Mais voyez ci-dessous un moyen de nettoyer cela.
  • Il est beaucoup plus facile comprendre d'un coup d'oeil en un coup d'oeil, et aucun constructeur verbose ne transfère des propriétés.
  • Si vous avez plusieurs threads accédant à vos objets de construction, je suis content de ne pas être vous :).

MODIFIER:  

J'ai trouvé un moyen de contourner la création d'objet parasite. Ajoutez d'abord ceci à chaque constructeur:

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

Puis dans le constructeur de chaque constructeur: 

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

Le coût est un fichier de classe supplémentaire pour la classe interne anonyme new Object(){}

2
Gus

Une chose à faire est de créer une méthode de fabrique statique dans chacune de vos classes:

NutritionFacts.newBuilder()
GMOFacts.newBuilder()

Cette méthode d'usine statique renverrait alors le générateur approprié. Vous pouvez avoir un GMOFacts.Builder prolongeant un NutritionFacts.Builder, ce n'est pas un problème. Le problème ici sera de traiter avec la visibilité ...

1
fge

La contribution suivante de l'IEEE Refined Fluent Builder in Java fournit une solution complète au problème. 

Il dissèque la question initiale en deux sous-problèmes héritage, déficience et quasi invariance, et montre comment une solution à ces deux sous-problèmes ouvre la voie à la prise en charge de l'héritage avec réutilisation du code dans le modèle de construction classique en Java.

1
mc00x1

J'ai créé une classe de générateur générique abstraite et parent qui accepte deux paramètres de type formels. La première concerne le type d'objet renvoyé par build (), la seconde est le type renvoyé par chaque configurateur de paramètres facultatif. Vous trouverez ci-dessous des classes de parents et d'enfants à des fins d'illustration:

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

Celui-ci a répondu à mes besoins à la satisfaction.

0
Jose Quijada