Sous-classification d'une classe Java Builder


133

Donnez cet article de Dr Dobbs , et le modèle Builder en particulier, comment gérer le cas de sous-classer un Builder? 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 celui-ci:

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

Mais, si nous nous trompons dans la commande, tout échoue:

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

Le problème est bien sûr que NutritionFacts.Builderrenvoie a NutritionFacts.Builder, pas a GMOFacts.Builder, alors comment résoudre ce problème, ou y a-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.


1
Je pense que le lien suivant décrit une bonne approche: egalluzzo.blogspot.co.at/2010/06/…
stuXnet

1
Mais comment faites-vous build()la sortie de b.GMO(true).calories(100)?
Sridhar Sarnobat

Réponses:


170

Vous pouvez le résoudre en utilisant des génériques. Je pense que c'est ce qu'on appelle les "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;
    }
}

Maintenant, instanciez 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;
    }
}

2
Hmm, je pense que je vais devoir soit (a) poster une nouvelle question, (b) reconcevoir avec implementsau lieu de extends, ou (c) tout jeter. J'ai maintenant une erreur de compilation étrange où leafBuilder.leaf().leaf()et leafBuilder.mid().leaf()est OK, mais leafBuilder.leaf().mid().leaf()échoue ...
Ken YN

11
@gkamal return (T) this;génère un unchecked or unsafe operationsavertissement. C'est impossible à éviter, non?
Dmitry Minkovsky

5
Pour résoudre l' unchecked castavertissement, consultez la solution suggérée ci-dessous parmi les autres réponses: stackoverflow.com/a/34741836/3114959
Stepan Vavra

8
Notez qu'il Builder<T extends Builder>s'agit en fait d'un rawtype - cela devrait être Builder<T extends Builder<T>>.
Boris the Spider

2
@ user2957378 le Builderpour GMOFactsdoit également être générique Builder<B extends Builder<B>> extends NutritionFacts.Builder<Builder>- et ce modèle peut continuer autant de niveaux que nécessaire. Si vous déclarez un générateur non générique, vous ne pouvez pas étendre le modèle.
Boris the Spider

44

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

unchecked or unsafe operations avertissement

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

Créez abstractle générateur qui déclare le type générique ( T extends Builderdans ce cas) et déclarez protected abstract T getThis()la 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); }
}

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


Pourquoi la build()méthode renvoie-t-elle les NutrutionFacts ici?
mvd

@mvd Parce que c'est une réponse à la question? Dans les sous-types, vous les remplacerez tels quepublic GMOFacts build() { return new GMOFacts(this); }
Stepan Vavra

Le problème se produit lorsque nous voulons ajouter un deuxième enfant BuilderC extends BuilderBet BuilderB extends BuilderAquand ce BuilderBn'est pas le casabstract
sosite

1
Ce n'est pas une réponse à la question, car la classe de base peut ne pas être abstraite!
Roland

"Rendre abstrait le constructeur qui déclare le type générique" - et si je voulais utiliser ce constructeur directement?
marguerite le

21

Basée sur un article de blog , cette approche nécessite que toutes les classes non-feuilles soient abstraites, et toutes les classes 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 générateur, 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 feuille concrète qui peut appeler toutes les méthodes du générateur 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 l'une des classes de la hiérarchie:

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

Savez-vous pourquoi les classes feuilles doivent être définitives? J'aimerais que mes classes concrètes soient sous-classables, mais je n'ai pas trouvé de moyen de faire comprendre au compilateur le type de B, il s'avère toujours comme la classe de base.
David Ganster

Notez que la classe Builder dans LeafClass ne suit pas le même <T extends SomeClass, B extends SomeClass.Builder<T,B>> extends SomeClassParent.Builder<T,B>modèle que la classe intermédiaire SecondLevel, elle déclare à la place des types spécifiques. Vous ne pouvez pas installer une classe avant d'arriver à la feuille en utilisant les types spécifiques, mais une fois que vous le faites, vous ne pouvez plus l'étendre parce que vous utilisez les types spécifiques et que vous avez abandonné le modèle de modèle curieusement récurrent. Ce lien pourrait vous aider: angelikalanger.com/GenericsFAQ/FAQSections/…
Q23

