Sottoclassi di una class Java Builder

Dare questo articolo del Dr Dobbs e in particolare il Pattern Builder, come gestiamo il caso della sottoclassi di un Builder? Prendendo una versione ridotta dell’esempio in cui vogliamo sottoclass per aggiungere l’etichettatura OGM, un’implementazione ingenua sarebbe:

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

sottoclass:

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

Ora, possiamo scrivere un codice come questo:

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

Ma se otteniamo l’ordine sbagliato, tutto fallisce:

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

Il problema è ovviamente che NutritionFacts.Builder restituisce un NutritionFacts.Builder , non un GMOFacts.Builder , quindi come risolviamo questo problema, o c’è un Pattern migliore da usare?

Nota: questa risposta a una domanda simile offre le classi che ho sopra; la mia domanda riguarda il problema di garantire che le chiamate del costruttore siano nell’ordine corretto.

Puoi risolverlo usando i farmaci generici. Penso che questo sia chiamato “Modelli generici che ricorrono in modo curioso”

Rendi il tipo di ritorno dei metodi del builder della class base un argomento generico.

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

Ora creare un’istanza del builder di base con il generatore di classi derivate come argomento generico.

 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 per la cronaca, per sbarazzarsi del

avviso unchecked or unsafe operations

per il return (T) this; la dichiarazione come @dimadima e @Thomas N. parlano di, la seguente soluzione si applica in alcuni casi.

Rendi abstract il builder che dichiara il tipo generico ( T extends Builder in questo caso) e dichiara il metodo astratto astratto protected abstract T getThis() come segue:

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

Fare riferimento a http://www.angelikalanger.com/GenericsFAQ/FAQSections/ProgrammingIdioms.html#FAQ205 per ulteriori dettagli.

Basato su un post del blog , questo approccio richiede che tutte le classi non foglia siano astratte e tutte le classi foglia devono essere definitive.

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

Quindi, hai una class intermedia che estende questa class e il suo builder, e tanti altri di cui hai bisogno:

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

E, infine, una class foglia concreta che può chiamare tutti i metodi di costruzione su uno qualsiasi dei suoi genitori in qualsiasi ordine:

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

Quindi, puoi chiamare i metodi in qualsiasi ordine, da una qualsiasi delle classi nella gerarchia:

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

È ansible sovrascrivere anche il metodo calories() e lasciare che restituisca il builder estendibile. Questo viene compilato perché Java supporta i tipi di ritorno covarianti .

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

Se non vuoi attirare l’attenzione su una parentesi angular o tre, o forse non sentirti … umm … voglio dire … tossire … il resto della tua squadra comprenderà rapidamente schema generico ricorrente, puoi fare questo:

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

sostenuto da

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

e il tipo genitore:

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

Punti chiave:

  • Incapsula l’object nel builder in modo che l’ereditarietà ti impedisca di impostare il campo sull’object conservato nel tipo genitore
  • Chiama per garantire che la logica (se presente) aggiunta ai metodi del builder di tipo super venga mantenuta nei sottotipi.
  • Il lato negativo è la creazione di oggetti spuri nelle classi genitore … Ma vedi sotto per un modo per ripulirlo
  • Il lato superiore è molto più facile da capire a prima vista e nessun costrutto prolisso che trasferisce le proprietà.
  • Se hai più thread che accedono agli oggetti del tuo costruttore … Suppongo di essere contento di non essere te :).

MODIFICARE:

Ho trovato un modo per aggirare la creazione di oggetti spuri. Prima aggiungilo a ciascun costruttore:

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

Quindi nel costruttore per ogni costruttore:

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

Il costo è un file di class extra per la new Object(){} class interna new Object(){} anonima

Esiste anche un altro modo per creare classi in base al modello Builder , che è conforms a “Preferisci la composizione sull’ereditarietà”.

Definire un’interfaccia, che Genitore di class genitore erediterà:

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

L’implementazione di NutritionFacts è quasi la stessa (tranne per Builder implementa l’interfaccia “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; } } 

La class Builder di un figlio dovrebbe estendere la stessa interfaccia (eccetto diverse implementazioni generiche):

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

Si noti che NutritionFacts.Builder è un campo all’interno di GMOFacts.Builder (chiamato baseBuilder ). Il metodo implementato dall’interfaccia FactsBuilder chiama il metodo baseBuilder con lo stesso nome:

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

C’è anche un grande cambiamento nel costruttore di GMOFacts(Builder builder) . La prima chiamata nel costruttore al costruttore della class padre deve passare appropriata. NutritionFacts.Builder :

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

La piena attuazione della lezione sugli 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 che potresti fare è creare un metodo factory statico in ciascuna delle tue classi:

 NutritionFacts.newBuilder() GMOFacts.newBuilder() 

Questo metodo factory statico restituirebbe quindi il builder appropriato. Puoi avere un GMOFacts.Builder estende un NutritionFacts.Builder , non è un problema. Il problema qui sarà di affrontare la visibilità …

Un esempio completo di livello 3 di ereditarietà di più builder sarà simile a questo :

(Per la versione con un costruttore di copia per il costruttore vedi il secondo esempio di seguito)

Primo livello – genitore (potenzialmente astratto)

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

Secondo livello

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

Terzo livello

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

E un esempio di utilizzo

 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 versione un po ‘più lunga con un costruttore di copia per il costruttore:

Primo livello – genitore (potenzialmente astratto)

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

Secondo livello

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

Terzo livello

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

E un esempio di utilizzo

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

Ho creato un genitore, una class generica di generatori astratti che accetta due parametri di tipo formale. Il primo è per il tipo di object restituito da build (), il secondo è il tipo restituito da ciascun setter di parametri opzionale. Di seguito sono elencate le classi genitore e figlio a scopo illustrativo:

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

Questo ha soddisfatto i miei bisogni di soddisfazione.