高度に拡張可能なクラスでの使用により適したBlochのBuilderパターンを改善する方法


34

Joshua BlochのEffective Javaの本(第2版)に大きな影響を受けました。おそらく私が読んだどのプログラミングの本よりもそうでしょう。特に、彼のビルダーパターン(項目2)が最大の効果をもたらしました。

Blochのビルダーは、過去10年間のプログラミングよりも数か月ではるかに遠くまで私を導いてくれましたが、私は今でも同じ壁にぶつかっています。 --especiallyジェネリックが遊びに来たときに、そして特に自己参照ジェネリック医薬品(などComparable<T extends Comparable<T>>)。

私には2つの主要なニーズがありますが、この質問で焦点を当てたいのは2番目だけです。

  1. 最初の問題は、「...単...クラスごとに再実装することなく、自己復帰メソッドチェーンを共有する方法」です。好奇心may盛な方のために、この回答記事の最後でこの部分について説明しましたが、ここで焦点を当てたいものではありません。

  2. 私がコメントを求めている2番目の問題は、「他の多くのクラスによって拡張されることを意図したクラスにビルダーを実装するにはどうすればよいですか」です。ビルダーを使用してクラスを拡張するのは、当然、ビルダーを使用せずに拡張するよりも困難です。を実装Needableするビルダーを持つクラスを拡張することは、そのため、それに関連付けられた重要なジェネリックを持ち、扱いにくいです。

それが私の質問です:Bloch Builder(私が呼ぶもの)をどのように改善できますか?そのクラスが「ベースクラス」であることを意図している場合でも、自由にビルダーを任意のクラスに添付できますビルダー(およびその潜在的なジェネリック)が彼らに課す余分な荷物のために、私の未来やライブラリのユーザーを落胆させることなく、何度も何度も拡張およびサブ拡張されましたか?


補遺
私の質問は上記のパート2に焦点を当てていますが、対処方法など、問題1について少し詳しく説明したいと思いました。

最初の問題は、「...単...クラスごとに再実装することなく、自己復帰メソッドチェーンを共有する方法」です。これは、拡張クラスがこれらのチェーンを再実装する必要があることを防ぐためではなく、もちろん、これらのメソッドチェーンを利用したい非サブクラスを防ぐ方法を再実装する必要があります- ユーザーがそれらを利用できるようにすべての自己復帰機能を実装しますか?このために、ここでインターフェースのスケルトンを印刷し、今のところはそのままにしておく必要がある必要性の高いデザインを考え出しました。私にとってはうまくいきました(この設計は長年の開発でした...最も困難な部分は循環依存を回避することでした):

public interface Chainable  {  
    Chainable chainID(boolean b_setStatic, Object o_id);  
    Object getChainID();  
    Object getStaticChainID();  
}
public interface Needable<O,R extends Needer> extends Chainable  {
    boolean isAvailableToNeeder();
    Needable<O,R> startConfigReturnNeedable(R n_eeder);
    R getActiveNeeder();
    boolean isNeededUsable();
    R endCfg();
}
public interface Needer  {
    void startConfig(Class<?> cls_needed);
    boolean isConfigActive();
    Class getNeededType();
    void neeadableSetsNeeded(Object o_fullyConfigured);
}

回答:


21

私は、Josh BlochのBuilderパターンを大きく改善するものを作成しました。それが「より良い」とは言いませんが、非常に特定の状況では、いくつかの利点を提供します-最大のことは、ビルダーをビルドされるクラスから切り離すことです。

この代替案を以下に完全に文書化しました。これをブラインドビルダーパターンと呼びます。


デザインパターン:ブラインドビルダー

Joshua Blochのビルダーパターン(Effective Javaのアイテム2、第2版)の代替として、私は「ブラインドビルダーパターン」と呼んでいるものを作成しました。まったく同じ方法で使用されます。ブラインドビルダーには次の利点があります。

  • ビルダーをその包含クラスから分離し、循環依存関係を排除し、
  • 囲んでいるクラスのソースコードのサイズを大幅に削減します(もはやではありません)。
  • ビルダーを拡張することなくToBeBuiltクラスを拡張できます。

