Modèle de générateur dans Java efficace

137

J'ai récemment commencé à lire Effective Java de Joshua Bloch. J'ai trouvé l'idée du modèle Builder [Item 2 dans le livre] vraiment intéressante. J'ai essayé de l'implémenter dans mon projet mais il y a eu des erreurs de compilation. Voici ce que j'essayais de faire:

La classe avec plusieurs attributs et sa classe de générateur:

public class NutritionalFacts {
    private int sodium;
    private int fat;
    private int carbo;

    public class Builder {
        private int sodium;
        private int fat;
        private int carbo;

        public Builder(int s) {
            this.sodium = s;
        }

        public Builder fat(int f) {
            this.fat = f;
            return this;
        }

        public Builder carbo(int c) {
            this.carbo = c;
            return this;
        }

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

    private NutritionalFacts(Builder b) {
        this.sodium = b.sodium;
        this.fat = b.fat;
        this.carbo = b.carbo;
    }
}

Classe où j'essaye d'utiliser la classe ci-dessus:

public class Main {
    public static void main(String args[]) {
        NutritionalFacts n = 
            new NutritionalFacts.Builder(10).carbo(23).fat(1).build();
    }
}

J'obtiens l'erreur de compilateur suivante:

une instance englobante qui contient effectivejava.BuilderPattern.NutritionalFacts.Builder est requise NutritionalFacts n = new NutritionalFacts.Builder (10) .carbo (23) .fat (1) .build ();

Je ne comprends pas ce que signifie le message. S'il vous plaît, expliquez. Le code ci-dessus est similaire à l'exemple suggéré par Bloch dans son livre.

Swaranga Sarma
la source

Réponses:

171

Faites du constructeur une staticclasse. Ensuite, cela fonctionnera. S'il n'est pas statique, il nécessiterait une instance de sa classe propriétaire - et le but est de ne pas en avoir d'instance, et même d'interdire la création d'instances sans le générateur.

public class NutritionFacts {
    public static class Builder {
    }
}

Référence: classes imbriquées

Bozho
la source
34
Et, en fait, Builderest staticdans l'exemple du livre (page 14, ligne 10 dans la 2e édition).
Powerlord
27

Vous devez rendre la classe Builder statique et vous devez également rendre les champs finaux et avoir des getters pour obtenir ces valeurs. Ne fournissez pas de setters à ces valeurs. De cette façon, votre classe sera parfaitement immuable.

public class NutritionalFacts {
    private final int sodium;
    private final int fat;
    private final int carbo;

    public int getSodium(){
        return sodium;
    }

    public int getFat(){
        return fat;
    }

    public int getCarbo(){
        return carbo;
    }

    public static class Builder {
        private int sodium;
        private int fat;
        private int carbo;

        public Builder sodium(int s) {
            this.sodium = s;
            return this;
        }

        public Builder fat(int f) {
            this.fat = f;
            return this;
        }

        public Builder carbo(int c) {
            this.carbo = c;
            return this;
        }

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

