45

動機

最近、コンストラクターに多くのパラメーターを渡さずに複雑なオブジェクトを初期化する方法を探しました。ビルダーパターンで試してみましたが、本当に必要な値をすべて設定したかどうかをコンパイル時に確認できないのが気になりません。

従来のビルダーパターン

ビルダーパターンを使用してComplexオブジェクトを作成すると、引数が何に使用されているかを確認しやすくなるため、作成はより「タイプセーフ」になります。

new ComplexBuilder()
        .setFirst( "first" )
        .setSecond( "second" )
        .setThird( "third" )
        ...
        .build();

しかし今、私は問題を抱えています。それは、重要なパラメータを簡単に見逃してしまう可能性があるということです。build()メソッド内で確認できますが、それは実行時のみです。コンパイル時に、何かを見逃した場合に警告するものは何もありません。

強化されたビルダーパターン

今の私のアイデアは、必要なパラメーターを見逃した場合に「思い出させる」ビルダーを作成することでした。私の最初の試みは次のようになります:

public class Complex {
    private String m_first;
    private String m_second;
    private String m_third;

    private Complex() {}

    public static class ComplexBuilder {
        private Complex m_complex;

        public ComplexBuilder() {
            m_complex = new Complex();
        }

        public Builder2 setFirst( String first ) {
            m_complex.m_first = first;
            return new Builder2();
        }

        public class Builder2 {
            private Builder2() {}
            Builder3 setSecond( String second ) {
                m_complex.m_second = second;
                return new Builder3();
            }
        }

        public class Builder3 {
            private Builder3() {}
            Builder4 setThird( String third ) {
                m_complex.m_third = third;
                return new Builder4();
            }
        }

        public class Builder4 {
            private Builder4() {}
            Complex build() {
                return m_complex;
            }
        }
    }
}

ご覧のとおり、ビルダークラスの各セッターは異なる内部ビルダークラスを返します。各内部ビルダークラスは正確に1つのsetterメソッドを提供し、最後のクラスはbuild()メソッドのみを提供します。

これで、オブジェクトの構築は次のようになります。

new ComplexBuilder()
    .setFirst( "first" )
    .setSecond( "second" )
    .setThird( "third" )
    .build();

...しかし、必要なパラメータを忘れる方法はありません。コンパイラはそれを受け入れません。

オプションのパラメータ

オプションのパラメーターがある場合は、最後の内部ビルダークラスを使用してBuilder4、「従来の」ビルダーと同じようにパラメーターを設定し、それ自体を返します。

質問

  • これはよく知られているパターンですか?特別な名前はありますか?
  • 落とし穴はありますか?
  • コードの行数を減らすという意味で、実装を改善するためのアイデアはありますか?
4

10 に答える 10

24

従来のビルダーパターンはすでにこれを処理しています。コンストラクターで必須パラメーターを取得するだけです。もちろん、呼び出し元がnullを渡すことを妨げるものはありませんが、メソッドも同様です。

私があなたのメソッドで見た大きな問題は、必須パラメーターの数を含むクラスの組み合わせ爆発があるか、ユーザーに1つの特定のシーケンスでパラメーターを設定するように強制することです。これは煩わしいことです。

また、それは多くの追加作業です。

于 2009-10-28T17:23:31.313 に答える
17

いいえ、新しいものではありません。実際に行っているのは、ブランチをサポートするように標準のビルダーパターンを拡張することで、一種のDSLを作成することです。これは、ビルダーが実際のオブジェクトに対して一連の競合する設定を生成しないようにするための優れた方法です。

個人的には、これはビルダーパターンの優れた拡張機能であり、さまざまな興味深いことを実行できると思います。たとえば、職場では、データ整合性テスト用のDSLビルダーがありますassertMachine().usesElectricity().and().makesGrindingNoises().whenTurnedOn();。OK、おそらく最良の例ではないかもしれませんが、私はあなたがポイントを得ると思います。

于 2009-10-28T17:20:28.857 に答える
16
public class Complex {
    private final String first;
    private final String second;
    private final String third;

    public static class False {}
    public static class True {}

    public static class Builder<Has1,Has2,Has3> {
        private String first;
        private String second;
        private String third;

        private Builder() {}

        public static Builder<False,False,False> create() {
            return new Builder<>();
        }

        public Builder<True,Has2,Has3> setFirst(String first) {
            this.first = first;
            return (Builder<True,Has2,Has3>)this;
        }

        public Builder<Has1,True,Has3> setSecond(String second) {
            this.second = second;
            return (Builder<Has1,True,Has3>)this;
        }

        public Builder<Has1,Has2,True> setThird(String third) {
            this.third = third;
            return (Builder<Has1,Has2,True>)this;
        }
    }

