patron - Subclasificación de una clase de Java Builder
metodos en java (8)
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
<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 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
<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 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<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();
}
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 etiquetas 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.
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<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 cumplido mis necesidades de satisfacción.
Puedes resolverlo usando genéricos. Creo que esto se llama "patrones genéricos 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<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 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<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;
}
}
Si no quieres echarle un ojo a un ángulo o tres, o tal vez no te sientes ... umm ... quiero decir ... tos ... el resto de tu equipo comprenderá rápidamente patrón recurrente de genéricos, 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(){}
Solo para el registro, para deshacerse de la
advertencia de
unchecked or unsafe operations
para el return (T) this;
declaración 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<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.
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<T> {
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<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
de una clase hija debe extender la misma interfaz (excepto la 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.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<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;
}
}
También puede anular el método de las calories()
y dejar que devuelva el constructor que se extiende. 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);
}
}
[...]
}
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 ...