Java:セッターの順序が重要ではないステップビルダーを実装する方法は?


10

編集:この質問は理論的な問題を説明していることを指摘したいと思います。必須パラメーターにコンストラクター引数を使用できること、またはAPIが正しく使用されていない場合はランタイム例外をスローできることを認識しています。ただし、コンストラクター引数やランタイムチェックを必要としないソリューションを探しています。

次のCarようなインターフェースがあるとします。

public interface Car {
    public Engine getEngine(); // required
    public Transmission getTransmission(); // required
    public Stereo getStereo(); // optional
}

コメントが示唆するCarように、Engineとは必須ですTransmissionが、Stereoはオプションです。つまりbuild()Carインスタンスが可能なビルダーは、ビルダーインスタンスにとの両方が既に与えられているbuild()場合にのみメソッドEngineTransmission持つ必要があります。その方法は、型チェッカーが試みが作成することは、任意のコードコンパイルを拒否しますCarせずにインスタンスEngineまたはTransmission

これには、ステップビルダーが必要です。通常、次のようなものを実装します。

public interface Car {
    public Engine getEngine(); // required
    public Transmission getTransmission(); // required
    public Stereo getStereo(); // optional

    public class Builder {
        public BuilderWithEngine engine(Engine engine) {
            return new BuilderWithEngine(engine);
        }
    }

    public class BuilderWithEngine {
        private Engine engine;
        private BuilderWithEngine(Engine engine) {
            this.engine = engine;
        }
        public BuilderWithEngine engine(Engine engine) {
            this.engine = engine;
            return this;
        }
        public CompleteBuilder transmission(Transmission transmission) {
            return new CompleteBuilder(engine, transmission);
        }
    }

    public class CompleteBuilder {
        private Engine engine;
        private Transmission transmission;
        private Stereo stereo = null;
        private CompleteBuilder(Engine engine, Transmission transmission) {
            this.engine = engine;
            this.transmission = transmission;
        }
        public CompleteBuilder engine(Engine engine) {
            this.engine = engine;
            return this;
        }
        public CompleteBuilder transmission(Transmission transmission) {
            this.transmission = transmission;
            return this;
        }
        public CompleteBuilder stereo(Stereo stereo) {
            this.stereo = stereo;
            return this;
        }
        public Car build() {
            return new Car() {
                @Override
                public Engine getEngine() {
                    return engine;
                }
                @Override
                public Transmission getTransmission() {
                    return transmission;
                }
                @Override
                public Stereo getStereo() {
                    return stereo;
                }
            };
        }
    }
}

別のビルダークラスのチェーンがあります(BuilderBuilderWithEngineCompleteBuilder)、その追加1は、だけでなく、すべてのオプションのsetterメソッドを含む最後のクラスで、別の後にsetterメソッドが必要でした。
つまり、このステップビルダーのユーザーは、作成者が必須のセッターを使用可能にした順序に制限されます。可能な使用法の例を次に示します(すべて厳密に順序付けられていることに注意してください:engine(e)最初に、次にtransmission(t)、最後にオプションstereo(s))。

new Builder().engine(e).transmission(t).build();
new Builder().engine(e).transmission(t).stereo(s).build();
new Builder().engine(e).engine(e).transmission(t).stereo(s).build();
new Builder().engine(e).transmission(t).engine(e).stereo(s).build();
new Builder().engine(e).transmission(t).stereo(s).engine(e).build();
new Builder().engine(e).transmission(t).transmission(t).stereo(s).build();
new Builder().engine(e).transmission(t).stereo(s).transmission(t).build();
new Builder().engine(e).transmission(t).stereo(s).stereo(s).build();

ただし、これはビルダーのユーザーにとって理想的ではないシナリオがたくさんあります。特にビルダーにセッターだけでなくアダーもある場合、またはビルダーの特定のプロパティが使用可能になる順序をユーザーが制御できない場合は特にそうです。

そのために私が考え得る唯一の解決策は非常に複雑です:設定された、またはまだ設定されていない必須プロパティのすべての組み合わせについて、到着する前に呼び出す必要がある可能性のある他の必須セッターを認識する専用ビルダークラスを作成しましたbuild()メソッドが利用できる場所を示し、それらのセッターのそれぞれが、build()メソッドを含むより一歩近いより完全なタイプのビルダーを返します。
私は以下のコードを追加しますが、私はあなたが作成することができますFSM作成するために、型システムを使用していると言うかもしれないBuilderいずれかに変換することができ、BuilderWithEngineまたはBuilderWithTransmissionその両方が、その後に変えることができ、CompleteBuilder、その実装build()方法。オプションのセッターは、これらのビルダーインスタンスのいずれでも呼び出すことができます。 ここに画像の説明を入力してください

public interface Car {
    public Engine getEngine(); // required
    public Transmission getTransmission(); // required
    public Stereo getStereo(); // optional

