[java] Java 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.Buildera 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(){}익명의 내부 클래스에 대한 추가 클래스 파일입니다.