このドキュメントでは、作成中のクラスを「ToBeBuilt」クラスと呼びます。

Bloch Builderで実装されたクラス

Bloch Builderは、public static classビルドするクラス内に含まれています。例:

パブリッククラスUserConfig {
   プライベート最終文字列sName;
   private final int iAge;
   private final String sFavColor;
   public UserConfig(UserConfig.Cfg uc_c){// CONSTRUCTOR
      //転送
         {
            sName = uc_c.sName;
         } catch(NullPointerException rx){
            throw NullPointerException( "uc_c");
         }
         iAge = uc_c.iAge;
         sFavColor = uc_c.sFavColor;
      //すべてのフィールドをここで検証
   }
   public String toString(){
      return "name =" + sName + "、age =" + iAge + "、sFavColor =" + sFavColor;
   }
   //ビルダー... START
   パブリックスタティッククラスCfg {
      プライベート文字列sName;
      private int iAge;
      private String sFavColor;
      public Cfg(String s_name){
         sName = s_name;
      }
      //自己復帰セッター... START
         public Cfg age(int i_age){
            iAge = i_age;
            これを返す;
         }
         public Cfg favoriteColor(String s_color){
            sFavColor = s_color;
            これを返す;
         }
      //自己復帰セッター... END
      public UserConfig build(){
         return(新しいUserConfig(this));
      }
   }
   //ビルダー... END
}

Bloch Builderを使用してクラスをインスタンス化する

UserConfig uc = new UserConfig.Cfg( "Kermit")。age(50).favoriteColor( "green")。build();

ブラインドビルダーとして実装された同じクラス

ブラインドビルダーには3つの部分があり、それぞれが個別のソースコードファイルにあります。

  1. ToBeBuilt(この例では:クラスUserConfig
  2. その「Fieldable」インターフェース
  3. ビルダー

1.作成予定のクラス

作成されるクラスは、そのFieldableインターフェイスを唯一のコンストラクタパラメータとして受け入れます。コンストラクターは、コンストラクターからすべての内部フィールドを設定し、それぞれを検証します。最も重要なことは、このToBeBuiltクラスにはそのビルダーに関する知識がないことです。

パブリッククラスUserConfig {
   プライベート最終文字列sName;
   private final int iAge;
   private final String sFavColor;
    public UserConfig(UserConfig_Fieldable uc_f){// CONSTRUCTOR
      //転送
         {
            sName = uc_f.getName();
         } catch(NullPointerException rx){
            新しいNullPointerException( "uc_f");をスローします。
         }
         iAge = uc_f.getAge();
         sFavColor = uc_f.getFavoriteColor();
      //すべてのフィールドをここで検証
   }
   public String toString(){
      return "name =" + sName + "、age =" + iAge + "、sFavColor =" + sFavColor;
   }
}

1人のスマートコメンター(答えが説明できないほど削除された)が指摘したように、ToBeBuiltクラスがそのを実装しているFieldable場合、その唯一のコンストラクターをプライマリコンストラクターコピーコンストラクターの両方として使用できます(欠点は、フィールドが常に検証されることです)オリジナルのフィールドToBeBuiltが有効であることがわかっています)。

2.「Fieldable」インターフェース

フィールド化可能なインターフェイスは、ToBeBuiltクラスとそのビルダー間の「ブリッジ」であり、オブジェクトの構築に必要なすべてのフィールドを定義します。このインターフェイスは、ToBeBuiltクラスコンストラクターに必要であり、ビルダーによって実装されます。このインターフェイスは、ビルダー以外のクラスによって実装される可能性があるため、どのクラスでもToBeBuilt、そのビルダーを使用せずに、クラスを簡単にインスタンス化できます。またToBeBuilt、ビルダーを拡張することが望ましくない場合や必要でない場合に、クラスを簡単に拡張できます。

