web-dev-qa-db-ja.com

複数の複雑なコンストラクタを持つオブジェクトのファクトリメソッド

レガシーコードベースをリファクタリングしています。
ポリモーフィックになるための良いターゲットになると判断した非常に類似した4つのオブジェクトがあるので、すべての一般的なコードを基本クラスに移動し、インターフェイスを追加しました。

これらのオブジェクトを作成しようとすると、実際の問題が発生します。すべてのクラスには、それぞれ5〜10個のパラメーターを持つ3つの異なるコンストラクターがあり、コンテキストに基づいてさまざまなケースで使用されます。
これらのクラスの処理が多すぎることはすでにわかっており、実際には、それぞれが1つのケースを処理するさまざまな小さなクラスに分割する方が適切ですが、これはレガシーコードベースであり、あまり多くの変更を行うことはできません既存のコードに。

現時点では、私のコードは次のようなものです。

public class HandlerFactory
{
    public static IHandler CreateInstance(HandlerType param1, string param2, ..., string param8)
    {
        switch(param1)
        {
            case HandlerType.Type1
                return new Handler1(param2, ..., param8)
            case HandlerType.Type2
                return new Handler2(param2, ..., param8)
            case HandlerType.Type3
                return new Handler3(param2, ..., param8)
            case HandlerType.Type4
                return new Handler4(param2, ..., param8)
        }
    }

    public static IHandler CreateInstance(HandlerType param1, int param2, ..., DateTime param10)
    {
        switch(param1)
        {
            case HandlerType.Type1
                return new Handler1(param2, ..., param10)
            case HandlerType.Type2
                return new Handler2(param2, ..., param10)
            case HandlerType.Type3
                return new Handler3(param2, ..., param10)
            case HandlerType.Type4
                return new Handler4(param2, ..., param10)
        }
    }

    public static IHandler CreateInstance(HandlerType param1, string param2, ..., CustomClass param5)
    {
        switch(param1)
        {
            case HandlerType.Type1
                return new Handler1(param2, ..., param5)
            case HandlerType.Type2
                return new Handler2(param2, ..., param5)
            case HandlerType.Type3
                return new Handler3(param2, ..., param5)
            case HandlerType.Type4
                return new Handler4(param2, ..., param5)
        }
    }
}

ただし、別のハンドラーを追加する場合、3つのファクトリーメソッドを編集する必要があるため、私は3つのファクトリーメソッドを使用するのが嫌いです(このリファクタリングを行う主な理由)。

1つのスイッチで1つのファクトリメソッドのみを保持できる方法はありますか?

3
BgrWorker

@JohnWuが提供する答えは非常にエレガントで、とても気に入っています。代替案を提供させてください。

Abstract Factory Pattern を使用できます。基本的に1つの大きな工場があり、コンクリートハンドラー用のコンクリート工場を生産します。次に、別のハンドラーが必要な場合は、その特定のハンドラー用に1つのファクトリーを作成するだけです。次のようなものになります。

public interface IHandler
{
}

public class Handler1 : IHandler
{
    public Handler1(string param1, string param2)
    {
    }

    public Handler1(int param1, int param2)
    {
    }
}

public class Handler2 : IHandler
{
    public Handler2(string param1, string param2)
    {
    }

    public Handler2(int param1, int param2)
    {
    }
}

簡単にするために、2つの異なるコンストラクターを持つ2つのHandlerクラスしかありません。ソリューションはまだあなたのケースに適用できると思います。次に、工場があります。

   public interface IHandlerFactory
    {
        IHandler Create(string param1, string param2);
        IHandler Create(int param1, int param2);
    }

    public class Handler1Factory : IHandlerFactory
    {
        public IHandler Create(int param1, int param2)
        {
            return new Handler1(param1, param2);
        }

        public IHandler Create(string param1, string param2)
        {
            return new Handler1(param1, param2);
        }
    }

    public class Handler2Factory : IHandlerFactory
    {
        public IHandler Create(int param1, int param2)
        {
            return new Handler2(param1, param2);
        }

        public IHandler Create(string param1, string param2)
        {
            return new Handler2(param1, param2);
        }
    }

    public class HandlerFactory
    {
        public static IHandler Create(IHandlerFactory factory, string param1, string param2)
        {
            return factory.Create(param1, param2);
        }

        public static IHandler Create(IHandlerFactory factory, int param1, int param2)
        {
            return factory.Create(param1, param2);
        }
    }

