web-dev-qa-db-ja.com

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

動機

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

従来のビルダーパターン

ビルダーパターンを使用して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_を使用して、「従来の」ビルダーと同じようにパラメーターを設定し、それ自体を返します。

質問

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

いいえ、新しいものではありません。実際にそこで行っているのは、ブランチをサポートするように標準のビルダーパターンを拡張することで、一種の [〜#〜] dsl [〜#〜] を作成することです。これは、とりわけ優れた方法です。ビルダーが実際のオブジェクトに対して競合する設定のセットを生成しないことを確認してください。

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

16
Esko

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

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

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

23
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"));
    }
}
14
clinux

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

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;
     }
  }
...
}

このパターンの概要は Effective Java です。

12
Martin

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

7
Sled

私見、これは肥大化しているようです。 haveですべてのパラメーターを取得する場合は、それらをコンストラクターに渡します。

5
JRL

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

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

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

5
reccles

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

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

ビルダーパターンを使用するタイミングとその利点の詳細については、私の投稿で別の同様の質問を確認する必要がありますここ

1
Aaron

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

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

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

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

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

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

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

0
brariden