164 votes

Sous-classement d'une classe Java Builder

Donnez cet article du Dr Dobbs et le modèle Builder en particulier, comment gérer le cas de la sous-classification d'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 la suivante :

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 un code comme celui-ci :

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

Mais, si nous nous trompons dans l'ordre, 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 et non un GMOFacts.Builder Alors comment résoudre ce problème, ou y a-t-il un meilleur modèle à utiliser ?

Nota: 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 au constructeur sont dans le bon ordre.

209voto

gkamal Points 9679

Vous pouvez résoudre ce problème en utilisant des génériques. Je pense que cela s'appelle le "Des modèles génériques curieusement récurrents"

Faire du type de retour des méthodes du constructeur de la 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 constructeur de base avec le constructeur de la classe dérivée 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;
    }
}

55voto

Stepan Vavra Points 2482

Juste pour info, pour se débarrasser de la

unchecked or unsafe operations avertissement

pour le return (T) this; comme @dimadima et @Thomas N. le mentionnent, 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 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.

22voto

Q23 Points 461

Sur la base de un article de blog Cette approche exige que toutes les classes non feuilles soient abstraites et que toutes les classes feuilles 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 dont vous 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 constructeur sur n'importe lequel 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 quelle classe de la hiérarchie :

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

7voto

Flavio Points 5369

Vous pouvez également remplacer l'option calories() et qu'elle renvoie le constructeur d'extension. Cela se compile parce que Java supporte 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);
        }
    }
    [...]
}

3voto

R. Zagórski Points 13234

Il existe également une autre façon de créer des classes selon les critères suivants Builder qui est conforme à la règle "Préférer la composition à l'héritage".

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

public interface FactsBuilder<T> {

    public T calories(int val);
}

La mise en œuvre de NutritionFacts est presque le 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;
    }
}

El Builder d'une classe enfant doivent é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;
    }
}

Remarquez, que NutritionFacts.Builder est un champ à l'intérieur de GMOFacts.Builder (appelé baseBuilder ). La méthode mise en œuvre à partir de FactsBuilder appels d'interface baseBuilder de la 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 au constructeur de la classe parente dans le constructeur de la classe doit passer les informations appropriées. NutritionFacts.Builder :

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

La mise en œuvre complète de GMOFacts classe :

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X