ここでの代替策は、HandlerFactoryタイプのプロパティを持つIHandlerFactoryクラスを使用することであり、メソッドは静的ではなく、IHandlerFactoryパラメータを持たないことになります。明らかに、HandlerFactoryクラスのオブジェクトは、ある時点でインスタンス化する必要があります。

使い方は次のようになります:

public class Example
{
    public void ExampleMethod()
    {
        Handler1Factory factory1 = new Handler1Factory();
        var handler1 = HandlerFactory.Create(factory1, "parameter1", "parameter2");

        Handler2Factory factory2 = new Handler2Factory();
        var handler2 = HandlerFactory.Create(factory2, 1, 2);
    }
}

このソリューションでは、コードを目的別に明確に分けています。最も重要なのは、別のハンドラーが必要になった場合に、既存のコードを変更する必要がないことです。必要なことは、その特定のハンドラーのファクトリーを作成することだけです。

すべてのハンドラーが同じパラメーターリストを持つコンストラクターを持っているため、これが可能であることに注意してください。そうでなければ、デザインは多少異なります。

2
Vladimir Stokic

このソリューションは、ケース/スイッチを排除し、一般的な引数に置き換えます。通常、コンストラクター引数を許可しないnew制約を使用しない限り、この方法でオブジェクトを作成することはできません。私は この回避策 を別の答えから盗みました。

また、ここにはファクトリメソッドが1つだけあり、コンストラクタ引数は params キーワードを使用して可変サイズのリストとして受け入れられます。

public class HandlerFactory
{
    public static T CreateInstance<T>(params object[] constructorArguments) where T : class, IHandler
    {
        return (T)Activator.CreateInstance(typeof(T), constructorArguments);
    }
}

ご覧のとおり、このメソッドははるかに短く、ケース/スイッチがなく、プロトタイプが1つだけ必要です。そして、素敵なのはHandler4またはその他のサブクラスの場合、ファクトリを操作する必要はありません。ただHandler4IHandlerを実装しており、これで準備は完了です。

次のように呼び出すことができます。

var o = HandlerFactory.CreateInstance<Handler3>(param1, param2);

戻り値の型はTなので、ovarとして宣言するか、コンパイラーが具象型を推測するかを選択できます。

IHandler o = HandlerFactory.CreateInstance<Handler3>(param1, param2);

インターフェースのみで作業したい場合。

8
John Wu

おっしゃったように、既存のコードにはそのまま設計上の問題があります。まず、非常に多くの(異なる!)パラメーターを持つメソッドとコンストラクターがあると、デザインのにおいがします。

複雑さの根本的な原因を修正しない場合は、抽象化の別のレイヤーを追加することで、人生を楽にすることができます。これは、コードをリファクタリングする(または新しいコードに移動する)までの時間を確保するまで、コードベースの維持を容易にします。

  1. 渡されたパラメーター(Param1、Param2、Param3)のクラスを作成します。
  2. [Hack]3つのクラスに共通のインターフェース(IParameterなど)を追加します。
    • インターフェースはあまり一般的ではないのでハックですが、ファクトリーにスイッチを1つだけ置きたい場合は、それが解決策です(私が見つけたものです)。
  3. IParameterインターフェイスのファクトリを作成します。ファクトリー自体は、設定されたパラメーターに基づいて、何をビルドするかを決定します。
  4. [Hack]既存のクラスのコンストラクターで、IParameterオブジェクトの具象クラス型に基づいてswitchステートメントを追加します。
    • これはインターフェースの使用目的ではないため、ハックです。インターフェイスは、一般的な動作の抽象化を提供する必要があります。

DonNet Fiddle使用例: https://dotnetfiddle.net/NoChOk

閲覧のためのコード:

using System;

public class Program
{
    public void Main()
    {
        IHandler first = HandlerFactory.CreateInstance(
            HandlerType.Handler1,
            ParamBuilder.start().withOne("x").withTwo("y").build());
        IHandler second = HandlerFactory.CreateInstance(
            HandlerType.Handler2,
            ParamBuilder.start().withAlpha(1).withBeta(new DateTime()).build());
        IHandler third = HandlerFactory.CreateInstance(
            HandlerType.Handler3,
            ParamBuilder.start().withMickey(":)").withMouse(1L).build());

        first.DoStuff();
        second.DoStuff();
        third.DoStuff();
    }

