Subclasificar una clase de Java Builder


133

Presente este artículo del Dr. Dobbs , y el Patrón del constructor en particular, ¿cómo manejamos el caso de subclasificar un Constructor? Tomando una versión reducida del ejemplo donde queremos subclasificar para agregar el etiquetado de OGM, una implementación ingenua sería:

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

Subclase:

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

Ahora, podemos escribir código como este:

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

Pero, si nos equivocamos en el pedido, todo falla:

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

El problema es, por supuesto, que NutritionFacts.Builderdevuelve un NutritionFacts.Builder, no un GMOFacts.Builder, entonces, ¿cómo resolvemos este problema, o hay un mejor patrón para usar?

Nota: esta respuesta a una pregunta similar ofrece las clases que tengo arriba; Mi pregunta es sobre el problema de garantizar que las llamadas del constructor estén en el orden correcto.


1
Creo que el siguiente enlace describe un buen enfoque: egalluzzo.blogspot.co.at/2010/06/…
stuXnet

1
Pero, ¿cómo es build()la salida de b.GMO(true).calories(100)?
Sridhar Sarnobat

Respuestas:


170

Puedes resolverlo usando genéricos. Creo que esto se llama los "patrones genéricos curiosamente recurrentes"

Haga que el tipo de retorno de los métodos del generador de clases base sea un argumento genérico.

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

Ahora cree una instancia del generador base con el generador de clases derivado como argumento genérico.

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, creo que tendré que (a) publicar una nueva pregunta, (b) rediseñar con en implementslugar de extends, o (c) tirar todo a la basura. Ahora tengo un extraño error de compilación donde leafBuilder.leaf().leaf()y leafBuilder.mid().leaf()está bien, pero leafBuilder.leaf().mid().leaf()falla ...
Ken YN

11
@gkamal return (T) this;da como resultado una unchecked or unsafe operationsadvertencia. Esto es imposible de evitar, ¿verdad?
Dmitry Minkovsky

55
Para resolver la unchecked castadvertencia, vea la solución sugerida a continuación entre las otras respuestas: stackoverflow.com/a/34741836/3114959
Stepan Vavra

8
Tenga en cuenta que en Builder<T extends Builder>realidad es un tipo sin formato , esto debería ser Builder<T extends Builder<T>>.
Boris the Spider

2
@ user2957378 el Builderfor GMOFactstambién debe ser genérico Builder<B extends Builder<B>> extends NutritionFacts.Builder<Builder>, y este patrón puede continuar tantos niveles como sea necesario. Si declara un constructor no genérico, no puede extender el patrón.
Boris the Spider

44

Solo para el registro, para deshacerse de la

unchecked or unsafe operations advertencia

para la return (T) this;declaración de la que hablan @dimadima y @Thomas N., la siguiente solución se aplica en ciertos casos.

Cree abstractel generador que declara el tipo genérico ( T extends Builderen este caso) y declare protected abstract T getThis()el método abstracto de la siguiente manera:

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

Consulte http://www.angelikalanger.com/GenericsFAQ/FAQSections/ProgrammingIdioms.html#FAQ205 para obtener más detalles.


¿Por qué el build()método devuelve los Datos de Nutrición aquí?
mvd

@mvd ¿Porque esta es una respuesta a la pregunta? En los subtipos, lo anulará comopublic GMOFacts build() { return new GMOFacts(this); }
Stepan Vavra

El problema ocurre cuando queremos agregar el 2º hijo BuilderC extends BuilderBy BuilderB extends BuilderAcuando BuilderBno lo esabstract
sosite

1
¡Esta no es una respuesta a la pregunta, porque la clase base puede no ser abstracta!
Roland

"Hacer abstracto el generador que declara el tipo genérico": ¿y si quisiera usar ese generador directamente?
Margarita

21

Basado en una publicación de blog , este enfoque requiere que todas las clases no hoja sean abstractas, y todas las clases hoja deben ser 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;
        }
    }
}

Luego, tiene alguna clase intermedia que extiende esta clase y su generador, y tantas más como necesite:

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

Y, finalmente, una clase de hoja concreta que puede llamar a todos los métodos de construcción en cualquiera de sus padres en cualquier orden:

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

Luego, puede llamar a los métodos en cualquier orden, desde cualquiera de las clases en la jerarquía:

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

¿Sabes por qué las clases de hoja deben ser finales? Me gustaría que mis clases concretas sean subclasables, pero no he encontrado una manera de hacer que el compilador comprenda el tipo de B, siempre resulta ser la clase base.
David Ganster

Observe cómo la clase Builder en LeafClass no sigue el mismo <T extends SomeClass, B extends SomeClass.Builder<T,B>> extends SomeClassParent.Builder<T,B>patrón que la clase SecondLevel intermedia, sino que declara tipos específicos. No puede instalar una clase hasta que llegue a la hoja usando los tipos específicos, pero una vez que lo haga, no puede extenderla más porque está usando los tipos específicos y ha abandonado el Patrón de plantilla curiosamente recurrente. Este enlace podría ayudar: angelikalanger.com/GenericsFAQ/FAQSections/…
Q23

