web-dev-qa-db-ja.com

コンストラクターの繰り返しを少なくできますか?

10種類のコンストラクタを持つクラス を拡張しています。新しいサブクラスSpecialImageは次のように使用されます。

SpecialImage specialImage = new SpecialImage(..);

// Leverage the Rotate() method of superclass Image, which
// returns a new rotated image.
// Notice the type is still SpecialImage.
SpecialImage rotated = specialImage.Rotate(...);

SpecialImageは5つのコンストラクターを実装し、それぞれが同じ追加パラメーターThing stuffおよびint rangeを取ります。

public class SpecialImage<TDepth> : Image<Gray, TDepth>
    where TDepth : new()
{

    Thing _stuff;
    int _range;

    public SpecialImage(Thing stuff, int range) : base()
    {
        InitParams(stuff, range)
    }

    public SpecialImage(Thing stuff, int range, Size size) : base(size)
    {
        InitParams(stuff, range)
    }

    public SpecialImage(Thing stuff, int range, int width, int height) : base(width, height)
    {
        InitParams(stuff, range)
    }

    public SpecialImage(Thing stuff, int range, string filename) : base(filename)
    {
        InitParams(stuff, range)
    }

    public SpecialImage(Thing stuff, int range, TDepth[,,] data) : base(data)
    {
        InitParams(stuff, range)
    }

    private void InitParams(Thing stuff, int range)
    {
        _stuff = stuff;
        _range = range;
    }
}

これは少し繰り返しですが、これは本当にメンテナンスに直面した場合の問題です。 SpecialImageの構築要件が変わるたびに、5つのコンストラクターを変更します。

feel繰り返しを少なくする必要があるように思えますが、このタイプの冗長性を削除する方法がわかりません。 これをより冗長にすることは可能ですか?

7
kdbanman

Builderパターン を使用できます。

Imageには10個のコンストラクターはありません。可能なパラメーターをすべて取るコンストラクターは1つだけです。ただし、実際のコードからこのコンストラクターを呼び出さないでください。別のビルダークラスから呼び出します。

Builderは、デフォルト値で初期化される別個のクラスであり、これらの値を変更するいくつかのセッターと、現在の値で新しいオブジェクトを作成するbuild()メソッドを提供します。

新しい画像を作成するためのコードは次のようになります。

ImageBuilder builder = new ImageBuilder();
builder.setStuff(stuff);
builder.setRange(range);
builder.setSize(size); // alternative overload: builder.setSize(width, height);
builder.setFilename(filename);
Image image = builder.build();

SpecialImageBuilderImageBuilderを継承します。 ImageSpecialImageが同じプロパティを持っている場合、オーバーライドする必要がある唯一のメソッドはbuildです。

ところで、ビルダーでできるエレガントなトリックは、各セッターがthisを返すようにすることです。これは「滑らかなインターフェース」と呼ばれ、次のようなコードを記述できます。

Image image = new ImageBuilder()
                  .setStuff(stuff)
                  .setRange(range)
                  .setSize(size)
                  .setFilename(filename)
                  .build();
7
Philipp

いいえ、基本クラスを変更できない、または1つのコンストラクターが他のコンストラクターにチェーンしていることがわかっていると仮定します。別のコンストラクターから複数の基本コンストラクターの1つを選択する方法はありません。あなたはジェネリックでそれを行うことができるかもしれないと思ったが、どうやらそうではない。

ファクトリーまたはビルダークラスを使用している場合でも、createメソッドを呼び出して、そのメソッド内で使用するコンストラクターを切り替えることができますが、当面の問題は解決しません。あなたはまだあなたのSpecialImageクラスに複数のコンストラクタを必要とするので

1つの解決策は、基本クラスを継承するのではなく、ラップすることです。これは、基本クラスがインターフェースを実装することを前提としていますが、インターフェースを追加することもできます。

using System;

namespace LessConstructors
{
    public class Size
    { }
    public class Thing
    { }
    public class Gray
    { }