7

Vous pouvez également remplacer la calories()méthode et la laisser renvoyer le générateur d'extension. Cela se 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);
        }
    }
    [...]
}

Ah, je ne le savais pas, car je viens d'un fond C ++. C'est une approche utile pour ce petit exemple, mais avec une classe à part entière, répétant toutes les méthodes devient une douleur, et une douleur sujette aux erreurs à cela. +1 pour m'apprendre quelque chose de nouveau, cependant!
Ken YN

Il me semble que cela ne résout rien. La raison (IMO) de sous-classer le parent est de réutiliser les méthodes parents sans les remplacer. Si les classes sont simplement des objets de valeur sans réelle logique dans les méthodes de générateur, sauf pour définir une valeur simple, alors l'appel de la méthode parente dans la méthode de substitution a peu ou pas de valeur.
Developer Dude

La réponse résout le problème décrit dans la question: le code utilisant le générateur se compile avec les deux ordres. Puisqu'une manière compile et l'autre non, je suppose qu'il doit y avoir une certaine valeur après tout.
Flavio

3

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

Définissez une interface, cette classe parent Builderhéritera:

public interface FactsBuilder<T> {

    public T calories(int val);
}

L'implémentation de NutritionFactsest presque la même (sauf pour l' Builderimplémentation de 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;
    }
}

Une Builderclasse enfant doit é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;
    }
}

Remarquez, c'est NutritionFacts.Builderun champ à l'intérieur GMOFacts.Builder(appelé baseBuilder). La méthode implémentée à partir de l' FactsBuilderinterface appelle baseBuilderla méthode 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 dans le constructeur au constructeur de classe parent doit passer correctement NutritionFacts.Builder:

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

L'implémentation complète de la 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 exemple complet de 3 niveaux d'héritage de plusieurs générateurs ressemblerait à ceci :

(Pour la version avec un constructeur de copie pour le constructeur, 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);
    }
}


Une 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

Si vous ne voulez pas attirer votre attention sur une ou trois parenthèses, ou peut-être ne pas vous sentir ... euh ... je veux dire ... toux ... le reste de votre équipe comprendra rapidement avec curiosité modèle générique 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 à super garantissent que la logique (le cas échéant) ajoutée aux méthodes de générateur de super type est conservée dans les sous-types.
  • L'inconvénient est la création d'objet faux dans la ou les classes parentes ... Mais voyez ci-dessous un moyen de nettoyer cela
  • Le côté positif est beaucoup plus facile à comprendre en un coup d'œil, et aucun constructeur verbeux ne transfère les propriétés.
  • Si vous avez plusieurs threads accédant à vos objets de construction ... Je suppose que je suis content de ne pas être vous :).

ÉDITER:

J'ai trouvé un moyen de contourner la création d'objet faux. 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


1

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

NutritionFacts.newBuilder()
GMOFacts.newBuilder()

Cette méthode de fabrique statique renverrait alors le générateur approprié. Vous pouvez avoir une GMOFacts.Builderextension a NutritionFacts.Builder, ce n'est pas un problème. LE problème ici sera de gérer la visibilité ...


0

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

Il dissèque la question originale en deux sous-problèmes de déficience d'héritage et de 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.


Cette réponse ne contient aucune information utile, ne contient pas au moins un résumé de la réponse donnée dans le lien et conduit à un lien qui nécessite une connexion.
Sonate le

Cette réponse renvoie à une publication de conférence évaluée par des pairs avec une autorité de publication officielle et une procédure officielle de publication et de partage.
mc00x1

0

J'ai créé une classe de générateur générique abstraite parente qui accepte deux paramètres de type formel. Le premier concerne le type d'objet retourné par build (), le second est le type renvoyé par chaque paramètre optionnel. Vous trouverez ci-dessous des cours pour parents et 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 avec satisfaction.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.