    class HandlerFactory
    {
        public static IHandler CreateInstance(HandlerType type, IParam param)
        {
            switch (type) {
            case HandlerType.Handler1: return new Handler1(param);
            case HandlerType.Handler2: return new Handler2(param);
            case HandlerType.Handler3: return new Handler3(param);
            default: throw new NotSupportedException();
            }
        }
    }

    enum HandlerType { Handler1, Handler2, Handler3 
    }

    interface IHandler { void DoStuff(); }

    class Handler1 : IHandler {
        private Param1 numbers; private Param2 letters; private Param3 disney;

        public Handler1(IParam param) {
            if (param is Param1) numbers = (Param1) param;
            if (param is Param2) letters = (Param2) param;
            if (param is Param3) disney = (Param3) param;
        }

        public void DoStuff() { Console.WriteLine("I am alive - Handler 1");}
    }

    class Handler2 : IHandler {
        private Param1 numbers; private Param2 letters; private Param3 disney;

        public Handler2(IParam param) {
            if (param is Param1) numbers = (Param1) param;
            if (param is Param2) letters = (Param2) param;
            if (param is Param3) disney = (Param3) param;
        }

        public void DoStuff() { Console.WriteLine("I am alive - Handler 2");}
    }

    class Handler3 : IHandler {
        private Param1 numbers;
        private Param2 letters;
        private Param3 disney;

        public Handler3(IParam param) {
            if (param is Param1) numbers = (Param1) param;
            if (param is Param2) letters = (Param2) param;
            if (param is Param3) disney = (Param3) param;
        }

        public void DoStuff() { Console.WriteLine("I am alive - Handler 3");}
    }

    interface IParam {
        string One { get; } string Two { get; } // first param class

        int Alpha { get; } DateTime Beta { get; } // second param class

        string Mickey { get; } long Mouse { get; } // third param class
    }

    class ParamBuilder {
        private string one, two;
        private int? alpha; private DateTime? beta;
        private  string mickey; private long? mouse;

        public static ParamBuilder start() { return new ParamBuilder(); }

        public IParam build() {
            if (!string.IsNullOrEmpty(one) && !string.IsNullOrEmpty(one)) {
                return new Param1(one, two);
            }
            if (alpha.HasValue && beta.HasValue) {
                return new Param2(alpha.Value, beta.Value);
            }
            if (!string.IsNullOrEmpty(mickey) && mouse.HasValue) {
                return new Param3(mickey, mouse.Value);
            }

            throw new NotSupportedException();
        }

        public ParamBuilder withOne(string one) { this.one = one; return this; }
        public ParamBuilder withTwo(string two) { this.two = two; return this; }
        public ParamBuilder withAlpha(int alpha) { this.alpha = alpha; return this; }
        public ParamBuilder withBeta(DateTime beta) { this.beta = beta; return this; }
        public ParamBuilder withMickey(string m) { this.mickey = m; return this; }
        public ParamBuilder withMouse(long m) { this.mouse = m; return this; }
    }

    class Param1 : IParam {
        public string One { private set; get; }
        public string Two { private set; get; }

        public int Alpha { get {throw new NotImplementedException();} }
        public DateTime Beta { get {throw new NotImplementedException();} }
        public string Mickey { get {throw new NotImplementedException();} }
        public long Mouse { get {throw new NotImplementedException();} }

        public Param1(string one, string two) {
            One = one; Two = two;
        }
    }

    class Param2 : IParam {
        public int Alpha { private set; get; }
        public DateTime Beta { private set; get; }

        public string One { get {throw new NotImplementedException();} }
        public string Two { get {throw new NotImplementedException();} }
        public string Mickey { get {throw new NotImplementedException();} }
        public long Mouse { get {throw new NotImplementedException();} }

        public Param2(int alpha, DateTime beta) {
            Alpha = alpha; Beta = beta;
        }
    }

    class Param3 : IParam {
        public string Mickey { private set; get; }
        public long Mouse { private set; get; }

        public string One { get {throw new NotImplementedException();} }
        public string Two { get {throw new NotImplementedException();} }
        public int Alpha { get {throw new NotImplementedException();} }
        public DateTime Beta { get {throw new NotImplementedException();} }

        public Param3(string mi, long mo) {
            Mickey = mi; Mouse = mo;
        }
    }
}
0
Andrei Epure