    public class Builder extends OptionalBuilder {
        public BuilderWithEngine engine(Engine engine) {
            return new BuilderWithEngine(engine, stereo);
        }
        public BuilderWithTransmission transmission(Transmission transmission) {
            return new BuilderWithTransmission(transmission, stereo);
        }
        @Override
        public Builder stereo(Stereo stereo) {
            super.stereo(stereo);
            return this;
        }
    }

    public class OptionalBuilder {
        protected Stereo stereo = null;
        private OptionalBuilder() {}
        public OptionalBuilder stereo(Stereo stereo) {
            this.stereo = stereo;
            return this;
        }
    }

    public class BuilderWithEngine extends OptionalBuilder {
        private Engine engine;
        private BuilderWithEngine(Engine engine, Stereo stereo) {
            this.engine = engine;
            this.stereo = stereo;
        }
        public CompleteBuilder transmission(Transmission transmission) {
            return new CompleteBuilder(engine, transmission, stereo);
        }
        public BuilderWithEngine engine(Engine engine) {
            this.engine = engine;
            return this;
        }
        @Override
        public BuilderWithEngine stereo(Stereo stereo) {
            super.stereo(stereo);
            return this;
        }
    }

    public class BuilderWithTransmission extends OptionalBuilder {
        private Transmission transmission;
        private BuilderWithTransmission(Transmission transmission, Stereo stereo) {
            this.transmission = transmission;
            this.stereo = stereo;
        }
        public CompleteBuilder engine(Engine engine) {
            return new CompleteBuilder(engine, transmission, stereo);
        }
        public BuilderWithTransmission transmission(Transmission transmission) {
            this.transmission = transmission;
            return this;
        }
        @Override
        public BuilderWithTransmission stereo(Stereo stereo) {
            super.stereo(stereo);
            return this;
        }
    }

    public class CompleteBuilder extends OptionalBuilder {
        private Engine engine;
        private Transmission transmission;
        private CompleteBuilder(Engine engine, Transmission transmission, Stereo stereo) {
            this.engine = engine;
            this.transmission = transmission;
            this.stereo = stereo;
        }
        public CompleteBuilder engine(Engine engine) {
            this.engine = engine;
            return this;
        }
        public CompleteBuilder transmission(Transmission transmission) {
            this.transmission = transmission;
            return this;
        }
        @Override
        public CompleteBuilder stereo(Stereo stereo) {
            super.stereo(stereo);
            return this;
        }
        public Car build() {
            return new Car() {
                @Override
                public Engine getEngine() {
                    return engine;
                }
                @Override
                public Transmission getTransmission() {
                    return transmission;
                }
                @Override
                public Stereo getStereo() {
                    return stereo;
                }
            };
        }
    }
}

ご存知のように、必要なさまざまなビルダークラスの数はO(2 ^ n)になるため、これは適切にスケーリングされません。ここで、nは必須のセッターの数です。

したがって、私の質問:これをよりエレガントに実行できますか?

(私はJavaで動作する答えを探していますが、Scalaも受け入れられます)


1
IoCコンテナーを使用してこれらの依存関係をすべて立てることができないのはなぜですか?また、なぜかは明確ではありませんが、あなたが主張したように順序が重要ではない場合、通常のsetterメソッドを使用して戻りthisませんか?
Robert Harvey

.engine(e)1つのビルダーに対して2回呼び出すとはどういう意味ですか?
Erik Eidt 16

3
組み合わせごとにクラスを手動で記述せずに静的に検証したい場合は、マクロやテンプレートメタプログラミングなどのネックベアードレベルのものを使用する必要があります。私の知る限り、Javaはそのための十分な表現力がなく、他の言語で動的に検証されたソリューションよりも、その努力はおそらく価値がありません。
カールビーレフェルト

1
ロバート:目標は、型チェッカーにエンジンとトランスミッションの両方が必須であるという事実を強制することです。これにより、以前に電話をかけたbuild()ことがない場合でも、電話をかけることはできません。engine(e)transmission(t)
derabbink 2016

エリック:デフォルトのEngine実装から始めて、後でより具体的な実装で上書きすることもできます。しかしengine(e)、セッターではなくアダーである場合、これはおそらくより意味のあることですaddEngine(e)。これはCar、複数のエンジン/モーターを備えたハイブリッド車を生産できるビルダーに役立ちます。これは不自然な例であるため、簡潔にするために、なぜこれを行う必要があるのか​​については詳しく説明しませんでした。
derabbink

回答:


3

指定したメソッド呼び出しに基づいて、2つの異なる要件があるようです。

  1. エンジンは1つ(必須)、トランスミッションは1つ(必須)、ステレオは1つ(オプション)のみです。
  2. 1つ以上の(必須)エンジン、1つ以上の(必須)トランスミッション、および1つ以上の(オプションの)ステレオ。

ここでの最初の問題は、クラスで何をしたいのか分からないことだと思います。その一部は、構築されたオブジェクトをどのように見せたいかがわからないことです。