    private NutritionalFacts(Builder b) {
        this.sodium = b.sodium;
        this.fat = b.fat;
        this.carbo = b.carbo;
    }
}

Et maintenant, vous pouvez définir les propriétés comme suit:

NutritionalFacts n = new NutritionalFacts.Builder().sodium(10).carbo(15).
fat(5).build();
Raj Hassani
la source
Pourquoi ne pas simplement rendre publics les champs NutritionalFacts? Ils sont déjà définitifs, et ce serait toujours immuable.
skia.heliou
finalLes champs n'ont de sens que si les champs sont toujours nécessaires lors de l'initialisation. Sinon, les champs ne devraient pas l'être final.
Piotrek Hryciuk
12

Vous essayez d'accéder à une classe non statique de manière statique. Changez Builderpour static class Builderet cela devrait fonctionner.

L'exemple d'utilisation que vous donnez échoue car il n'y a aucune instance de Builderpresent. Une classe statique à toutes fins pratiques est toujours instanciée. Si vous ne le rendez pas statique, vous devrez dire:

Widget = new Widget.Builder(10).setparm1(1).setparm2(3).build();

Parce que vous auriez besoin d'en construire un nouveau à Builderchaque fois.

Michael K
la source
12

Pour générer un constructeur interne dans Intellij IDEA, consultez ce plugin: https://github.com/analytically/innerbuilder

analytiquement
la source
2
Cela n'a rien à voir avec la question posée mais très utile! Bonne trouvaille!
The Hungry Androider
5

Une fois que vous avez une idée, dans la pratique, vous trouverez peut-être le lombok @Builderbeaucoup plus pratique.

@Builder vous permet de produire automatiquement le code requis pour que votre classe soit instanciable avec du code tel que:

Person.builder()
  .name("Adam Savage")
  .city("San Francisco")
  .job("Mythbusters")
  .job("Unchained Reaction")
 .build(); 

Documentation officielle: https://www.projectlombok.org/features/Builder

torina
la source
4

Cela signifie que vous ne pouvez pas créer de type fermé. Cela signifie que vous devez d'abord créer une instance de classe "parent", puis à partir de cette instance, vous pouvez créer des instances de classe imbriquées.

NutritionalFacts n = new NutritionalFacts()

Builder b = new n.Builder(10).carbo(23).fat(1).build();

Classes imbriquées

Damian Leszczyński - Vash
la source
3
cela n'a pas beaucoup de sens, car il a besoin du constructeur pour construire les «faits», et non l'inverse.
Bozho
5
vrai si nous nous concentrons sur le modèle de constructeur, je me suis concentré uniquement sur "Je ne comprends pas ce que signifie le message" et j'ai présenté l'une des deux solutions.
Damian Leszczyński - Vash
3

La classe Builder doit être statique. Je n'ai pas le temps pour le moment de tester le code au-delà de cela, mais si cela ne fonctionne pas, faites-le moi savoir et je vais jeter un autre coup d'œil.

Shaun
la source
1

Personnellement, je préfère utiliser l'autre approche, lorsque vous avez 2 classes différentes. Vous n'avez donc besoin d'aucune classe statique. C'est essentiellement pour éviter d'écrire Class.Builderlorsque vous devez créer une nouvelle instance.

public class Person {
    private String attr1;
    private String attr2;
    private String attr3;

    // package access
    Person(PersonBuilder builder) {
        this.attr1 = builder.getAttr1();
        // ...
    }

    // ...
    // getters and setters 
}

public class PersonBuilder (
    private String attr1;
    private String attr2;
    private String attr3;

    // constructor with required attribute
    public PersonBuilder(String attr1) {
        this.attr1 = attr1;
    }

    public PersonBuilder setAttr2(String attr2) {
        this.attr2 = attr2;
        return this;
    }

    public PersonBuilder setAttr3(String attr3) {
        this.attr3 = attr3;
        return this;
    }

    public Person build() {
        return new Person(this);
    }
    // ....
}

Ainsi, vous pouvez utiliser votre constructeur comme ceci:

Person person = new PersonBuilder("attr1")
                            .setAttr2("attr2")
                            .build();
empreintes
la source
0

Comme beaucoup l'ont déjà dit ici, vous devez faire le cours static. Juste un petit ajout - si vous voulez, il y a une manière un peu différente sans statique.

Considère ceci. Implémenter un générateur en déclarant quelque chose comme des withProperty(value)types setters à l'intérieur de la classe et en leur faisant renvoyer une référence à lui-même. Dans cette approche, vous avez une classe unique et élégante qui est un thread sûr et concis.

Considère ceci:

public class DataObject {

    private String first;
    private String second;
    private String third;

    public String getFirst(){
       return first; 
    }

    public void setFirst(String first){
       this.first = first; 
    }

    ... 

    public DataObject withFirst(String first){
       this.first = first;
       return this; 
    }

    public DataObject withSecond(String second){
       this.second = second;
       return this; 
    }

    public DataObject withThird(String third){
       this.third = third;
       return this; 
    }
}


DataObject dataObject = new DataObject()
     .withFirst("first data")
     .withSecond("second data")
     .withThird("third data");

Consultez-le pour plus d' exemples Java Builder .

Johnny
la source
0

Vous devez changer la classe Builder en classe statique Builder . Ensuite, cela fonctionnera très bien.

Krishna Kirti
la source