Subclasificación de una clase de Java Builder

Presente este artículo del Dr. Dobbs y, en particular, el Patrón del constructor, ¿cómo manejamos el caso de la creación de subclases de un Generador? Tomando una versión reducida del ejemplo en el que queremos crear una subclase para agregar tags 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 un código como este:

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

Pero, si hacemos el pedido incorrecto, todo falla:

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

El problema es, por supuesto, que NutritionFacts.Builder devuelve 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 generador estén en el orden correcto.

Puedes resolverlo usando generics. Creo que esto se llama “patrones generics curiosamente recurrentes”

Haga que el tipo de devolución 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> { 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 crea una instancia del generador de bases con el generador de clases derivado como el argumento genérico.

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

Solo para el registro, para deshacerse de la

advertencia de unchecked or unsafe operations

para el return (T) this; statement como @dimadima y @Thomas N. hablamos, la siguiente solución se aplica en ciertos casos.

Haga abstract el constructor que declara el tipo genérico ( T extends Builder en este caso) y declare el método abstracto protected abstract T getThis() siguiente manera:

 public abstract static class Builder> { 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.

Basado en una publicación de blog , este enfoque requiere que todas las clases que no sean hojas sean abstractas, y todas las clases de hoja deben ser definitivas.

 public abstract class TopLevel { protected int foo; protected TopLevel() { } protected static abstract class Builder > { 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 una clase intermedia que amplía esta clase y su generador, y tantos más como necesite:

 public abstract class SecondLevel extends TopLevel { protected int bar; protected static abstract class Builder > extends TopLevel.Builder { public B bar(int bar) { object.bar = bar; return thisObject; } } } 

Y, finalmente, una clase de hoja de hormigón 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 { 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(); } 

También puede anular el método de las calories() y dejar que devuelva el constructor que se extiende. Esto se comstack 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); } } [...] } 

Si no quieres echarle un ojo a un ángulo o tres, o tal vez no te sientes … umm … quiero decir … tos … el rest de tu equipo comprenderá rápidamente patrón recurrente de generics, puedes 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 para que la herencia le impida configurar el campo en el objeto que se encuentra en el tipo padre
  • Llamadas para asegurar súper que la lógica (si la hay) agregada a los métodos del generador de tipo super se retenga en los subtipos.
  • El lado negativo es la creación de objetos falsos en la (s) clase (s) primaria (s) … Pero mira a continuación una forma de limpiar eso
  • El lado de arriba es mucho más fácil de entender de un vistazo, y no tiene propiedades de transferencia del constructor detallado.
  • Si tienes varios hilos que acceden a tus objetos de construcción … supongo que me alegro de que no seas tú :).

EDITAR:

Encontré una forma de evitar la creación de objetos espurios. Primero agrega 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 new Object(){}

También hay otra manera de crear clases de acuerdo con el patrón del Builder , que cumple con “Prefiere la composición sobre la herencia”.

Defina una interfaz, ese generador de clase padre heredará:

 public interface FactsBuilder { public T calories(int val); } 

La implementación de NutritionFacts es casi la misma (a excepción de Builder implementa la interfaz ‘FactsBuilder’):

 public class NutritionFacts { private final int calories; public static class Builder implements FactsBuilder { 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 de una clase hija debe extender la misma interfaz (excepto la implementación genérica diferente):

 public static class Builder implements FactsBuilder { 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.Builder es un campo dentro de GMOFacts.Builder (llamado baseBuilder ). El método implementado desde la interfaz FactsBuilder llama baseBuilder método de baseBuilder del 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 clase GMOFacts :

 public class GMOFacts extends NutritionFacts { private final boolean hasGMO; public static class Builder implements FactsBuilder { 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; } } 

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

 NutritionFacts.newBuilder() GMOFacts.newBuilder() 

Este método de fábrica estática devolvería el constructor apropiado. Puedes hacer que un GMOFacts.Builder extienda un NutritionFacts.Builder , eso no es un problema. EL problema aquí será lidiar con la visibilidad …

Un ejemplo completo de 3 niveles de herencia de constructores múltiples se vería así :

(Para la versión con un constructor de copia para el constructor, 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 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> extends Class1.Builder { 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> extends Class2.Builder { 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 copias 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 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> extends Class1.Builder { 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> extends Class2.Builder { 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); } } 

Creé un padre, una clase de generador genérico abstracto que acepta dos parámetros de tipo formales. Primero es para el tipo de objeto devuelto por build (), el segundo es el tipo devuelto por cada setter de parámetros opcional. A continuación se encuentran las clases para padres e hijos con fines ilustrativos:

 **Parent** public abstract static class Builder> { // Required parameters private final String name; // Optional parameters private List outputFields = null; public Builder(String pName) { name = pName; } public U outputFields(List 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 { // Required parameters private final Map nameValuePairsToAdd; // Optional parameters private String fooBar; Builder(String pName, Map pNameValPairs) { super(pName); /** * Must do this, in case client code (Ie 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 cumplido mis necesidades de satisfacción.