    public interface IImage
    {
        object MethodX();
    }

    public class Image<TColour, TDepth> : IImage
    {
        public Image() { }

        public Image(Size size) { }

        public Image(int width, int height) { }

        public Image(string filename) { }

        public Image(TDepth[,,] data) { }

        public object MethodX()
        {
            throw new NotImplementedException();
        }
    }

    public interface IConstructor<T1, T2>
    {
        Image<T1, T2> Create();
    }
    public class SizeConstructor<T1, T2> : IConstructor<T1, T2>
    {
        public Size size { get; set; }

        public Image<T1, T2> Create()
        {
            return new Image<T1, T2>(this.size);
        }
    }

    public class HeightAndWidthConstructor<T1, T2> : IConstructor<T1, T2>
    {
        public int Height { get; set; }
        public int Width { get; set; }

        public Image<T1, T2> Create()
        {
            return new Image<T1, T2>(this.Height, this.Width);
        }
    }

    public class SpecialImage<TDepth> : IImage
        where TDepth : new()
    {
        private Image<Gray, TDepth> image;
        Thing _stuff;
        int _range;

        public SpecialImage(Thing stuff, int range, IConstructor<Gray, TDepth> constructor)
        {
            _stuff = stuff;
            _range = range;
            image = constructor.Create();
        }

        public object MethodX()
        {
            return this.image.MethodX();
        }
    }
}
2
Ewan

Lxrecが提案するように構成を使用し、実際にImageインスタンスが必要なときにデリゲートを取得できます。以前にSpecialImageを使用していた場所でImageのみを使用したい場合、これは問題になる可能性があります。

それ以外は、本当により良い解決策を見ることができません。私が持っている他の唯一の提案は、コンストラクタの代わりに名前付きファクトリメソッドを使用することですが、それは単に読みやすくするためです。

1
moofins

主な考慮事項は、クラスのユーザーの利便性です。これは、APIの使いやすさの一部です。オプションが多すぎたり少なすぎたりすると、使いやすさが低下します。特に、オプションが多すぎると、ユーザーが混乱し、選択に多くの時間を費やすようになります。したがって、各コンストラクターオーバーロードが実際に使用されるかどうかを慎重に検討する必要があります。 (APIの内部または外部ユーザーに)公開する前に、未使用のものを積極的に削除します。

たとえば、Sizeを受け取るコンストラクターを提供する場合、int width, int heightを受け取る別のコンストラクターを提供する必要はありません。

string filenameをとるコンストラクターも少し不誠実です。画像ファイルの色空間は、ファイルの内容によって決まります。 JPEGファイルは、グレー、YCbCr、YCCKのいずれかです。 (YCbCrは通常、ロード時にRGBに自動変換されますが、変換によって発生する量子化エラーなしにファイルのYCbCr値が必要になる場合があります。)したがって、コンストラクターは、画像形式を調べずに色空間を決定するべきではありません。最初。

Imageを初期化しないコンストラクタに私は深く困惑しています。クラス設計の残りの部分は、そのImageを初期化する必要があることを示唆しているようです。 Null Object にするつもりですか?それがあなたの意図しないのであれば、そのコンストラクタは存在すべきではありませんでした。

最後に少し注意してください。既存のライブラリの上に「シングルチャネルハイダイナミックレンジ」画像処理レイヤーを設計しているようです。 (「灰色」のようなシングルチャネル、32ビット整数、32ビットフロート、64ビットフロートをサポートするためのハイダイナミックレンジ。)一部のイメージ操作がHDRをサポートしていないことに驚くかもしれません。深度-言い換えれば、一部の画像操作はTDepth = Byteのみに制限されています。 HDR深度で操作を実行しようとすると、実行時に例外がスローされる場合があります。

基礎となるライブラリーには多くの誤りを見つけることができますが、画像ライブラリーのAPIを設計することは非常に難しいことを認めなければなりません。頑張って作品をシェアしてくださった皆さんに拍手を送ります。

1
rwong