以下のセクションで説明するように、このインターフェイスの機能についてはまったく説明しません。

パブリックインターフェイスUserConfig_Fieldable {
   文字列getName();
   int getAge();
   文字列getFavoriteColor();
}

3.ビルダー

ビルダーはFieldableクラスを実装します。検証はまったく行われず、この事実を強調するために、そのフィールドはすべてパブリックで変更可能です。このパブリックアクセシビリティは要件ではありませんが、ToBeBuiltのコンストラクタが呼び出されるまで検証が行われないという事実を強化するため、私はそれを好み、推奨します。これは重要です。なぜなら、別のスレッドがのコンストラクターに渡される前にビルダーをさらに操作する可能性があるからToBeBuiltです。フィールドが有効であることを保証する唯一の方法は、ビルダーが何らかの形でその状態を「ロック」できないと仮定して、ToBeBuiltクラスが最終チェックを行うことです。

最後に、Fieldableインターフェースと同様に、そのゲッターについては文書化していません。

パブリッククラスUserConfig_CfgはUserConfig_Fieldableを実装します{
   public String sName;
   public int iAge;
    public String sFavColor;
    public UserConfig_Cfg(String s_name){
       sName = s_name;
    }
    //自己復帰セッター... START
       public UserConfig_Cfg age(int i_age){
          iAge = i_age;
          これを返す;
       }
       public UserConfig_Cfg favoriteColor(String s_color){
          sFavColor = s_color;
          これを返す;
       }
    //自己復帰セッター... END
    //ゲッター... START
       public String getName(){
          return sName;
       }
       public int getAge(){
          iAgeを返します。
       }
       public String getFavoriteColor(){
          return sFavColor;
       }
    //ゲッター... END
    public UserConfig build(){
       return(新しいUserConfig(this));
    }
}

ブラインドビルダーでクラスをインスタンス化する

UserConfig uc = new UserConfig_Cfg( "Kermit")。age(50).favoriteColor( "green")。build();

唯一の違いは、「UserConfig_Cfg」ではなく「UserConfig.Cfg」です

ノート

短所:

  • ブラインドビルダーは、そのToBeBuiltクラスのプライベートメンバーにアクセスできません。
  • ビルダーとインターフェースの両方でゲッターが必要になったため、これらはより冗長です。
  • 単一クラスのすべてが1つの場所にあるのではありません。

ブラインドビルダーのコンパイルは簡単です。

  1. ToBeBuilt_Fieldable
  2. ToBeBuilt
  3. ToBeBuilt_Cfg

Fieldableインターフェースは完全に任意です

以下のためにToBeBuiltいくつかの必要なフィールドを持つクラス-このようなUserConfig例クラス、コンストラクタは単に可能性があり

