Javaで最も簡単で複雑なBuilder



最も一般的に考えられているパターンの1つは、Builderパターンです。 基本的に、このパターンの「クラシック」バージョンを実装するためのオプションが考慮されます。

MyClass my = MyClass.builder().first(1).second(2.0).third("3").build(); 

パターンはシンプルでスツールとして理解できますが、控えめな表現が感じられます-最小オプションがアンチパターンとして宣言されているか、より複雑なケースが無視されています。 制限のあるケースを考慮し、このパターンの複雑さの最小および最大境界を決定することにより、この点を修正したいと思います。

だから、それらを考慮してください:

最小限のビルダーまたはリハビリテーションの二重装具の初期化


最初に、忘れられがちな最小限のビルダー、二重括弧の初期化(
http://stackoverflow.com/questions/1958636/what-is-double-brace-initialization-in-java、http://c2.com/cgi/wiki?DoubleBraceInitialization )。 ダブルブレースの初期化を使用すると、次のことができます。

 new MyClass() {{ first = 1; second = 2.0; third = "3"; }} 

ここで何が見えますか?
  1. 互換性違反が等しい
    互換性とは何ですか? 実際には、標準の等号は次のようなものです。

     @Override public boolean equals(Object obj) { if(this == obj) return true; if(!super.equals(obj)) return false; if(getClass() != obj.getClass()) return false; ... } 

    そして、継承されたクラスと比較すると、equalsはfalseを返します。 しかし、匿名継承クラスを作成し、継承チェーンに介入します。
  2. 可能性のあるメモリリーク、として 匿名クラスは、作成コンテキストへの参照を保持します。
  3. チェックなしのフィールドの初期化。

さらに、この方法では、最終フィールドを使用できないため、不変オブジェクトを作成することはできません。

その結果、通常、複合構造を初期化するために二重括弧の初期化が使用されます。 例:

 new TreeMap<String, Object>() {{ put("first", 1); put(second, 2.0); put("third", "3"); }} 

ここでは、フィールドへの直接アクセスではなくメソッドが使用され、通常、同等の互換性は必要ありません。 では、このような信頼性の低いハックのような方法をどのように使用できますか? はい、それは非常に簡単です-ダブルブレースの初期化のために別個のビルダークラスを割り当てます。

そのようなビルダーのコードには、デフォルト値が設定されたフィールドの定義と、パラメーターのチェックとコンストラクターの呼び出しを行う構築メソッドのみが含まれています。

 public static class Builder { public int first = -1 ; public double second = Double.NaN; public String third = null ; public MyClass create() { return new MyClass( first , second, third ); } } 

使用法:

 new MyClass.Builder(){{ first = 1; third = "3"; }}.create() 

何が得られますか?
  1. Builderは継承チェーンに干渉しません-これは別個のクラスです。
  2. Builderは流れません-オブジェクトの作成後にその使用は停止します。
  3. Builderは、オブジェクトを作成する方法でパラメーターを制御できます。

出来上がり! ダブルブレースの初期化がリハビリされました。

継承を使用するために、Builderは次のように2つの部分に分かれています(1つはフィールド、もう1つは作成メソッド)。

 public class MyBaseClass { protected static class BuilderImpl { public int first = -1 ; public double second = Double.NaN; public String third = null ; } public static class Builder extends BuilderImpl { public MyBaseClass create() { return new MyBaseClass( first , second, third ); } } ... } public class MyChildClass extends MyBaseClass { protected static class BuilderImpl extends MyBaseClass.BuilderImpl { public Object fourth = null; } public static class Builder extends BuilderImpl { public MyChildClass create() { return new MyChildClass( first , second, third , fourth ); } } ... } 

必須パラメーターが必要な場合、これらは次のようになります。

 public static class Builder { public double second = Double.NaN; public String third = null ; public MyClass create(int first) { return new MyClass( first , second, third ); } } 

使用法:

 new MyClass.Builder(){{ third = "3"; }}.create(1) 

非常に単純なので、関数パラメーターのビルダーとしても使用できます。次に例を示します。

 String fn = new fn(){{ first = 1; third = "3"; }}.invoke(); 

完全なgithubコード。

難しいところに移りましょう。

最も洗練されたメガビルダー


そして実際、何が複雑になるのでしょうか? そして、これは何ですか! Builderを作成してみましょう。これはコンパイル時に次のようになります。
  1. 無効なパラメーターの組み合わせの使用を許可しない
  2. 必要なパラメータが入力されていない場合、オブジェクトの構築を許可しません
  3. パラメータの再初期化を防ぎます

これには何が必要ですか? これを行うには、パラメーターの組み合わせのすべてのバリエーションを持つインターフェイスを作成する必要があります。最初に、オブジェクトを各パラメーターに対応する個別のインターフェイスに分解します。

各パラメーターを割り当て、新しいビルダーを返すためのインターフェイスが必要です。 次のようになります。

 public interface TransitionNAME<T> { T NAME(TYPE v); } 

同時に、NAMEはインターフェースごとに異なる必要があります。その場合、それらを組み合わせる必要があるためです。

この割り当て後に値を取得できるように、ゲッターも必要です。

 public interface GetterNAME { TYPE NAME(); } 

transition-getterバンドルが必要なので、次のように移行インターフェイスを定義します。

 public interface TransitionNAME<T extends GetterNAME> { T NAME(TYPE v); } 

これにより、記述に静的制御も追加されます。

反復するインターフェイスのセットはほぼ明確です。 これを行う方法を決定します。

前の例と同じクラス1-2-3を取り、すべてのパラメーターの組み合わせを開始するために書きましょう。 使い慣れたバイナリ表現を取得します。

 first second third - - - - - + - + - - + + + - - + - + + + - + + + 

便宜上、次のようにツリーの形でこれを想像してください。

 first second third - - - / + - - /+ + + - /+/+ + + + /+/+/+ + - + /+/-/+ - + - /-/+ - + + /-/+/+ - - + /-/-/+ 

たとえば、次のように、許容される組み合わせにマークを付けます。

 first second third - - - / * + - - /+ * + + - /+/+ * + + + /+/+/+ + - + /+/-/+ * - + - /-/+ - + + /-/+/+ * - - + /-/-/+ * 

余分なノードを削除しましょう-無効なノードと空のノードを終端します。 一般的な場合、これは削除するノードがある限り継続する循環プロセスですが、この場合、無効な端末は1つだけです。

 first second third - - - / * + - - /+ * + + - /+/+ * + - + /+/-/+ * - + - /-/+ - + + /-/+/+ * - - + /-/-/+ * 

これを実現する方法は?

残りのユースケースを減らすために、各要素の割り当てが必要です。 これを行うには、遷移インターフェイスを介した要素の各割り当ては、新しいビルダークラスと、この遷移のゲッターインターフェイスからこの遷移インターフェイスを引いたものを返す必要があります。

インターフェイスを描画します。

 public interface Get_first { int first (); } public interface Get_second { double second(); } public interface Get_third { String third (); } public interface Trans_first <T extends Get_first > { T first (int first ); } public interface Trans_second<T extends Get_second> { T second(double second); } public interface Trans_third <T extends Get_third > { T third (String third ); } 

これでタブレットを描くのは不便です;識別子を減らします:

 public interface G_1 extends Get_first {} public interface G_2 extends Get_second{} public interface G_3 extends Get_third {} public interface T_1<T extends G_1> extends Trans_first <T> {} public interface T_2<T extends G_2> extends Trans_second<T> {} public interface T_3<T extends G_3> extends Trans_third <T> {} 

遷移ラベルを描いてみましょう:

 public interface B extends T_1<B_1 >, T_2<B_2 >, T_3<B_3 > {} // - - - / * public interface B_1 extends T_2<B_1_2>, T_3<B_1_3> {} // + - - /+ * public interface B_1_2 extends {} // + + - /+/+ * public interface B_1_3 extends {} // + - + /+/-/+ * public interface B_2 extends T_1<B_1_2>, T_3<B_2_3> {} // /-/+ public interface B_2_3 extends {} // - + + /-/+/+ * public interface B_3 extends T_1<B_1_3>, T_2<B_2_3> {} // - - + /-/-/+ * 

定義済みインターフェースの定義:

 public interface Built { MyClass build(); } 

Builtインターフェースを使用してクラスを既に構築できるインターフェースをマークし、ゲッターを追加して、結果のBuilderインターフェースを定義します。

 //  // |   //  | | // | | | // ------------- ---------------------------------- ----- // // first first first // | second | second | second // | | third| | third | | third // | | | | | | | | | public interface B extends T_1<B_1 >, T_2<B_2 >, T_3<B_3 >, Built {} // - - - / * public interface B_1 extends G_1, T_2<B_1_2>, T_3<B_1_3>, Built {} // + - - /+ * public interface B_1_2 extends G_1, G_2, Built {} // + + - /+/+ * public interface B_1_3 extends G_1, G_3, Built {} // + - + /+/-/+ * public interface B_2 extends G_2, T_1<B_1_2>, T_3<B_2_3> {} // /-/+ public interface B_2_3 extends G_2, G_3, Built {} // - + + /-/+/+ * public interface B_3 extends G_3, T_1<B_1_3>, T_2<B_2_3>, Built {} // - - + /-/-/+ * public interface Builder extends B {} 

これらの説明は、実行時にプロキシを構築できるようにするのに十分です。マーカーインターフェイスを追加して、結果の定義を修正するだけです。

 public interface Built extends BuiltBase<MyClass> {} public interface Get_first extends GetBase { int first (); } public interface Get_second extends GetBase { double second(); } public interface Get_third extends GetBase { String third (); } public interface Trans_first <T extends Get_first > extends TransBase { T first (int first ); } public interface Trans_second<T extends Get_second> extends TransBase { T second(double second); } public interface Trans_third <T extends Get_third > extends TransBase { T third (String third ); } 

次に、Builderクラスから値を取得して、実際のクラスを作成する必要があります。 2つのオプションがあります-各ビルダーのメソッドを作成するか、静的に型指定された各ビルダーからパラメーターを取得します。

 public MyClass build(B builder) { return new MyClass(-1 , Double.NaN , null); } public MyClass build(B_1 builder) { return new MyClass(builder.first(), Double.NaN , null); } public MyClass build(B_1_2 builder) { return new MyClass(builder.first(), builder.second(), null); } ... 

または、およそ次のように定義された一般化された方法を使用します。

 public MyClass build(BuiltValues values) { return new MyClass( //   values ); } 

しかし、値を取得する方法は?

まず、目的のゲッターを持つビルダークラスのセットがまだあります。 したがって、目的のゲッターの実装があるかどうかを確認する必要があります。実装されている場合は、型をキャストして値を取得します。

 (values instanceof Get_first) ? ((Get_first) values).first() : -1 

もちろん、値を取得するメソッドを追加できますが、既存の型から値型を取得できないため、型指定されません。

 Object getValue(final Class< ? extends GetBase> key); 

使用法:

 (Integer) values.getValue(Get_first.class) 

型を取得するには、次のような追加のクラスとバンドルを作成する必要があります。

 public interface TypedGetter<T, GETTER> { Class<GETTER> getterClass(); }; public static final Classed<T> GET_FIRST = new Classed<Integer>(Get_first.class); 

次に、値を取得する方法を次のように定義できます。

 public <T, GETTER> T get(TypedGetter<T, GETTER> typedGetter); 

しかし、ゲッターおよび遷移インターフェースとは何とかしようとします。 次に、型キャストなしで、ゲッターインターフェイスを返すことによってのみ値を返すことができます。このようなインターフェイスがこのビルダーに定義されていない場合はnullを返します。

 <T extends GetBase> T get(Class<T> key); 

使用法:

 (null == values.get(Get_second.class)) ? Double.NaN: values.get(Get_second.class).second() 

これはすでに優れています。 しかし、タイプを維持している間にインターフェースがない場合、デフォルト値を追加することは可能ですか? もちろん、型指定されたゲッターインターフェイスを返すことは可能ですが、型指定されていないデフォルト値を渡す必要があります。

 <T extends GetBase> T get(Class<T> key, Object defaultValue); 

ただし、遷移インターフェイスを使用してデフォルト値を設定できます。

 <T extends TransBase> T getDefault(Class< ? super T> key); 

そして、次のように使用します。

 values.getDefault(Get_third.class).third("1").third() 

これで、既存のインターフェイスを使用して型安全にビルドできます。 リストされたユースケースを示す一般化された初期化メソッドを作成し、結果のビルダーを初期化します。

 protected static final Builder __builder = MegaBuilder.newBuilder( Builder.class, null, new ClassBuilder<Object, MyClass>() { @Override public MyClass build(Object context, BuiltValues values) { return new MyClass( (values instanceof Get_first) ? ((Get_first) values).first() : -1, (null == values.get(Get_second.class)) ? Double.NaN: values.get(Get_second.class).second(), values.getDefault(Get_third.class).third(null).third() ); } } ); public static Builder builder() { return __builder; } 

今、あなたはそれを呼び出すことができます:

 builder() .build(); builder().first(1) .build(); builder().first(1).second(2) .build(); builder().second(2 ).first (1).build(); builder().first(1) .third("3").build(); builder().third ("3").first (1).build(); builder() .second(2).third("3").build(); builder().third ("3").second(2).build(); builder() .third("3").build(); 

コードをダウンロードして、 ここからコンテキストアシストの動作を確認できます。

特に:
問題の例のコード: MyClass.java
ジェネリック型の例: MyParameterizedClass.java
非静的ビルダーのMyLocalClass.java

合計


Source: https://habr.com/ru/post/J261163/


All Articles