Java Builderクラスのサブクラス化


133

与えるこの博士ドブスの記事、特に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.BuilderNutritionFacts.Builderではなくを返すGMOFacts.Builderことです。この問題を解決するにはどうすればよいですか。

注:同様の質問に対するこの回答は、上記のクラスを提供します。私の質問は、ビルダー呼び出しが正しい順序になっていることを確認する問題についてです。


1
:私は、次のリンクは良いアプローチについて説明し考えるegalluzzo.blogspot.co.at/2010/06/...
Stuxnetの

1
しかし、どのようにbuild()出力されb.GMO(true).calories(100)ますか?
Sridhar Sarnobat

回答:


170

あなたはジェネリックを使用してそれを解決することができます。これは「奇妙に繰り返される一般的なパターン」と呼ばれていると思います

基本クラスビルダーメソッドの戻り値の型をジェネリック引数にします。

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
うーん、私は(a)新しい質問を投稿する、(b)implementsではなくで再設計するextends、または(c)すべてを捨てる必要があると思います。leafBuilder.leaf().leaf()そして、leafBuilder.mid().leaf()OK で奇妙なコンパイルエラーが発生しましたが、leafBuilder.leaf().mid().leaf()失敗します...
Ken YN

11
@gkamal return (T) this;unchecked or unsafe operations警告になります。これは避けられないでしょう?
ドミトリーミンコフスキー2015年

5
unchecked cast警告を解決するには、他の回答の中で推奨される解決策を参照してください:stackoverflow.com/a/34741836/3114959
ステパンVavra

8
これBuilder<T extends Builder>は実際にはrawtypeであることに注意してください-これはそうであるべきですBuilder<T extends Builder<T>>
ボリスザスパイダー

2
@ user2957378 Builderfor GMOFactsも汎用的である必要がありBuilder<B extends Builder<B>> extends NutritionFacts.Builder<Builder>、このパターンは必要なだけ多くのレベルまで継続できます。非ジェネリックビルダーを宣言すると、パターンを拡張できません。
ボリス・ザ・スパイダー

44

参考までに、

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を参照してください


build()メソッドがここでNutrutionFactsを返すのはなぜですか?
mvd 2016年

@mvdこれは質問への回答なので?サブタイプでは、次のような、それを上書きしますpublic GMOFacts build() { return new GMOFacts(this); }
ステパンVavra

問題は、私たちが第二子を追加したいときに発生するBuilderC extends BuilderBBuilderB extends BuilderAするときBuilderBではありませんabstract
sosite

1
基本クラスは抽象的ではない可能性があるため、これは質問に対する回答ではありません!
Roland

「ジェネリック型を宣言するビルダーを抽象化する」-そのビルダーを直接使用したい場合はどうなりますか?
デイジー

21

ブログ投稿に基づいて、このアプローチでは、すべての非リーフクラスが抽象的であり、すべてのリーフクラスがfinalである必要があります。

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

リーフクラスが最終である必要がある理由を知っていますか?具体的なクラスをサブクラス化できるようにしたいのですが、コンパイラーにのタイプを認識させる方法が見つかりませんでしたB。常に基本クラスであることが判明しました。
David Ganster 2017

LeafClassのBuilderクラスは<T extends SomeClass, B extends SomeClass.Builder<T,B>> extends SomeClassParent.Builder<T,B>、中間のSecondLevelクラスが行うのと同じパターンに従っていないことに注意してください。代わりに、特定の型を宣言しています。特定のタイプを使用して葉に到達するまでクラスを初期化することはできませんが、特定のタイプを使用していて、不思議な繰り返しテンプレートパターンを放棄しているため、一度クラスを拡張すると、それ以上拡張することはできません。このリンクのかもしれないのヘルプ:angelikalanger.com/GenericsFAQ/FAQSections/...
Q23

7

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

ああ、私はC ++の出身なので、それを知りませんでした。これはこの小さな例では便利なアプローチですが、本格的なクラスですべてのメソッドを繰り返すと、問題が発生し、エラーが発生しやすくなります。しかし、何か新しいことを教えてくれた+1!
Ken YN 2013年

これは何も解決しないように思えます。親をサブクラス化する理由(IMO)は、親メソッドをオーバーライドせずに再利用するためです。クラスが単なる値オブジェクトであり、単純な値を設定する以外はビルダーメソッドに実際のロジックがない場合、オーバーライドするメソッドで親メソッドを呼び出しても、ほとんどまたはまったく値がありません。
開発者Dude

答えは質問で説明されている問題を解決します。ビルダーを使用するコードは両方の順序でコンパイルされます。片方はコンパイルし、もう片方はコンパイルしないので、結局何らかの価値があるに違いないと思います。
フラビオ2017

3

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.BuilderGMOFacts.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

複数のビルダー継承の完全な3レベルの例は次のようになります

(ビルダーのコピーコンストラクターがあるバージョンについては、下の2番目の例を参照してください)

第1レベル-親(抽象の可能性あり)

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

第3レベル

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


ビルダーのコピーコンストラクターを備えたもう少し長いバージョン:

第1レベル-親(抽象の可能性あり)

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

第3レベル

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

2

山かっこや3つに目を向けたくない場合、またはおそらく感じない場合...うーん...つまり... ...チームの他のメンバーは好奇心旺盛にすぐに理解しますジェネリックパターンの繰り返し、これを行うことができます:

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

キーポイント:

  • 継承によって親タイプに保持されているオブジェクトのフィールドを設定できないように、ビルダーでオブジェクトをカプセル化します
  • superを呼び出すと、スーパータイプビルダーメソッドに追加されたロジック(存在する場合)がサブタイプに保持されます。
  • 欠点は、親クラスでの偽のオブジェクト作成です...しかし、それをクリーンアップする方法については、以下を参照してください
  • 利点は一目ではるかに理解しやすく、プロパティを転送する詳細なコンストラクタはありません。
  • 複数のスレッドがビルダーオブジェクトにアクセスしている場合...私はあなたではないのでよかったと思います:)

編集:

偽のオブジェクト作成を回避する方法を見つけました。まず、これを各ビルダーに追加します。

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

次に、各ビルダーのコンストラクターで:

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

コストは、new Object(){}匿名の内部クラス用の追加のクラスファイルです


1

できることの1つは、クラスのそれぞれに静的なファクトリメソッドを作成することです。

NutritionFacts.newBuilder()
GMOFacts.newBuilder()

この静的ファクトリーメソッドは、適切なビルダーを返します。をGMOFacts.Builder拡張することができますがNutritionFacts.Builder、これは問題ではありません。ここでの問題は、可視性に対処することです...


0

次のIEEE貢献Javaでの洗練されたFluent Builder、問題に対する包括的なソリューションを提供します。

元の質問を継承不足準不変性の 2つのサブ問題に分解し、Javaの従来のビルダーパターンでコードを再利用して継承をサポートするために、これらの2つのサブ問題に対するソリューションがどのように開かれるかを示します。


この回答には役立つ情報が含まれておらず、少なくともリンクに記載されている回答の概要が含まれておらず、ログインが必要なリンクにつながっています。
ソナタ

この回答は、公式の発行機関と公式の発行および共有手順を備えた査読済みの会議の出版物にリンクしています。
mc00x1

0

2つの正式な型パラメーターを受け入れる親の抽象ジェネリックビルダークラスを作成しました。1つ目はbuild()によって返されるオブジェクトのタイプ、2つ目はオプションの各パラメーターセッターによって返されるタイプです。以下は、説明のための親クラスと子クラスです。

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

これは私のニーズを満足させてくれました。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.