7

También puede anular el calories()método y dejar que devuelva el generador de extensión. Esto se compila porque Java admite tipos de retorno covariantes .

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, no lo sabía, ya que vengo de un fondo C ++. Ese es un enfoque útil para este pequeño ejemplo, pero con una clase completa que repite todos los métodos se convierte en un dolor y un dolor propenso a errores. ¡+1 por enseñarme algo nuevo, sin embargo!
Ken YN

Me parece que esto no resuelve nada. La razón (IMO) para subclasificar al padre es reutilizar los métodos de los padres sin anularlos. Si las clases son simplemente objetos de valor sin lógica real en los métodos de construcción, excepto para establecer un valor simple, entonces llamar al método padre en el método de anulación tiene poco o ningún valor.
Desarrollador Dude

La respuesta resuelve el problema descrito en la pregunta: el código que usa el constructor compila con ambos ordenamientos. Dado que una forma compila y la otra no, supongo que debe haber algún valor después de todo.
Flavio

3

También hay otra forma de crear clases de acuerdo con el Builderpatrón, que se ajusta a "Preferir composición sobre herencia".

Defina una interfaz, esa clase padre Builderheredará:

public interface FactsBuilder<T> {

    public T calories(int val);
}

La implementación de NutritionFactses casi la misma (excepto para Builderimplementar la interfaz '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 Builderde una clase secundaria debería extender la misma interfaz (excepto una implementación genérica diferente):

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

Tenga en cuenta que NutritionFacts.Builderes un campo dentro GMOFacts.Builder(llamado baseBuilder). El método implementado desde el método de FactsBuilderllamadas de interfaz baseBuilderdel mismo nombre:

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

También hay un gran cambio en el constructor de GMOFacts(Builder builder). La primera llamada en el constructor al constructor de la clase padre debe pasar apropiado NutritionFacts.Builder:

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

La implementación completa de la GMOFactsclase:

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 ejemplo completo de 3 niveles de herencia de constructor múltiple se vería así :

(Para la versión con un constructor de copia para el generador, vea el segundo ejemplo a continuación)

Primer nivel - padre (potencialmente abstracto)

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

Segundo nivel

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

Tercer nivel

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

Y un ejemplo de uso

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


Una versión un poco más larga con un constructor de copia para el constructor:

Primer nivel - padre (potencialmente abstracto)

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

Segundo nivel

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

Tercer nivel

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

Y un ejemplo de uso

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 no quieres enfocarte en un par de ángulos o tres, o tal vez no te sientas ... umm ... quiero decir ... tos ... el resto de tu equipo comprenderá rápidamente con curiosidad patrón genérico recurrente, puede hacer esto:

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

Apoyado por

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

y el tipo principal:

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 + '\'' +
        '}';
  }
}

Puntos clave:

  • Encapsule el objeto en el generador de modo que la herencia le impida establecer el campo en el objeto contenido en el tipo primario
  • Las llamadas a super aseguran que la lógica (si la hay) agregada a los métodos del generador de super tipos se conserva en los subtipos.
  • El lado negativo es la creación de objetos espurios en la (s) clase (s) principal (es) ... Pero vea a continuación una forma de limpiar eso
  • El lado positivo es mucho más fácil de entender de un vistazo, y no hay propiedades de transferencia de constructor detallado.
  • Si tiene múltiples hilos accediendo a sus objetos de construcción ... Supongo que me alegro de no ser usted :).

EDITAR:

Encontré una forma de evitar la creación de objetos espurios. Primero agregue esto a cada constructor:

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

Luego, en el constructor para cada constructor:

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

El costo es un archivo de clase adicional para la new Object(){}clase interna anónima


1

Una cosa que podría hacer es crear un método de fábrica estático en cada una de sus clases:

NutritionFacts.newBuilder()
GMOFacts.newBuilder()

Este método de fábrica estático devolvería el generador apropiado. Puede tener una GMOFacts.Builderextensión a NutritionFacts.Builder, eso no es un problema. El problema aquí será lidiar con la visibilidad ...


0

La siguiente contribución de IEEE Refined Fluent Builder en Java brinda una solución integral al problema.

Disecciona la pregunta original en dos subproblemas de deficiencia de herencia y cuasi invariancia y muestra cómo se abre una solución a estos dos subproblemas para el soporte de herencia con la reutilización de código en el patrón clásico de construcción en Java.


Esta respuesta no contiene ninguna información útil, no contiene al menos un resumen de la respuesta dada en el enlace y conduce a un enlace que requiere inicio de sesión.
Sonata

Esta respuesta se vincula a una publicación de conferencia revisada por pares con una autoridad editorial oficial y un procedimiento oficial de publicación y uso compartido.
mc00x1

0

Creé una clase de generador genérica abstracta y principal que acepta dos parámetros de tipo formal. El primero es para el tipo de objeto devuelto por build (), el segundo es el tipo devuelto por cada configurador de parámetros opcional. A continuación se encuentran las clases para padres e hijos con fines ilustrativos:

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

Este ha satisfecho mis necesidades a satisfacción.

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.