Подкласс класса Java Builder
Дайте эту статью доктора Доббса и, в частности, шаблон Builder, как мы обрабатываем случай подклассификации Builder? Принимая сокращенную версию примера, в котором мы хотим подклассом, чтобы добавить маркировку GMO, наивная реализация была бы следующей:
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;
}
}
Подкласс:
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;
}
}
Теперь мы можем написать такой код:
GMOFacts.Builder b = new GMOFacts.Builder();
b.GMO(true).calories(100);
Но, если мы получим неправильный порядок, все это терпит неудачу:
GMOFacts.Builder b = new GMOFacts.Builder();
b.calories(100).GMO(true);
Проблема, конечно, в том, что NutritionFacts.Builder
возвращает NutritionFacts.Builder
, а не GMOFacts.Builder
, так как мы решаем эту проблему или есть лучший шаблон для использования?
Примечание: этот ответ на аналогичный вопрос предлагает классы, которые у меня выше; мой вопрос касается проблемы обеспечения того, чтобы вызовы строителя были в правильном порядке.
Ответы
Ответ 1
Вы можете решить его с помощью дженериков. Я думаю, что это называется "Любопытно повторяющиеся общие шаблоны"
Сделать возвращаемый тип методов базового класса построением общего аргумента.
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;
}
}
Теперь создайте базовый построитель с построением производного класса как общий аргумент.
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
Только для записи, чтобы избавиться от
unchecked or unsafe operations
предупреждение
для оператора return (T) this;
, как говорят @dimadima и @Thomas N., в некоторых случаях применяется следующее решение.
Сделайте abstract
строитель, который объявляет общий тип (T extends Builder
в этом случае) и объявит protected abstract T getThis()
абстрактный метод следующим образом:
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); }
}
Подробнее см. http://www.angelikalanger.com/GenericsFAQ/FAQSections/ProgrammingIdioms.html#FAQ205.
Ответ 3
Основываясь на сообщении в блоге, этот подход требует, чтобы все неклассические классы были абстрактными, и все классы листа должны быть окончательными.
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;
}
}
}
Затем у вас есть промежуточный класс, который расширяет этот класс и его построитель и столько, сколько вам нужно:
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;
}
}
}
И, наконец, конкретный листовой класс, который может вызвать все методы построения для любого из его родителей в любом порядке:
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;
}
}
}
Затем вы можете вызвать методы в любом порядке из любого из классов в иерархии:
public class Demo {
LeafClass leaf = new LeafClass.Builder().baz(2).foo(1).bar(3).build();
}
Ответ 4
Вы можете переопределить также метод calories()
и позволить ему возвращать расширяющийся строитель. Это компилируется, потому что Java поддерживает ковариантные типы возврата.
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);
}
}
[...]
}
Ответ 5
Если вы не хотите выказывать свой глаз на угловой кронштейн или три, или, может быть, не чувствуете себя... ммм... Я имею в виду... кашель. остальная часть вашей команды быстро постигает любопытно повторяющийся шаблон дженериков, вы можете это сделать:
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");
}
}
поддерживается
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();
}
}
и родительский тип:
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 + '\'' +
'}';
}
}
Ключевые моменты:
- Инкапсулировать объект в построителе, чтобы наследование не позволяло вам устанавливать поле на объекте, хранящемся в родительском типе
- Вызовы супер гарантируют, что логика (если таковая имеется) добавлена к методам построения супер-типа, сохраняется в подтипах.
- Вниз сторона - это создание ложных объектов в родительском классе (ы)... Но см. ниже, чтобы очистить это.
- Наверху гораздо легче понять с первого взгляда, и нет подробных конструкторов, передающих свойства.
- Если у вас есть несколько потоков, обращающихся к вашим объектам-строителям... Наверное, я рад, что я не являюсь вам:).
EDIT:
Я нашел способ создания ложных объектов. Сначала добавьте это каждому строителю:
private Class whoAmI() {
return new Object(){}.getClass().getEnclosingMethod().getDeclaringClass();
}
Затем в конструкторе для каждого строителя:
if (whoAmI() == this.getClass()) {
this.obj = new ObjectToBuild();
}
Стоимость - это дополнительный файл класса для анонимного внутреннего класса new Object(){}
Ответ 6
Существует также другой способ создания классов согласно шаблону Builder
, который соответствует "Предпочитает композицию над наследованием".
Определите интерфейс, который родительский класс Builder
наследует:
public interface FactsBuilder<T> {
public T calories(int val);
}
Реализация NutritionFacts
почти такая же (за исключением Builder
реализации интерфейса 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
дочернего класса должен расширять один и тот же интерфейс (кроме другой общей реализации):
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;
}
}
Обратите внимание, что NutritionFacts.Builder
- это поле внутри GMOFacts.Builder
(называемое baseBuilder
). Метод, реализованный из интерфейса FactsBuilder
, вызывает метод baseBuilder
с тем же именем:
@Override
public Builder calories(int val) {
baseBuilder.calories(val);
return this;
}
Также существует большое изменение в конструкторе GMOFacts(Builder builder)
. Первый вызов конструктора в конструктор родительского класса должен передать соответствующий NutritionFacts.Builder
:
protected GMOFacts(Builder builder) {
super(builder.baseBuilder);
hasGMO = builder.hasGMO;
}
Полная реализация класса 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;
}
}
Ответ 7
Полный трехуровневый пример наследования нескольких сборщиков будет выглядеть следующим образом::
(Для версии с конструктором копирования для компоновщика см. второй пример ниже)
Первый уровень - родитель (потенциально абстрактный)
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;
}
}
}
Второй уровень
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;
}
}
}
Третий уровень
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;
}
}
}
И пример использования
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);
}
}
Немного длиннее версия с конструктором копирования для сборщика:
Первый уровень - родитель (потенциально абстрактный)
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;
}
}
}
Второй уровень
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;
}
}
}
Третий уровень
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;
}
}
}
И пример использования
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);
}
}
Ответ 8
Одна вещь, которую вы можете сделать, - создать статический метод factory в каждом из ваших классов:
NutritionFacts.newBuilder()
GMOFacts.newBuilder()
Этот статический метод factory затем вернет соответствующий строитель. Вы можете иметь GMOFacts.Builder
расширение NutritionFacts.Builder
, что не является проблемой. Проблема здесь будет связана с видимостью...
Ответ 9
Следующий вклад IEEE Уточненный Fluent Builder в Java дает исчерпывающее решение проблемы.
Он разбивает исходный вопрос на две подзадачи: дефицит наследования и квазиинвариантность и показывает, как открывается решение этих двух подзадач для поддержки наследования с повторным использованием кода в классическом компоновщике. шаблон в Java.
Ответ 10
Я создал родительский, абстрактный универсальный класс построителя, который принимает два параметра формального типа. Первый - для типа объекта, возвращаемого build(), второй - тип, возвращаемый каждым необязательным установщиком параметров. Ниже приведены родительские и дочерние классы в иллюстративных целях:
// **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;
}
}
Это удовлетворение моих потребностей.