public UserConfig(String s_name、int i_age、String s_favColor){

そして、ビルダーで呼び出されます

public UserConfig build(){
   return(new UserConfig(getName()、getAge()、getFavoriteColor()));
}

または、(ビルダーで)ゲッターを完全に削除することによっても:

   return(新しいUserConfig(sName、iAge、sFavoriteColor));

フィールドを直接渡すことにより、ToBeBuiltクラスはFieldableインターフェイスと同様に「ブラインド」(ビルダーを認識しない)になります。ただし、ToBeBuilt「この投稿のタイトルにある」「何度も拡張およびサブ拡張される」ことを意図したクラスの場合、フィールドを変更すると、すべてのサブクラス、すべてのビルダーおよびToBeBuiltコンストラクターで変更が必要になります。フィールドとサブクラスの数が増えると、これを維持するのが非現実的になります。

(実際、必要なフィールドがほとんどないので、ビルダーを使用するのはやり過ぎかもしれません。興味のある方のために、ここに私の個人用ライブラリのより大きなFieldableインターフェースのサンプルを示します。)

サブパッケージのセカンダリクラス

Fieldableすべてのブラインドビルダーのすべてのビルダーとクラスを、クラスのサブパッケージに含めることを選択しますToBeBuilt。サブパッケージの名前は常に「z」です。これにより、これらのセカンダリクラスがJavaDocパッケージリストを乱雑にすることを防ぎます。例えば

  • library.class.my.UserConfig
  • library.class.my.z.UserConfig_Fieldable
  • library.class.my.z.UserConfig_Cfg

検証例

上記のように、すべての検証はToBeBuiltのコンストラクターで行われます。検証コードの例を示したコンストラクターを次に示します。

public UserConfig(UserConfig_Fieldable uc_f){
   //転送
      {
         sName = uc_f.getName();
      } catch(NullPointerException rx){
         新しいNullPointerException( "uc_f");をスローします。
      }
      iAge = uc_f.getAge();
      sFavColor = uc_f.getFavoriteColor();
   //検証(実際にパターンをプリコンパイルする必要があります...)
      {
         if(!Pattern.compile( "\\ w +")。matcher(sName).matches()){
            throw new IllegalArgumentException( "uc_f.getName()(\" "+ sName +" \ ")は空ではなく、数字とアンダースコアのみを含める必要があります。");
         }
      } catch(NullPointerException rx){
         throw new NullPointerException( "uc_f.getName()");
      }
      if(iAge <0){
         throw new IllegalArgumentException( "uc_f.getAge()(" + iAge + ")はゼロ未満です。");
      }
      {
         if(!Pattern.compile( "(?:red | blue | green | hot pink)")。matcher(sFavColor).matches()){
            throw new IllegalArgumentException( "uc_f.getFavoriteColor()(\" "+ uc_f.getFavoriteColor()+" \ ")は赤、青、緑、またはホットピンクではありません。");
         }
      } catch(NullPointerException rx){
         throw NullPointerException( "uc_f.getFavoriteColor()");
      }
}

ビルダーの文書化

このセクションはBloch BuilderとBlind Builderの両方に適用されます。このデザインでクラスを文書化し、セッター(ビルダー内)とそのゲッター(ToBeBuiltクラス内)を相互に直接相互参照する方法を示します。マウスを1回クリックするだけで、ユーザーはどこを知る必要もありません。これらの機能は実際に存在し、開発者が何も冗長に文書化する必要はありません。

ゲッター:ToBeBuiltクラスのみ

ゲッターはToBeBuiltクラスでのみ文書化されます。クラス_Fieldable_Cfgクラスの両方の同等のゲッターは無視されます。私はそれらをまったく文書化しません。

/ **
   <P>ユーザーの年齢。</ P>
   @returnユーザーの年齢を表すint。
   @see UserConfig_Cfg#age(int)
   @see getName()
 ** /
public int getAge(){
   iAgeを返します。
}

1つ@seeは、ビルダークラスにあるセッターへのリンクです。

セッター:ビルダークラス

セッターは、文書化され、それがであるかのようにToBeBuiltクラスかのようにも、そしてそれが(本当にによって行われ、検証んToBeBuiltのコンストラクタ)。アスタリスク( " *")は、リンクのターゲットが別のクラスにあることを示す視覚的な手がかりです。

/ **
   <P>ユーザーの年齢を設定します。</ P>
   @param i_ageゼロより小さくすることはできません。{@code UserConfig#getName()getName()} *で取得します。
   @see #favoriteColor(String)
 ** /
public UserConfig_Cfg age(int i_age){
   iAge = i_age;
   これを返す;
}

さらに詳しい情報

すべてをまとめる:ブラインドビルダーのサンプルの完全なソースと完全なドキュメント

UserConfig.java

import java.util.regex.Pattern;
/ **
   <P>ユーザーに関する情報-<I> [builder:UserConfig_Cfg] </ I> </ P>
   <P>すべてのフィールドの検証は、このクラスコンストラクターで行われます。ただし、各検証要件は、ビルダーのセッター関数のドキュメントのみです。</ P>
   <P> {@ code java xbn.z.xmpl.lang.builder.finalv.UserConfig} </ P>
 ** /
パブリッククラスUserConfig {
   public static final void main(String [] igno_red){
      UserConfig uc = new UserConfig_Cfg( "Kermit")。age(50).favoriteColor( "green")。build();
      System.out.println(uc);
   }
   プライベート最終文字列sName;
   private final int iAge;
   private final String sFavColor;
   / **
      <P>新しいインスタンスを作成します。これにより、すべてのフィールドが設定および検証されます。</ P>
      @param uc_fは{@code null}にはできません。
    ** /
   public UserConfig(UserConfig_Fieldable uc_f){
      //転送
         {
            sName = uc_f.getName();
         } catch(NullPointerException rx){
            新しいNullPointerException( "uc_f");をスローします。
         }
         iAge = uc_f.getAge();
         sFavColor = uc_f.getFavoriteColor();
      //検証
         {
            if(!Pattern.compile( "\\ w +")。matcher(sName).matches()){
               throw new IllegalArgumentException( "uc_f.getName()(\" "+ sName +" \ ")は空ではなく、数字とアンダースコアのみを含める必要があります。");
            }
         } catch(NullPointerException rx){
            throw new NullPointerException( "uc_f.getName()");
         }
         if(iAge <0){
            throw new IllegalArgumentException( "uc_f.getAge()(" + iAge + ")はゼロ未満です。");
         }
         {
            if(!Pattern.compile( "(?:red | blue | green | hot pink)")。matcher(sFavColor).matches()){
               throw new IllegalArgumentException( "uc_f.getFavoriteColor()(\" "+ uc_f.getFavoriteColor()+" \ ")は赤、青、緑、またはホットピンクではありません。");
            }
         } catch(NullPointerException rx){
            throw NullPointerException( "uc_f.getFavoriteColor()");
         }
   }
   //ゲッター... START
      / **
         <P>ユーザーの名前。</ P>
         @return非{@code null}、空でない文字列。
         @see UserConfig_Cfg#UserConfig_Cfg(String)
         @see #getAge()
         @see #getFavoriteColor()
       ** /
      public String getName(){
         return sName;
      }
      / **
         <P>ユーザーの年齢。</ P>
         @returnゼロ以上の数値。
         @see UserConfig_Cfg#age(int)
         @see #getName()
       ** /
      public int getAge(){
         iAgeを返します。
      }
      / **
         <P>ユーザーの好きな色。</ P>
         @return非{@code null}、空でない文字列。
         @see UserConfig_Cfg#age(int)
         @see #getName()
       ** /
      public String getFavoriteColor(){
         return sFavColor;
      }
   //ゲッター... END
   public String toString(){
      return "getName()=" + getName()+ "、getAge()=" + getAge()+ "、getFavoriteColor()=" + getFavoriteColor();
   }
}

UserConfig_Fieldable.java

/ **
   <P> {@ link UserConfig} {@code UserConfig#UserConfig(UserConfig_Fieldable)コンストラクターが必要}。</ P>
 ** /
パブリックインターフェイスUserConfig_Fieldable {
   文字列getName();
   int getAge();
   文字列getFavoriteColor();
}

UserConfig_Cfg.java

import java.util.regex.Pattern;
/ **
   <P> {@ link UserConfig}のビルダー。</ P>
   <P>すべてのフィールドの検証は、<CODE> UserConfig </ CODE>コンストラクターで行われます。ただし、各検証要件は、このクラスのセッター関数でのみ文書化されます。</ P>
 ** /
パブリッククラスUserConfig_CfgはUserConfig_Fieldableを実装します{
   public String sName;
   public int iAge;
   public String sFavColor;
   / **
      <P>ユーザーの名前で新しいインスタンスを作成します。</ P>
      @param s_name {@code null}または空であってはならず、文字、数字、アンダースコアのみを含める必要があります。{@code UserConfig#getName()getName()} {@ code()}で取得します。
    ** /
   public UserConfig_Cfg(String s_name){
      sName = s_name;
   }
   //自己復帰セッター... START
      / **
         <P>ユーザーの年齢を設定します。</ P>
         @param i_ageゼロより小さくすることはできません。{@code UserConfig#getName()getName()} {@ code()}で取得します。
         @see #favoriteColor(String)
       ** /
      public UserConfig_Cfg age(int i_age){
         iAge = i_age;
         これを返す;
      }
      / **
         <P>ユーザーのお気に入りの色を設定します。</ P>
         @param s_color {@code "red"}、{@ code "blue"}、{@ code green}、または{@code "hot pink"}でなければなりません。{@code UserConfig#getName()getName()} {@ code()} *で取得します。
         @see #age(int)
       ** /
      public UserConfig_Cfg favoriteColor(String s_color){
         sFavColor = s_color;
         これを返す;
      }
   //自己復帰セッター... END
   //ゲッター... START
      public String getName(){
         return sName;
      }
      public int getAge(){
         iAgeを返します。
      }
      public String getFavoriteColor(){
         return sFavColor;
      }
   //ゲッター... END
   / **
      <P>構成に従ってUserConfigをビルドします。</ P>
      @return <CODE>(新しい{@link UserConfig#UserConfig(UserConfig_Fieldable)UserConfig}(this))</ CODE>
    ** /
   public UserConfig build(){
      return(新しいUserConfig(this));
   }
}


1
間違いなく、それは改善です。ここで実装されているBloch's Builderは、2つの具体的なクラスを結合します。これらは、作成れるクラスとそのビルダーです。これはそれ自体が悪い設計です。あなたが記述するブラインドビルダーは、構築されるクラスにその構築依存関係を抽象化として定義させることにより、その結合を破ります。基本的なオブジェクト指向の設計ガイドラインであるものを大幅に適用しました。
rucamzu

3
アルゴリズム設計のすばらしい部分をまだお持ちでない場合は、どこかで実際にブログに投稿してください!私は今それを共有していません:-)。
Martijn Verburg

4
優しい言葉をありがとう。これが私の新しいブログの最初の投稿です:aliteralmind.wordpress.com/2014/02/14/blind_builder
aliteralmind

ビルダーとビルドされたオブジェクトの両方がFieldableを実装する場合、パターンは私がReadableFoo / MutableFoo / ImmutableFooと呼んでいるものに似始めますが、ビルダーの「ビルド」メンバーになるようにメソッドを作成するのではなく、それasImmutableを呼び出してReadableFooインターフェイスに含めます(その哲学を使用するとbuild、不変オブジェクトを呼び出すと、同じオブジェクトへの参照が返されます)。
supercat

1
@ThomasN *_Fieldable新しいゲッターを拡張して追加し、を拡張して *_Cfg新しいセッターを追加する必要がありますが、既存のゲッターとセッターを再現する必要がある理由がわかりません。それらは継承され、異なる機能が必要でない限り、それらを再作成する必要はありません。
aliteralmind

13

ここでの質問は、最初から何かを証明しようとせず、ビルダーパターンが本質的に優れていることを前提としていると思います。

tl; drビルダーパターンは 良いアイデアになることめったにないと思います。


ビルダーパターンの目的

ビルダーパターンの目的は、クラスの使用を容易にする2つのルールを維持することです。

  1. 一貫性のない/使用できない/無効な状態でオブジェクトを構築することはできません。

    • これは、たとえば、PersonオブジェクトをId塗りつぶさずに構築できるシナリオを指しますが、そのオブジェクトを使用するすべてのコードIdで適切に動作するだけでよい場合がありますPerson
  2. オブジェクトコンストラクターは、あまり多くのパラメーターを必要としません。

したがって、ビルダーパターンの目的は論争の余地のないものです。私はそれの欲求と使用法の多くはこれまで基本的に行ってきた分析に基づいていると思います:これらの2つのルールが必要です、これはこれらの2つのルールを与えます-これらの2つのルールを達成する他の方法を調査する価値があると思いますが


なぜ他のアプローチに目を向けるのですか?

理由は、この質問自体の事実によって十分に示されていると思います。構造が複雑であり、ビルダーパターンを構造に適用する際に構造に多くのセレモニーが追加されます。この質問は、その複雑さの一部をどのように解決するかを尋ねています。なぜなら、それは複雑さのように、奇妙に振る舞う(継承する)シナリオを作るからです。この複雑さにより、メンテナンスのオーバーヘッドも増加します(プロパティの追加、変更、削除は、他の方法よりもはるかに複雑です)。


その他のアプローチ

それでは、上記のルール1では、どのようなアプローチがありますか?このルールが参照するキーは、構築時にオブジェクトが適切に機能するために必要なすべての情報を持ち、構築後にその情報を外部から変更できないことです(したがって、不変情報です)。

構築時に必要なすべての情報をオブジェクトに提供する1つの方法は、単にコンストラクターにパラメーターを追加することです。その情報がコンストラクターによって要求された場合、すべての情報がなければこのオブジェクトを構築することはできません。したがって、有効な状態に構築されます。しかし、オブジェクトが有効であるために多くの情報が必要な場合はどうでしょうか?ああ、このアプローチが上記のルール#2に違反する場合です。

他に何がありますか?オブジェクトを一貫した状態にするために必要なすべての情報を取得し、構築時に取得する別のオブジェクトにまとめることができます。ビルダーパターンを使用する代わりに上記のコードは次のようになります。

//DTO...START
public class Cfg  {
   public String sName    ;
   public int    iAge     ;
   public String sFavColor;
}
//DTO...END

public class UserConfig  {
   private final String sName    ;
   private final int    iAge     ;
   private final String sFavColor;
   public UserConfig(Cfg uc_c)  {
      ...
   }

   public String toString()  {
      return  "name=" + sName + ", age=" + iAge + ", sFavColor=" + sFavColor;
   }
}

これはビルダーパターンとそれほど大きな違いはありませんが、少し単純ですが、最も重要なことは、今ではルール1とルール2を満たしていることです

それでは、もう少し余分に行って、ビルダーでいっぱいにしてみませんか? 単に不要です。私は、このアプローチのビルダーパターンの両方の目的を、少しシンプルで、保守しやすく、再利用可能なもので満たしました。その最後のビットが使用されている。この例では架空ですので、のショーはどのようにしましょう、現実世界の意味的な目的に役立たない、鍵となる再利用可能なDTOはなく、単一目的のクラスで、このアプローチの結果

public class NetworkAddress {
   public String Ip;
   public int Port;
   public NetworkAddress Proxy;
}

public class SocketConnection {
   public SocketConnection(NetworkAddress address) {
      ...
   }
}

public class FtpClient {
   public FtpClient(NetworkAddress address) {
      ...
   }
}

したがって、このような凝集性のある DTO を構築すると、ビルダーパターンの目的をより簡単に、より広い価値/有用性で満たすことができます。さらに、このアプローチは、ビルダーパターンがもたらす継承の複雑さを解決します。

public class SslCert {
   public NetworkAddress Authority;
   public byte[] PrivateKey;
   public byte[] PublicKey;
}

public class FtpsClient extends FtpClient {
   public FtpsClient(NetworkAddress address, SslCert cert) {
      super(address);
      ...
   }
}

DTOが常にまとまりがあるとは限らないか、プロパティのグループ化をまとまりのあるものにするために、複数のDTOで分割する必要がある場合があります。これは実際には問題ではありません。オブジェクトに18個のプロパティが必要で、それらのプロパティを使用して3つのまとまりのあるDTOを作成できる場合、ビルダーの目的に合ったシンプルな構造が得られます。凝集性のグループ化を考え出すことができない場合、これは、オブジェクトがまったく無関係なプロパティを持っている場合、オブジェクトが凝集性でないというサインかもしれません-しかし、それでも単一の非凝集性DTOを作成することは、よりシンプルな実装プラスのためにまだ望ましいです継承の問題を解決します。


ビルダーパターンを改善する方法

わかりましたので、すべてのとりとめのないとりとめは別として、あなたには問題があり、それを解決するための設計アプローチを探しています。私の提案:継承クラスは、スーパークラスのビルダークラスから継承するネストされたクラスを単純に持つことができるため、継承クラスは基本的にスーパークラスと同じ構造を持ち、追加機能で正確に機能するビルダーパターンを持っていますサブクラスの追加プロパティの場合。


いいアイデアだと

余談ですが、ビルダーパターンにはニッチがあります。ある時点でこの特定のビルダーを学んだからです。StringBuilderます -ここでは、目的は単純な構築ではありません。文字列の構築や連結などは簡単ではないためです。 。

したがって、パフォーマンス上の利点は次のとおりです。オブジェクトがたくさんあり、それらは不変型であり、それらを不変型の1つのオブジェクトにまとめる必要があります。これを段階的に行うと、ここで多くの中間オブジェクトが作成されるため、一度にすべてを実行する方がはるかにパフォーマンスが高く理想的です。

だから私はそれが良いアイデアであるときのキーは問題領域にあると思いますStringBuilder不変型の複数のインスタンスを不変型の単一インスタンスに変える必要がある


あなたの与えられた例はどちらのルールも満たしていないと思います。無効な状態でCfgを作成するのを止めるものは何もありません。また、パラメーターがctorから移動されている間、それらはより慣用的でより冗長な場所に移動されました。fooBuilder.withBar(2).withBang("Hello").withBaz(someComplexObject).build()fooを構築するための簡潔なAPIを提供し、ビルダー自体で実際のエラーチェックを提供できます。ビルダーがないと、オブジェクト自体がその入力をチェックする必要があります。つまり、以前よりも良くなっているということです。
Phoshi

DTOは、セッターでアノテーションを使用して宣言的にさまざまな方法でプロパティを検証できますが、あなたはそれをやりたいです-検証は別の問題であり、彼のビルダーアプローチでは、コンストラクターで検証が発生していることを示し、同じロジックが完全に適合します私のアプローチで。ただし、一般に、DTOを使用して検証する方がよいでしょう。これは、DTOを使用して複数の型を構築できるため、検証を行うと複数の型の検証に役立つためです。ビルダーは、作成された特定のタイプに対してのみ検証します。
ジミーホファ14

おそらく、最も柔軟な方法は、単一のFieldableパラメーターを受け入れるビルダーで静的検証関数を使用することです。私はからこの検証関数を呼び出しますToBeBuiltコンストラクタが、それはどこからでも、どこからも呼び出すことができます。これにより、特定の実装を強制することなく、冗長コードの可能性が排除されます。(そして、Fieldableコンセプトが気に入らない場合、個々のフィールドを検証関数に渡すことを止めるものは何もありませんが、フィールドリストを維持する必要がある少なくとも3つの場所があります。)
aliteralmind 14

+1そして、コンストラクタに依存関係が多すぎるクラスは、明らかに十分に凝集性がなく、より小さなクラスにリファクタリングする必要があります。
バシレフ14

@JimmyHoffa:ああ、なるほど、あなたはそれを省略しました。私はこれとビルダーの違いがわからないのですが、これ以外は、いくつかのビルダーで.buildを呼び出す代わりにconfigインスタンスをctorに渡し、ビルダーにはすべての正確性をチェックするためのより明白なパスがありますデータ。個々の変数は有効範囲内にありますが、その特定の順列では無効になります。.buildはこれをチェックできますが、アイテムをctorに渡すには、オブジェクト自体の内部でエラーチェックが必要です。
Phoshi
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.