Podaj ten artykuł Dr Dobbs , aw szczególności wzorzec Builder, jak radzimy sobie z przypadkiem podklasy Buildera? Biorąc okrojoną wersję przykładu, w którym chcemy podklasę, aby dodać etykietowanie GMO, naiwna implementacja byłaby taka:
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;
}
}
Podklasa:
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;
}
}
Teraz możemy napisać taki kod:
GMOFacts.Builder b = new GMOFacts.Builder();
b.GMO(true).calories(100);
Ale jeśli źle zamówimy, wszystko zawiedzie:
GMOFacts.Builder b = new GMOFacts.Builder();
b.calories(100).GMO(true);
Problem polega oczywiście na tym, że NutritionFacts.Builder
zwraca a NutritionFacts.Builder
, a nie a GMOFacts.Builder
, więc jak rozwiązać ten problem, czy też istnieje lepszy wzorzec do użycia?
Uwaga: ta odpowiedź na podobne pytanie zawiera klasy, które mam powyżej; moje pytanie dotyczy problemu z zapewnieniem, że wywołania budowniczych są we właściwej kolejności.
java
design-patterns
Ken YN
źródło
źródło
build()
wynikb.GMO(true).calories(100)
?Odpowiedzi:
Możesz go rozwiązać za pomocą typów ogólnych. Myślę, że to się nazywa „Ciekawie powtarzające się ogólne wzorce”
Ustaw zwracany typ metod konstruktora klasy bazowej jako argument ogólny.
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; } }
Teraz utwórz wystąpienie konstruktora podstawowego z konstruktorem klas pochodnych jako argumentem ogólnym.
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; } }
źródło
implements
zamiastextends
lub (c) wyrzucić wszystko. Mam teraz dziwny błąd kompilacji, gdzieleafBuilder.leaf().leaf()
ileafBuilder.mid().leaf()
jest OK, aleleafBuilder.leaf().mid().leaf()
nie udaje mi się ...return (T) this;
powoduje wyświetlenieunchecked or unsafe operations
ostrzeżenia. Tego nie da się uniknąć, prawda?unchecked cast
ostrzeżenie, zobacz rozwiązanie sugerowane poniżej, obok innych odpowiedzi: stackoverflow.com/a/34741836/3114959Builder<T extends Builder>
rzeczywistości jest to typ surowy - tak powinno byćBuilder<T extends Builder<T>>
.Builder
dlaGMOFacts
również muszą mieć charakter ogólnyBuilder<B extends Builder<B>> extends NutritionFacts.Builder<Builder>
- i ten wzór można kontynuować w dół jak wielu poziomach, jak wymagane. Jeśli zadeklarujesz nieogólny program budujący, nie możesz rozszerzyć wzorca.Tak dla porządku, żeby pozbyć się pliku
w przypadku
return (T) this;
stwierdzenia, o którym mówią @dimadima i @Thomas N., w niektórych przypadkach ma zastosowanie następujące rozwiązanie.Utwórz
abstract
konstruktora, który deklaruje typ ogólny (T extends Builder
w tym przypadku) i zadeklarujprotected abstract T getThis()
metodę abstrakcyjną w następujący sposób: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); } }
Więcej informacji można znaleźć pod adresem http://www.angelikalanger.com/GenericsFAQ/FAQSections/ProgrammingIdioms.html#FAQ205 .
źródło
build()
metoda zwraca tutaj NutrutionFacts?public GMOFacts build() { return new GMOFacts(this); }
BuilderC extends BuilderB
aBuilderB extends BuilderA
kiedyBuilderB
nieabstract
Opierając się na poście na blogu , podejście to wymaga, aby wszystkie klasy niebędące liśćmi były abstrakcyjne, a wszystkie klasy liści muszą być ostateczne.
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; } } }
Następnie masz klasę pośrednią, która rozszerza tę klasę i jej konstruktora, a także tyle, ile potrzebujesz:
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; } } }
I wreszcie konkretna klasa liścia, która może wywoływać wszystkie metody konstruktora dowolnego ze swoich elementów nadrzędnych w dowolnej kolejności:
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; } } }
Następnie możesz wywoływać metody w dowolnej kolejności, z dowolnej klasy w hierarchii:
public class Demo { LeafClass leaf = new LeafClass.Builder().baz(2).foo(1).bar(3).build(); }
źródło
B
, zawsze okazuje się, że jest to klasa bazowa.<T extends SomeClass, B extends SomeClass.Builder<T,B>> extends SomeClassParent.Builder<T,B>
wzorcem, co klasa pośrednia SecondLevel, zamiast tego deklaruje określone typy. Nie możesz zainicjować klasy, dopóki nie dojdziesz do liścia przy użyciu określonych typów, ale kiedy już to zrobisz, nie możesz jej dalej rozszerzyć, ponieważ używasz określonych typów i porzuciłeś Curiously Recurring Template Pattern. Ten link może pomóc: angelikalanger.com/GenericsFAQ/FAQSections/…Możesz także przesłonić
calories()
metodę i pozwolić jej zwrócić rozszerzający konstruktor. Kompiluje się, ponieważ Java obsługuje kowariantne typy zwracane .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); } } [...] }
źródło
Istnieje również inny sposób tworzenia klas według
Builder
wzorca, który jest zgodny z zasadą „Preferuj kompozycję zamiast dziedziczenia”.Zdefiniuj interfejs, który
Builder
odziedziczy klasa nadrzędna :public interface FactsBuilder<T> { public T calories(int val); }
Implementacja
NutritionFacts
jest prawie taka sama (z wyjątkiemBuilder
implementacji interfejsu „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; } }
Builder
Klasy dziecka powinni przedłużyć ten sam interfejs (z wyjątkiem innej realizacji rodzajowe):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; } }
Zauważ, że
NutritionFacts.Builder
jest to pole wewnątrzGMOFacts.Builder
(nazywanebaseBuilder
). Metoda zaimplementowana z metodyFactsBuilder
wywołań interfejsubaseBuilder
o tej samej nazwie:@Override public Builder calories(int val) { baseBuilder.calories(val); return this; }
Jest też duża zmiana w konstruktorze
GMOFacts(Builder builder)
. Pierwsze wywołanie w konstruktorze konstruktora klasy nadrzędnej powinno zostać przekazane odpowiednioNutritionFacts.Builder
:protected GMOFacts(Builder builder) { super(builder.baseBuilder); hasGMO = builder.hasGMO; }
Pełna realizacja
GMOFacts
klasy: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; } }
źródło
Pełny przykład 3 poziomu dziedziczenia wielu konstruktorów wyglądałby następująco :
(W przypadku wersji z konstruktorem kopiującym dla konstruktora zobacz drugi przykład poniżej)
Poziom pierwszy - rodzic (potencjalnie abstrakcyjny)
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; } } }
Drugi poziom
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; } } }
Trzeci poziom
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; } } }
I przykład użycia
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); } }
Nieco dłuższa wersja z konstruktorem kopiującym dla konstruktora:
Poziom pierwszy - rodzic (potencjalnie abstrakcyjny)
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; } } }
Drugi poziom
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; } } }
Trzeci poziom
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; } } }
I przykład użycia
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); } }
źródło
Jeśli nie chcesz wystawiać oczu na kątownik lub trzy, a może nie czujesz cię ... hm ... to znaczy ... kaszel ... reszta twojego zespołu szybko zrozumie z zaciekawieniem powtarzający się wzorzec generyczny, możesz to zrobić:
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"); } }
wspierany przez
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(); } }
i typ rodzica:
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 + '\'' + '}'; } }
Kluczowe punkty:
EDYTOWAĆ:
Znalazłem sposób na obejście fałszywego tworzenia obiektów. Najpierw dodaj to do każdego konstruktora:
private Class whoAmI() { return new Object(){}.getClass().getEnclosingMethod().getDeclaringClass(); }
Następnie w konstruktorze dla każdego konstruktora:
if (whoAmI() == this.getClass()) { this.obj = new ObjectToBuild(); }
Koszt to dodatkowy plik klasy dla
new Object(){}
anonimowej klasy wewnętrznejźródło
Jedną z rzeczy, które możesz zrobić, jest utworzenie statycznej metody fabrycznej w każdej z klas:
Ta statyczna metoda fabryki zwróci następnie odpowiedni konstruktor. Możesz mieć
GMOFacts.Builder
przedłużenie aNutritionFacts.Builder
, to nie jest problem. Problemem będzie tutaj poradzenie sobie z widocznością ...źródło
Poniższy wkład w projekt IEEE Refined Fluent Builder in Java przedstawia kompleksowe rozwiązanie problemu.
Rozbija pierwotne pytanie na dwa podproblemy: niedobór dziedziczenia i quasi-niezmienniczość i pokazuje, w jaki sposób otwiera się rozwiązanie tych dwóch podproblemów do obsługi dziedziczenia z ponownym wykorzystaniem kodu w klasycznym wzorcu konstruktora w Javie.
źródło
Utworzyłem nadrzędną, abstrakcyjną generyczną klasę konstruktora, która akceptuje dwa formalne parametry typu. Pierwsza dotyczy typu obiektu zwracanego przez funkcję build (), druga jest typem zwracanym przez każdy opcjonalny ustawiacz parametrów. Poniżej znajdują się klasy dla rodziców i dzieci w celach ilustracyjnych:
// **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; } }
Ten spełnił moje potrzeby do satysfakcji.
źródło