ジョシュア・ブロッホの効果的なJava本(第2版))から大きな影響を受けました。おそらく、私が読んだどのプログラミング本よりも影響が大きいでしょう。最大の効果。
Blochのビルダーは、過去10年間のプログラミングよりも数か月で私をはるかに遠くに連れて行っていますが、私は同じ壁にぶつかっています:自己復帰メソッドチェーンでクラスを拡張することは、せいぜい落胆し、最悪の場合悪夢です-特にジェネリックが登場する場合、および特に自己参照ジェネリック(Comparable<T extends Comparable<T>>
など)。
私の主なニーズは2つありますが、この質問で焦点を当てたいのは2つ目だけです。
最初の問題は、「すべての...単一の...クラスでそれらを再実装する必要なしに、自己復帰メソッドチェーンを共有する方法」です。気になるかもしれない人のために、私はこの回答の投稿の下部でこの部分に対処しましたが、ここで焦点を当てたいのはそれではありません。
私がコメントを求めている2番目の問題は、「他の多くのクラスによって拡張されることを目的としたクラスにビルダーを実装するにはどうすればよいですか?」ビルダーを使用してクラスを拡張することは、クラスを拡張せずに拡張するよりも当然困難です。ビルダーがあるクラスを拡張するNeedable
も実装しているため、それに関連する重要なジェネリックがあるは扱いにくいです。
それが私の質問です:Bloch Builder(私が呼ぶもの)をどのように改善できますか?そのクラスが意図されている場合でも、ビルダーをanyクラスに自由にアタッチできます-何回も拡張およびサブ拡張される可能性のある「基本クラス」--私の将来の自分、または私のライブラリのユーザーを落胆させることなく、ビルダー(およびその潜在的なジェネリック)の追加の手荷物のためそれらに課す?
補遺
私の質問は上記のパート2に焦点を当てていますが、問題1について少し詳しく説明したいと思います。
最初の問題は、「すべての...単一の...クラスでそれらを再実装する必要なしに、自己復帰メソッドチェーンを共有する方法」です。これはextending classesがこれらのチェーンを再実装しなければならないことを防ぐためではありません。もちろん、チェーンを再実装する必要があります-non-sub-classesを防ぐ方法は、 theirユーザーがそれらを利用できるようにするために、すべての自己復帰関数を再実装する必要がないことから、これらのメソッドチェーンを利用したいと考えていますか?このために、私はここにインターフェーススケルトンを印刷し、今のところはそのままにしておく必要のある必要なデザインを考え出しました。それは私にとってうまくいきました(この設計は何年もの間作られていました...最も難しい部分は循環依存を避けることでした):
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);
}
Josh BlochのBuilderパターンを大幅に改善したものを作成しました。それが「より良い」とは言いませんが、非常に特定の状況では、いくつかの利点があります-最大のものはそれですこれは、ビルダーをビルドされるクラスから分離します。
私はこの代替案を完全に文書化しました。これをブラインドビルダーパターンと呼びます。
Joshua Blochの Builderパターン (Effective Java、第2版の項目2)の代わりに、Bloch Builderの多くの利点を共有する「Blind Builderパターン」と呼ばれるものを作成しました。は、単一の文字を除いて、まったく同じ方法で使用されます。ブラインドビルダーには、
ToBeBuilt
クラスを拡張できるようにします ビルダーを拡張する必要なし。このドキュメントでは、ビルドされるクラスを「ToBeBuilt
」クラスと呼びます。
Bloch Builderは、それが構築するクラス内に含まれるpublic static class
です。例:
public class UserConfig { private final String sName; private final int iAge; private final String sFavColor; public UserConfig(UserConfig.Cfg uc_c){// CONSTRUCTOR //転送 try { sName = uc_c.sName; } catch(NullPointerException rx){ throw new NullPointerException( "uc_c "); } iAge = uc_c.iAge; sFavColor = uc_c.sFavColor; //ここですべてのフィールドを検証 } public String toString(){ return "name =" + sName + "、age =" + iAge + "、sFavColor =" + sFavColor; } //builder...START public static class Cfg { private String sName; private int iAge; private String sFavColor; public Cfg (文字列s_name){ sName = s_name; } // se lfを返すセッター... START public Cfg age(int i_age){ iAge = i_age; return this; } public Cfg favouriteColor(String s_color){ sFavColor = s_color; return this; } //自己復帰セッター... END public UserConfig build(){ return(new UserConfig(this)); } } //ビルダー... END }
UserConfig uc = new UserConfig.Cfg( "Kermit")。age(50).favoriteColor( "green")。build();
ブラインドビルダーには3つの部分があり、それぞれが個別のソースコードファイルにあります。
ToBeBuilt
クラス(この例ではUserConfig
)Fieldable
」インターフェースビルドされるクラスは、そのFieldable
インターフェイスを唯一のコンストラクタパラメータとして受け入れます。コンストラクタはそこからすべての内部フィールドを設定し、 そして検証する 各。最も重要なのは、このToBeBuilt
クラスがそのビルダーを認識していないことです。
public class UserConfig { private final String sName; private final int iAge; private final String sFavColor; public UserConfig(UserConfig_Fieldable uc_f){// CONSTRUCTOR //転送 試行{ sName = uc_f.getName(); } catch(NullPointerException rx){ throw new 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
のフィールドが有効であることがわかっている場合でも、フィールドが常に検証されることです)。
Fieldable
"インターフェイスフィールド化可能なインターフェースは、ToBeBuilt
クラスとそのビルダーの間の「ブリッジ」であり、オブジェクトの構築に必要なすべてのフィールドを定義します。このインターフェースはToBeBuilt
クラスコンストラクターに必要であり、ビルダーによって実装されます。このインターフェイスはビルダー以外のクラスによって実装される可能性があるため、どのクラスでも、ビルダーを使用せずにToBeBuilt
クラスを簡単にインスタンス化できます。また、ビルダーの拡張が望ましくない、または必要ない場合に、ToBeBuilt
クラスを拡張するのが簡単になります。
以下のセクションで説明するように、私はこのインターフェースの関数をまったく文書化していません。
パブリックインターフェイスUserConfig_Fieldable { String getName(); int getAge(); String getFavoriteColor(); }
ビルダーは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; return this; } //セルフリターンセッター... END //ゲッター... START public String getName(){ return sName; } public int getAge(){ return iAge; } public String getFavoriteColor(){ return sFavColor; } //ゲッター... END public UserConfig build (){ return(new UserConfig(this)); } }
UserConfig uc = new UserConfig_Cfg( "Kermit")。age(50).favoriteColor( "green")。build();
唯一の違いは、「UserConfig_Cfg
」ではなく「UserConfig.Cfg
」です。
短所:
ToBeBuilt
クラスのプライベートメンバーにアクセスできません。ブラインドビルダーのコンパイルは簡単です。
ToBeBuilt_Fieldable
ToBeBuilt
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
」です。これにより、これらの2次クラスがJavaDocパッケージリストを混乱させることを防ぎます。例えば
library.class.my.UserConfig
library.class.my.z.UserConfig_Fieldable
library.class.my.z.UserConfig_Cfg
上記のように、すべての検証はToBeBuilt
のコンストラクターで行われます。以下は、検証コードの例を含むコンストラクタです。
public UserConfig(UserConfig_Fieldable uc_f){ //transfer try { sName = uc_f.getName(); } catch(NullPointerException rx){ throw new 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){ 新しい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){ 新しいNullPointerException( "uc_f.getFavoriteColor()"); } }をスローします
このセクションは、Bloch BuilderとBlind Buildersの両方に適用されます。これは、この設計でクラスを文書化し、セッター(ビルダー)とゲッター(ToBeBuilt
クラス)を相互に直接相互参照する方法を示しています。マウスを1回クリックするだけで、ユーザーは、これらの関数が実際にどこにあるかを知る必要があり、開発者は何も冗長に文書化する必要がありません。
ToBeBuilt
クラスのみゲッターはToBeBuilt
クラスでのみドキュメント化されています。 _Fieldable
および_Cfg
クラスの両方の同等のゲッターは無視されます。私はそれらをまったく文書化しません。
/** <P>ユーザーの年齢。</ P> @returnユーザーの年齢を表すint。 @see UserConfig_Cfg#age(int) @see getName() **/ public int getAge(){ return iAge; }
最初の@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; これを返す; }
Why should a builder be an inner class instead of in its own class file?
Benefit of using static inner builder class
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> **/ public class UserConfig { public static final void main(String [] igno_red){ UserConfig uc = new UserConfig_Cfg( "Kermit")。age(50).favoriteColor( "green")。build(); System.out.println(uc); } private final String 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){ throw new NullPointerException( "uc_f"); } iAge = uc_f.getAge(); sFavColor = uc_f.getFavoriteColor(); //validate try { 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 + ")is less than zero。"); } 試してみてください{ if(!Pattern.compile( "(?:red | blue | green | hot pink)")。matcher(sFavColor).matches()){ スロー新しいIllegalArgumentException( "uc_f.getFavoriteColor()(\" "+ uc_f.getFavoriteColor()+"\")は赤、青、緑、またはホットピンクではありません。"); } } catch(NullPointerException rx){ throw new NullPointerException( "uc_f.getFavoriteColor()"); } } // getters ... START /** <P>ユーザーの名前。</ P> @return {@code null}以外、空でない文字列。 @see UserConfig_Cfg#UserConfig_Cfg(String) @see #getAge() @see #getFavoriteColor() **/ public String getName(){ return sName; } /** <P>ユーザーの年齢。</ P> @return Aゼロ以上の数値。 @see UserConfig_Cfg#age(int) @see #getName() **/ public int getAge(){ return iAge; } /** <P>ユーザーの好きな色。</ P> @ {@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 { String getName(); int getAge(); String getFavoriteColor(); }
UserConfig_Cfg.Java
import Java.util.regex.Pattern; /** <P> Builder for {@link UserConfig}。</ P> <P>すべてのフィールドの検証が行われます<CODE> UserConfig </ CODE>コンストラクタ内。ただし、各検証要件は、このクラスセッター関数のみのドキュメントです。</ P> **/ public class UserConfig_Cfg implements 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; return this; } /** < P>ユーザーの好みの色を設定します。</ P> @param s_color {@code "red"}、{@ code "blue"}、{@ code green}、または{@code "hotピンク"}。 {@code UserConfig#getName()getName()} {@ code()} *で取得します。 @see #age(int) **/ public UserConfig_Cfg favouriteColor (文字列s_color){ sFavColor = s_color; return this; } //セルフリターンセッター... END // getters ... START public String getName(){ return sName; } public int getAge(){ return iAge; } public String getFavoriteColor(){ return sFavColor; } //ゲッター... END /** <P>設定どおりにUserConfigをビルドします。</ P> @return <CODE>(new {@link UserConfig#UserConfig(UserConfig_Fieldable)UserConfig}(this))</ CODE> **/ public UserConfig build(){ return(new UserConfig(this)); } }
ここでの質問は、ビルダーパターンが本質的に優れていることを証明しようとせずに、最初から何かを想定していると思います。
tl; drビルダーパターンはrarelyだと思います。
ビルダーパターンの目的は、クラスの使用を容易にする2つのルールを維持することです。
オブジェクトは、一貫性のない、使用できない、無効な状態で構築できないようにする必要があります。
Person
オブジェクトをId
に入力せずに作成できるシナリオを指しますが、そのオブジェクトを使用するすべてのコードはrequireId
は、Person
を適切に使用するために使用します。オブジェクトコンストラクターは パラメータが多すぎます を必要としません。
したがって、ビルダーパターンの目的は、論争の余地なく良好です。私はそれの欲求と使用法の多くが基本的にこれまで行ってきた分析に基づいていると思います:これらの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) {
...
}
}
したがって、このようにcohesive 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を作成できる場合、ビルダーの目的に合う単純な構造が得られます。まとまりのあるグループ化が思いつかない場合、完全に関連性のないプロパティがある場合、オブジェクトがまとまりのない兆候である可能性があります。継承の問題を解決します。
さて、よろめきの話はさておき、あなたは問題を抱えており、それを解決するための設計アプローチを探しています。私の提案:継承するクラスはスーパークラスのビルダークラスから継承するネストされたクラスを持つことができるので、継承するクラスは基本的にスーパークラスと同じ構造を持ち、追加の関数とまったく同じように機能するビルダーパターンを持っていますサブクラスの追加プロパティ用。
ビルダーパターンにはニッチがあります。ある時点でこの特定のビルダーをすべて学んだので、誰もが知っています:StringBuilder
-ここでの目的は単純な構築ではありません。文字列は簡単に構築および連結できないためです。これは素晴らしいビルダーです。パフォーマンス上の利点があるためです。
したがって、パフォーマンス上の利点は次のとおりです。たくさんのオブジェクトがあり、それらは不変タイプであり、それらを不変タイプの1つのオブジェクトに折りたたむ必要があります。これを段階的に行うと、多くの中間オブジェクトが作成されるため、一度にすべてを行う方がはるかにパフォーマンスが高く、理想的です。
だから私はそれが良いアイデアである場合の鍵はStringBuilder
の問題領域にあると思います:不変型の複数のインスタンスを単一のインスタンスに変える必要がある不変タイプ。