줄 이 박사 돕 스는 기사 우리는 빌더를 서브 클래스의 경우를 처리 어떻게, 특히 빌더 패턴을? 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
a NutritionFacts.Builder
가 아닌 을 반환 GMOFacts.Builder
하므로이 문제를 어떻게 해결합니까? 사용하기에 더 나은 패턴이 있습니까?
참고 : 비슷한 질문에 대한이 답변 은 위의 수업을 제공합니다. 내 질문은 빌더 호출이 올바른 순서인지 확인하는 문제에 관한 것입니다.
답변
제네릭을 사용하여 해결할 수 있습니다. 나는 이것이 “호 기적으로 반복되는 일반적인 패턴” 이라고 생각합니다
기본 클래스 빌더 메소드의 리턴 유형을 일반 인수로 만드십시오.
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;
}
}
답변
기록을 위해, 제거하기 위해
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 를 참조 하십시오.
답변
블로그 게시물을 기반으로 하는 이 방법은 모든 비 리프 클래스가 추상이어야하며 모든 리프 클래스는 최종이어야합니다.
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();
}
답변
또한 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);
}
}
[...]
}
답변
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;
}
}
답변
다중 빌더 상속의 전체 3 레벨 예제는 다음과 같습니다 .
(빌더 용 복사 생성자가있는 버전의 경우 아래 두 번째 예를 참조하십시오.)
첫 번째 수준-부모 (잠재적으로 요약)
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);
}
}
답변
꺾쇠 괄호 또는 세 개로 시선을 찌르고 싶지 않거나 아마도 당신을 느끼지 않으려면 … 음 … 내 말은 … 기침 … 나머지 팀은 빨리 호기심을 이해할 것입니다 되풀이되는 제네릭 패턴, 다음을 수행 할 수 있습니다.
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 + '\'' +
'}';
}
}
키 포인트:
- 상속으로 인해 상위 유형으로 보유 된 오브젝트의 필드를 설정할 수 없도록 빌더에서 오브젝트를 캡슐화하십시오.
- 수퍼 유형 작성기 메소드에 추가 된 논리 (있는 경우)가 하위 유형에 유지되도록 수퍼 호출.
- 단점은 부모 클래스에서 가짜 객체 생성입니다 …하지만 그것을 정리하는 방법은 아래를 참조하십시오
- 업사이드는 한 눈에 이해하기가 훨씬 쉬우 며 자세한 생성자 전송 속성은 없습니다.
- 빌더 객체에 액세스하는 여러 스레드가있는 경우 … 당신이 아니기 때문에 기쁩니다.
편집하다:
가짜 객체 생성을 둘러싼 방법을 찾았습니다. 먼저 이것을 각 빌더에 추가하십시오.
private Class whoAmI() {
return new Object(){}.getClass().getEnclosingMethod().getDeclaringClass();
}
그런 다음 각 빌더의 생성자에서 :
if (whoAmI() == this.getClass()) {
this.obj = new ObjectToBuild();
}
비용은 new Object(){}
익명의 내부 클래스에 대한 추가 클래스 파일입니다.