web-dev-qa-db-ja.com

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

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

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

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

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

これには、Step Builderが必要です。通常、次のようなものを実装します。

_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)のチェーンがあり、必要なセッターメソッドを次々に追加し、最後のクラスにはすべてのオプションのセッターメソッドも含まれています。
これは、このステップビルダーのユーザーが作成者が必須のセッターを使用可能にした順序に制限されることを意味します。以下は可能な使用例です(すべてが厳密に順序付けられていることに注意してください:最初に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()メソッドを含める方法。
以下のコードを追加しましたが、型システムを使用して、BuilderまたはBuilderWithEngineまたはBuilderWithTransmissionに変換できるFSMを作成できるFSMを作成していると言うかもしれません。 build()メソッドを実装するCompleteBuilderに変換されます。オプションのセッターは、これらのビルダーインスタンスのいずれでも呼び出すことができます。 enter image description here

_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は必須のセッターの数です。

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

(Scalaでもかまいませんが、Javaで動作する答えを探しています)

10
derabbink

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

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

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

自動車は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;
    }
}

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

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

3
Zymus

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

    //...

}
2
Spotted

まず、私が働いたどの店よりも多くの時間がない限り、操作の順序を許可したり、複数のラジオを指定できるという事実だけで生活したりする価値はないでしょう。ユーザー入力ではなくコードについて話していることに注意してください。そのため、ユニットテスト中に、コンパイル時の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 

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

1
Pete Kirkham

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

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

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


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

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


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


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

0
rwong