Builderパターンとの互換性のない構成をどのように処理する必要がありますか?


9

これは、別の質問に対するこの回答が動機です。

ビルダーパターン)、特に、オプションの初期化パラメータとの複合体の初期化を簡略化するために使用されます。しかし、相互に排他的な構成を適切に管理する方法がわかりません。

ここにImageクラスがあります。 Imageファイルまたはサイズから初期化できますが、両方から初期化することはできません。コンストラクタを使用してこの相互排除を強制することは、クラスが十分に単純な場合に明らかです。

public class Image
{
    public Image(Size size, Thing stuff, int range)
    {
    // ... initialize empty with size
    }

    public Image(string filename, Thing stuff, int range)
    {
        // ... initialize from file
    }
}

Imageビルダーパターンが役立つように実際に十分に構成可能であると想定すると、突然これが可能になる可能性があります。

Image image = new ImageBuilder()
                  .setStuff(stuff)
                  .setRange(range)
                  .setSize(size)           // <----------  NOT
                  .setFilename(filename)   // <----------  COMPATIBLE
                  .build();

これらの問題は、コンパイル時ではなく実行時に検出する必要がありますが、これは最悪の事態ではありません。問題は、ImageBuilderクラス内でこれらの問題を一貫して包括的に検出すると、特にメンテナンスに関して複雑になる可能性があることです。

ビルダーパターンで互換性のない構成に対処するにはどうすればよいですか?

回答:


12

あなたはあなたのビルダーを持っています。ただし、この時点でいくつかのインターフェースが必要です。

メソッドの1つのサブセット(ではないsetSize)を定義するFileBuilderインターフェイスと、メソッドの別のサブセット(ではないsetFilename)を定義するSizeBuilderインターフェイスがあります。GenericBuilderインターフェースでFileBuilderとSizeBuilderを拡張することもできますが、その必要がない場合もあります。

このメソッドsetSize()は、SizeBuilderを返します。このメソッドsetFilename()はFileBuilderを返します。

ImageBuilderには、との両方のすべてのロジックがsetSize()ありsetFileName()ます。ただし、これらの戻り型は、適切なサブセットインターフェイスを指定します。

class ImageBulder implements FileBuilder, SizeBuilder {
    ImageBuilder() {
        doInitThings;
    }

    ImageBuilder setStuff(Thing) {
        doStuff;
        return this;
    }

    ImageBuilder setRange(int range) {
        rangeStuff;
        return this;
    }

    SizeBuilder setSize(Size size) {
        stuff;
        return this;
    }

    FileBuilder setFilename(String filename) {
        otherStuff;
        return this;
    }

    Image build() {
        return new Image(...);
    }
}

ここでの特別なビットは、SizeBuilderを取得すると、すべての戻り値がSizeBuildersである必要があるということです。そのためのインターフェースは次のようになります。

interface SizeBuilder {
    SizeBuilder setRange(int range);
    SizeBuilder setSize(Size size);
    SizeBuilder setStuff(Thing stuff);
    Image build();
}

interface FileBuilder {
    FileBuilder setRange(int range);
    FileBuilder setFilename(String filename);
    FileBuilder setStuff(Thing stuff);
    Image build();
}

したがって、これらのメソッドの1つを呼び出すと、もう1つを呼び出して無効な状態のオブジェクトを作成することができなくなります。


本当に面白い、ありがとう。私はこれらがどのように使用されるかについて少し混乱しています。具体的には、宣言と初期化の種類がどうなるかわかりません。必要以上に複雑なことを想像しているだけかもしれません。使用例を教えてください。
kdbanman 2015

画像ビルダーは、そのメソッドが呼び出す状態変化に対応するインターフェースを返します。ただし、ImageBuilderから特定のインターフェイスを取得すると、そのオブジェクトに対する以降の呼び出しはそのインターフェイスで行われ、互換性のないメソッドを呼び出す機能が制限されます。

1
@rwong私はそれをあまり深く調べないことを認めますが、そのようなアプローチで私が持っていたと私が思った問題は、ビルダーの「状態」がリセットされる可能性があるということでした。setSize()が呼び出されたら、それ以降のすべてのビルダー呼び出しがSizeBuilder上で行われるようにする必要があります。setRange()のタイプがSizeBuilderまたは拡張/実装されいない場合、setFilenameを再度呼び出すことで回避できます。また、サイズの代わりにintの幅とintの高さの両方を呼び出す必要があるような状況(ここでは説明しません)あります。

1
@MichaelT迂回の複雑な問題を考えると、ビルダーのパターンを使用する場合は、パラメーターの初期化の厳密な順序を強制すること(パラメーター項目のプレフィックスツリーを生成すること)が良いことかもしれないと思います。その結果、Rangeおよびなどの共通パラメーター項目Stuffは、任意の時点ではなく、最初に初期化する必要があります。
rwong 2015

1
@MichaelT:その時点で、LSPが登場します。見かけの型(RangeAndStuffBuilder)のメソッドが実際の型で呼び出せることを確認できます。一部のメソッドに対してより多くの基本型を返すことにより、さらに制限を実装できます(これにより、型が指数的に増加します)、操作を効果的に削除します。メソッドの結果が下位階層に戻らない限り、型エラーは発生しません。setHeight/ setWidthシナリオはありません兄弟階層で実現することができたbuild方法を。
outis 2015
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.