Javaの正しいポリモーフィックビルダー

これは何についてですか?



Javaでチェーンビルダーを実装する場合、継承を追加する必要があるまですべて問題ありません。 すぐに2つの問題が発生します。親ビルダーのメソッドが子ビルダーのオブジェクトを返す方法と、子ビルダーを親を受け入れる関数に渡す方法です。 両方の問題を解決できるパターンの実装が提案されています。 ソースはgithubでここにあります。



更新しました。 本当の問題



アプリケーションには、結果を表示するためのdtoオブジェクトがあり、次のように構成されています。



1)目的のdtoオブジェクトのビルダーが作成されます。

2)ビルダーはチェーン内のさまざまなクラスに転送され、各クラスはビルダーを使用して必要なフィールドを設定します。



ある晴れた日、彼らはAPIの新しいバージョンを導入することを決定し、dtoオブジェクトは継承を使用して拡張され、そのビルダーを既存のクラスチェーンに挿入して完了できないことが判明しました。



問題の声明



パターンビルダーの重要性については100500の言葉があるはずです。読者にこのくだらないことに飽きさせないために、すぐにビジネスに取り掛かりましょう。 Gen1、Gen2、Gen3というわかりやすい名前のクラスが3つあるとします。 それらは、Gen3→Gen2→Gen1の線形階層を形成します。 それぞれにsetValXと呼ばれる非常に重要なメソッドが1つだけ含まれています(Xはクラス名の数字です)。 ビルダーBuilder1、Builder2、Builder3を取得します。各ビルダーには、対応するvalXメソッドが含まれます。このメソッドは、1つのクラスにのみ実装されます(コピーと貼り付けは行いません)。



チェーンも動作するはずです:



Gen1 gen1 = builder1.val1("val1").build(); Gen2 gen2 = builder2.val1("val1").val2("val2").build(); Gen3 gen3 = builder3.val1("val1").val2("val2").val3("val3").build();
      
      





そして、親ビルダーの代わりに子ビルダーを使用する機能:



 Gen1 someFunction(Builder1 builder1) { return builder1.val1("val1111"); } ... someFunction1(builder3.val2("val222").val3("val333"));
      
      





何がどのように起こったのか



ビルダーは、次のスキームに従って作成されることになっています-最初にオブジェクトを作成し、そのフィールドに入力して、build()関数でクライアントに返します。 この場合、単純なことを行うクラスが必要です。構築するオブジェクトへのリンクを保存し、目的のタイプのビルダーへのリンクを保存して、すべてのセッターメソッドが返します。 次のクラスは問題を解決します。



 public class BuilderImpl<T, RetBuilder> { protected T nested; RetBuilder returnBuilder; protected BuilderImpl(T child) { nested = child; } protected T getNested() { return nested; } protected void injectReturnBuilder(RetBuilder builder) { returnBuilder = builder; } protected RetBuilder self() { return returnBuilder; } public T build() { return nested; } }
      
      





もちろん、必要なデータをコンストラクターに渡すことにより、injectReturnBuilderメソッドを削除する方が良いでしょうが、残念ながら、これはこの子ビルダーに渡され、親コンストラクターsuper()が終了するまで使用できません。 getNested()メソッドはアマチュアであり、ネストされたフィールドに直接アクセスできます。 self()メソッドは、フィールドをthisという単語と混同しないように作られています。



次に、この問題について考えてみましょう。 Gen1クラスに必要なすべてを実装するジェネリックBuilder1 <>(パラメーターGen1、Builder1を含む)がある場合、Gen2のジェネリックBuilder2を(いくつかのパラメーターGen1、Builder1を使用して)継承する必要があります。また、Gen3のBuilder3から、祖先のBuilder3には、異なるパラメーターを持つ元のBuilder1の2つの実装がありますが、これはJavaで厳密に禁止されています。



しかし、抜け道があります-オブジェクトフィールドの設定とオブジェクトの作成を異なるクラスに分ける必要があります。

InnerBuilderXという名前のクラスは、フィールドを設定し、オブジェクトを返し、継承を許可します。 FinalBuilderXという名前のクラスは、対応するInnerBuilderXから継承されるため、ソースオブジェクトの作成が追加され、さらに継承することはできません。



ワイルドカードの正しい組み合わせでInnerBuilderXを記述することは、別の困難をもたらします。 長い試行錯誤を通して(仕様を読むことは私たちのやり方ではありません)、受け入れられる選択肢が見つかりました。 しかし、彼が見つかった間に、組み合わせが試されました。そのために、アイデアインスペクタが死亡したか、ミスを犯しました。 そして、Gen1のInnerBuilder1クラスのコードは次のとおりです。 パラメーターTは保存されたオブジェクトのタイプ、RetBuilderはインストール関数val1から返されるビルダーのタイプです。



 public static class InnerBuilder1<T extends Gen1, RetBuilder extends InnerBuilder1<? extends T, ?>> extends BuilderImpl<T, RetBuilder> { protected InnerBuilder1(T created) { super(created); } public RetBuilder val1(String val) { getNested().setVal1(val); return self(); } }
      
      





もちろん、再帰構造はクラスInnerBuilder1 <TがGen1を拡張し、RetBuilderがInnerBuilder1を拡張します<? Tを拡張しますか?>>は少し面倒ですが、実際に機能します。



さて、FinalBuilderは非常に簡単です。



 private static class FinalBuilder1 extends InnerBuilder1<Gen1, FinalBuilder1> { private FinalBuilder1() { super(new Gen1()); //   this injectReturnBuilder(this); } }
      
      





静的関数を追加してビルダーを作成することは残ります。



 public static InnerBuilder1<? extends Gen1, ?> builder() { return new FinalBuilder1(); }
      
      





次に、子ビルダーに移りましょう。 内部ビルダーの実装を継承し、最終的にオブジェクトの作成を行います。



 public static InnerBuilder2<? extends Gen2, ?> builder() { return new FinalBuilder2(); } public static class InnerBuilder2<T extends Gen2, RetBuilder extends InnerBuilder2<? extends T,?>> extends InnerBuilder1<T, RetBuilder> { protected InnerBuilder2(T created) { super(created); } public RetBuilder val2(String val) { getNested().setVal2(val); return self(); } } private static class FinalBuilder2 extends InnerBuilder2<Gen2, FinalBuilder2> { private FinalBuilder2() { super(new Gen2()); injectReturnBuilder(this); } }
      
      





テストコードのコンパイルを試すことができます。



 Gen2.builder().val1("111").val1("111").val1("111").val1("111").val2("222").build();
      
      





わかった! そして、多型はどうですか?



 //    Gen1 Gen1 buildGen1Final(Gen1.InnerBuilder1<? extends Gen1, ?> builder) { builder.val1("set value from Gen1 builder"); return builder.build(); } ... //     Gen2 buildGen1Final( Gen2.builder().val2("set value from Gen2 builder") );
      
      





すべてがうまくいきます。 同様に、Gen3クラスのビルダーが実装されています。詳細については、 githubに連絡してください。



All Articles