Sous-classe une classe Java Builder


Donnez cet article de Dr Dobbs, et le modèle de constructeur en particulier, comment gérons-nous le cas de la sous-classification d'un Constructeur? En prenant une version réduite de l'exemple où nous voulons sous-classer pour ajouter l'étiquetage des OGM, 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 obtenons l'ordre mal, tout échoue:

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

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

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

Author: Community, 2013-06-18

10 answers

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

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

Instancie maintenant le générateur de base avec le générateur de classe dérivé comme 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;
    }
}
 136
Author: gkamal, 2017-08-15 16:15:39

Juste pour mémoire, pour se débarrasser du

unchecked or unsafe operations avertissement

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

Faire abstract le constructeur qui déclare le type générique (T extends Builder dans ce cas) et de déclarer protected abstract T getThis() méthode abstraite 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); }
}

Se référer à http://www.angelikalanger.com/GenericsFAQ/FAQSections/ProgrammingIdioms.html#FAQ205 pour plus de détails.

 30
Author: Stepan Vavra, 2016-01-12 10:59:03

Basé sur un article de blog , cette approche nécessite que toutes les classes non-feuilles soient abstraites, et toutes les classes de feuilles doivent être 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 que vous en avez besoin:

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 de feuille concrète qui peut appeler toutes les méthodes de constructeur sur l'un 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 classes dans l' hiérarchie:

public class Demo {
    LeafClass leaf = new LeafClass.Builder().baz(2).foo(1).bar(3).build();
}
 17
Author: Q23, 2016-05-16 18:35:25

Vous pouvez également remplacer la méthode calories() et la laisser renvoyer le générateur d'extension. Cela compile parce que Java prend en charge 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
Author: Flavio, 2013-06-18 09:07:08

Si vous ne voulez pas pousser votre œil sur un support d'angle ou trois, ou peut-être ne vous sentez pas... umm... Je veux dire... la toux... le reste de votre équipe comprendra rapidement le modèle générique 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");
  }
}

Soutenu 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 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:

  • Encapsuler l'objet dans le générateur afin que l'héritage vous empêche de définir le champ sur l'objet détenu dans le générateur. type parent
  • Les appels à super garantissent que la logique (le cas échéant) ajoutée aux méthodes super type builder est conservée dans les sous-types.
  • Vers le bas est la création d'objets parasites dans la ou les classes parentes... Mais voir ci-dessous pour un moyen de nettoyer cela
  • Up side est beaucoup plus facile à comprendre en un coup d'œil, et aucun constructeur verbeux ne transfère de propriétés.
  • Si plusieurs threads accèdent à vos objets builder... Je suis content de ne pas être toi :).

MODIFIER:

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

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

, Puis dans le constructeur pour chaque constructeur:

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

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

 2
Author: Gus, 2016-03-17 14:26:51

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

Définir une interface, que la classe parent {[6] } héritera:

public interface FactsBuilder<T> {

    public T calories(int val);
}

L'implémentation de NutritionFacts est presque la même (sauf pour Builder implémentant 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;
    }
}

Le Builder d'une classe enfant devrait étendre la même interface (sauf 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 à l'intérieur de GMOFacts.Builder (appelé baseBuilder). La méthode implémentée à partir de l'interface FactsBuilder appelle la méthode du même nom baseBuilder:

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

, Il est aussi un grand changement dans le constructeur de GMOFacts(Builder builder). Le premier appel du constructeur au constructeur de la classe parent doit passer NutritionFacts.Builder approprié:

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

L'implémentation complète de GMOFacts class:

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
Author: R. Zagórski, 2016-10-17 13:24:43

Une chose que vous pourriez faire est de créer une méthode d'usine statique dans chacune de vos classes:

NutritionFacts.newBuilder()
GMOFacts.newBuilder()

Cette méthode d'usine statique retournerait alors le constructeur approprié. Vous pouvez avoir un GMOFacts.Builder étendant un NutritionFacts.Builder, ce n'est pas un problème. LE problème ici sera de traiter la visibilité...

 1
Author: fge, 2015-07-29 22:19:03

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


Un peu plus de la version 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);
    }
}
 1
Author: v0rin, 2018-06-28 09:58:29

La contribution IEEE suivante Refined Fluent Builder en Java donne une solution complète au problème.

Il dissèque la question d'origine en deux sous-problèmes de déficit en héritageet quasi invariance et montre comment une solution à ces deux sous-problèmes s'ouvre pour le support de l'héritage avec la réutilisation du code dans le modèle de constructeur classique en Java.

 1
Author: mc00x1, 2018-10-01 15:57:45

J'ai créé une classe de constructeur générique abstraite parent qui accepte deux paramètres de type formel. Le premier concerne le type d'objet renvoyé par build(), le second le type renvoyé par chaque paramétreur optionnel. Voici les classes parents et enfants à titre indicatif:

**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
Author: Jose Quijada, 2017-11-15 22:39:39