Patrón de constructor en Java efectivo


137

Recientemente comencé a leer Effective Java de Joshua Bloch. Me pareció muy interesante la idea del patrón Builder [Elemento 2 del libro]. Traté de implementarlo en mi proyecto pero hubo errores de compilación. Lo siguiente es, en esencia, lo que estaba tratando de hacer:

La clase con múltiples atributos y su clase de constructor:

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

Clase donde trato de usar la clase anterior:

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

Recibo el siguiente error del compilador:

se requiere una instancia de cierre que contenga efectivajava.BuilderPattern.NutritionalFacts.Builder NutritionalFacts n = new NutritionalFacts.Builder (10) .carbo (23) .fat (1) .build ();

No entiendo lo que significa el mensaje. Por favor explique. El código anterior es similar al ejemplo sugerido por Bloch en su libro.


Respuestas:


171

Haz del constructor una staticclase. Entonces funcionará. Si no es estático, requeriría una instancia de su clase propietaria, y el punto es no tener una instancia de la misma, e incluso prohibir hacer instancias sin el generador.

public class NutritionFacts {
    public static class Builder {
    }
}

Referencia: clases anidadas


34
Y, de hecho, Builderestá staticen el ejemplo en el libro (página 14, línea 10 en la 2da edición).
Powerlord

27

Debe hacer que la clase Builder sea estática y también debe hacer que los campos sean finales y tener captadores para obtener esos valores. No proporcione definidores a esos valores. De esta manera tu clase será perfectamente inmutable.

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

Y ahora puede establecer las propiedades de la siguiente manera:

NutritionalFacts n = new NutritionalFacts.Builder().sodium(10).carbo(15).
fat(5).build();

¿Por qué no solo hacer públicos los campos de NutritionalFacts? Ya son finales, y aún sería inmutable.
skia.heliou

finallos campos solo tienen sentido si los campos siempre son necesarios durante la inicialización. Si no, entonces los campos no deberían ser final.
Piotrek Hryciuk

12

Está intentando acceder a una clase no estática de forma estática. Cambiar Builderastatic class Builder y debería funcionar.

El uso de ejemplo que da falla porque no hay una instancia de Builderpresente. Siempre se crea una instancia de una clase estática para todos los fines prácticos. Si no lo hace estático, deberá decir:

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

Porque necesitarías construir uno nuevo Buildercada vez.




5

Una vez que tenga una idea, en la práctica, puede encontrar que Lombok es @Buildermucho más conveniente.

@Builder le permite producir automáticamente el código requerido para que su clase sea instanciable con código como:

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

Documentación oficial: https://www.projectlombok.org/features/Builder


4

Esto significa que no puede crear el tipo de cierre. Esto significa que primero debe crear una instancia de la clase "padre" y luego, desde esta instancia, puede crear instancias de clase anidadas.

NutritionalFacts n = new NutritionalFacts()

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

Clases anidadas


3
eso no tiene mucho sentido, porque necesita que el constructor construya los "hechos", no al revés.
Bozho

55
Es cierto que si nos centramos en el patrón de construcción, me concentré solo en "No entiendo lo que significa el mensaje" y presenté una de las dos soluciones.
Damian Leszczyński - Vash

3

La clase Builder debe ser estática. No tengo tiempo en este momento para probar el código más allá de eso, pero si no funciona, avíseme y volveré a analizarlo.


1

Personalmente prefiero usar el otro enfoque, cuando tienes 2 clases diferentes. Entonces no necesitas ninguna clase estática. Esto es básicamente para evitar escribir Class.Buildercuando tiene que crear una nueva instancia.

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

Entonces, puedes usar tu constructor así:

Person person = new PersonBuilder("attr1")
                            .setAttr2("attr2")
                            .build();

0

Como muchos ya dijeron aquí, necesitas hacer la clase static. Solo una pequeña adición: si lo desea, hay una forma un poco diferente sin una estática.

Considera esto. Implementando un constructor declarando algo así como los withProperty(value)creadores de tipos dentro de la clase y haciendo que devuelvan una referencia a sí mismo. En este enfoque, tiene una clase única y elegante que es un hilo seguro y conciso.

Considera esto:

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

Compruébalo para ver más ejemplos de Java Builder .


0

Debe cambiar la clase de generador a generador de clase estática . Entonces funcionará bien.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.