自動車は1つのエンジンと1つのトランスミッションのみを持つことができます。ハイブリッド車でもエンジンは1つしかありません(おそらくGasAndElectricEngine

両方の実装について説明します。

public class CarBuilder {

    public CarBuilder(Engine engine, Transmission transmission) {
        // ...
    }

    public CarBuilder setStereo(Stereo stereo) {
        // ...
        return this;
    }
}

そして

public class CarBuilder {

    public CarBuilder(List<Engine> engines, List<Transmission> transmission) {
        // ...
    }

    public CarBuilder addStereo(Stereo stereo) {
        // ...
        return this;
    }
}

エンジンとトランスミッションが必要な場合、それらはコンストラクター内にある必要があります。

必要なエンジンまたはトランスミッションがわからない場合は、まだ設定しないでください。それは、あなたがビルダーをスタックから離れすぎて作成しているという兆候です。


2

nullオブジェクトパターンを使用しないのはなぜですか?このビルダーを取り除く、あなたが書くことができる最もエレガントなコードは、あなたが実際に書く必要がないものです。

public final class CarImpl implements Car {
    private final Engine engine;
    private final Transmission transmission;
    private final Stereo stereo;

    public CarImpl(Engine engine, Transmission transmission) {
        this(engine, transmission, new DefaultStereo());
    }

    public CarImpl(Engine engine, Transmission transmission, Stereo stereo) {
        this.engine = engine;
        this.transmission = transmission;
        this.stereo = stereo;
    }

    //...

}

これはすぐに思いました。3つのパラメーターコンストラクターはありませんが、必須の要素を持つ2つのパラメーターコンストラクターと、オプションのステレオのセッターのみです。
Encaitar

1
のような単純な(考案された)例ではCar、c'tor引数の数が非常に少ないため、これは理にかなっています。ただし、適度に複雑なもの(> = 4の必須の引数)を処理するとすぐに、処理が難しくなり、読みにくくなります(「エンジンまたはトランスミッションが最初に来ましたか?」)。そのため、ビルダーを使用することになります。APIを使用すると、何を構築しているかをより明確にする必要があります。
derabbink

1
@derabbinkこの場合、クラスを小さいクラスに分割しないでください。ビルダーを使用すると、クラスが実行しすぎてメンテナンスできなくなったという事実を隠すだけです。
発見

1
パターンの狂気を終わらせるための称賛。
ロバートハーベイ

@Spottedいくつかのクラスは、大量のデータを含むだけです。たとえば、HTTPリクエストに関するすべての関連情報を保持し、データをCSVまたはJSON形式で出力するアクセスログクラスを作成する場合などです。大量のデータが存在するため、コンパイル時に一部のフィールドを強制的に表示したい場合は、非常に長い引数リストコンストラクターを使用したビルダーパターンが必要ですが、見栄えがよくありません。
ssgao

1

まず、私が働いたどのショップよりも多くの時間がない限り、操作の順序を許可したり、複数のラジオを指定できるという事実だけで生活する価値はないでしょう。ユーザー入力ではなくコードについて話していることに注意してください。そのため、コンパイル時に1秒前ではなく、ユニットテスト中にアサーションが失敗する可能性があります。

ただし、コメントに記載されているように、エンジンとトランスミッションが必要な制約がある場合は、必須のプロパティをすべてビルダのコンストラクタに配置することでこれを強制します。

new Builder(e, t).build();                      // ok
new Builder(e, t).stereo(s).build();            // ok
new Builder(e, t).stereo(s).stereo(s).build();  // exception on second call to stereo as stereo is already set 

オプションのステレオのみである場合、ビルダーのサブクラスを使用して最終ステップを実行することは可能ですが、それを超えて、テストではなくコンパイル時にエラーを取得することによる利益は、おそらく努力に値しません。


0

必要なさまざまなビルダークラスの数はO(2 ^ n)になります。nは必須のセッターの数です。

あなたはすでにこの質問の正しい方向を推測しました。

コンパイル時のチェックを取得する場合は、(2^n)型が必要になります。ランタイムチェックを取得する場合は、(2^n)状態を格納できる変数が必要です。nビット整数が行います。


C ++は非型テンプレートパラメータ(整数値など)をサポートしているため、C ++クラステンプレートはO(2^n)これに類似したスキームを使用して、異なる型にインスタンス化することが可能です。

ただし、非型テンプレートパラメーターをサポートしていない言語では、型システムに依存してO(2^n)異なる型をインスタンス化することはできません。


次の機会は、Javaアノテーション(およびC#属性)です。これらの追加のメタデータを使用すると、注釈プロセッサが使用されている場合に、コンパイル時にユーザー定義の動作をトリガーできます。ただし、これらを実装するのは大変な作業です。この機能を提供するフレームワークを使用している場合は、それを使用してください。それ以外の場合は、次の機会を確認してください。


最後に、O(2^n)実行時にさまざまな状態を変数として(文字通り、少なくともnビット幅の整数として)格納するのは非常に簡単です。潜在的な利益と比較した場合、コンパイル時のチェックを実装するために必要な労力が多すぎるため、最も支持されている回答はすべて、実行時にこのチェックを実行することをお勧めします。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.