    public Complex(Builder<True,True,True> builder) {
        first = builder.first;
        second = builder.second;
        third = builder.third;
    }

    public static void test() {
        // Compile Error!
        Complex c1 = new Complex(Complex.Builder.create().setFirst("1").setSecond("2"));

        // Compile Error!
        Complex c2 = new Complex(Complex.Builder.create().setFirst("1").setThird("3"));

        // Works!, all params supplied.
        Complex c3 = new Complex(Complex.Builder.create().setFirst("1").setSecond("2").setThird("3"));
    }
}
于 2013-07-27T09:10:11.653 に答える
13

ビルダーコンストラクターに「必要な」パラメーターを入れてみませんか?

public class Complex
{
....
  public static class ComplexBuilder
  {
     // Required parameters
     private final int required;

     // Optional parameters
     private int optional = 0;

     public ComplexBuilder( int required )
     {
        this.required = required;
     } 

     public Builder setOptional(int optional)
     {
        this.optional = optional;
     }
  }
...
}

このパターンは、EffectiveJavaで概説されています

于 2009-10-28T17:17:47.363 に答える
7

複数のクラスを使用する代わりに、1つのクラスと複数のインターフェイスを使用します。それほど多くの入力を必要とせずに構文を強制します。また、関連するすべてのコードを間近で確認できるため、コードで何が起こっているのかをより大きなレベルで理解しやすくなります。

于 2011-07-07T15:48:13.800 に答える
5

私見、これは肥大化したようです。すべてのパラメーターが必要な場合、コンストラクターに渡します。

于 2009-10-28T17:21:52.860 に答える
5

私はこれを見た/使用しました:

new ComplexBuilder(requiredvarA, requiedVarB).optional(foo).optional(bar).build();

次に、これらを必要とするオブジェクトに渡します。

于 2009-10-28T17:24:31.467 に答える
2

Builderパターンは通常、オプションのパラメーターが多数ある場合に使用されます。多くの必須パラメーターが必要な場合は、最初に次のオプションを検討してください。

  • あなたのクラスはやりすぎかもしれません。単一責任原則に違反していないことを再確認してください。必要なインスタンス変数が非常に多いクラスが必要な理由を自問してください。
  • コンストラクターがやりすぎている可能性があります。コンストラクターの仕事は構築することです。(名前を付けたとき、彼らはあまり創造的ではありませんでした; D)クラスと同じように、メソッドには単一責任の原則があります。コンストラクターがフィールドの割り当て以上のことを行っている場合は、それを正当化する正当な理由が必要です。BuilderではなくFactoryメソッドが必要な場合があります。
  • パラメータの処理が少なすぎる可能性があります。パラメータを小さな構造体(またはJavaの場合は構造体のようなオブジェクト)にグループ化できるかどうかを自問してください。少人数のクラスを作ることを恐れないでください。構造体または小さなクラスを作成する必要がある場合は、大きなクラスではなく、構造体に属する機能 リファクタリング する ことを忘れないでください。
于 2013-02-23T18:55:11.173 に答える
1

Builderパターンを使用するタイミングとその利点の詳細については、私の投稿で別の同様の質問を確認してください。

于 2009-12-23T18:42:09.580 に答える
0

質問1:パターンの名前に関して、私は「ステップビルダー」という名前が好きです。

質問2/3:落とし穴と推奨事項に関して、これはほとんどの状況で複雑すぎると感じます。

  • 私の経験では珍しい、ビルダーの使用方法にシーケンスを適用しています。場合によってはこれがどのように重要になるかはわかりましたが、必要になったことがありません。たとえば、ここでシーケンスを強制する必要はありません。

    Person.builder().firstName("John").lastName("Doe").build() Person.builder().lastName("Doe").firstName("John").build()

  • ただし、多くの場合、ビルダーは、偽のオブジェクトがビルドされるのを防ぐために、いくつかの制約を適用する必要がありました。必要なすべてのフィールドが提供されていること、またはフィールドの組み合わせが有効であることを確認したい場合があります。これが、建物にシーケンスを導入したい本当の理由だと思います。

    この場合、build()メソッドで検証を行うためのJoshuaBlochの推奨が好きです。この時点ですべてが利用可能であるため、これはクロスフィールド検証に役立ちます。この回答を参照してください:https ://softwareengineering.stackexchange.com/a/241320

要約すると、ビルダーメソッドの呼び出しを「見逃す」ことを心配しているという理由だけで、コードに複雑さを加えることはしません。実際には、これはテストケースで簡単に見つけることができます。たぶん、バニラビルダーから始めて、メソッド呼び出しの欠落に悩まされ続ける場合は、これを導入してください。

于 2015-03-05T20:30:57.397 に答える