異なる入力パラメーターを持つワーカーのC#デザインパターン


14

どのデザインパターンがこの問題の解決に役立つかはわかりません。

使用するWorkerクラスを決定する「コーディネーター」クラスがあります-存在するすべての種類のWorkerについて知る必要はありませんが、WorkerFactoryを呼び出し、共通のIWorkerインターフェイスに基づいて動作します。

次に、適切なWorkerを動作するように設定し、 'DoWork'メソッドの結果を返します。

これまでは順調でした...今までは。新しいワーカークラス「WorkerB」の新しい要件があります。これは、作業を行うために追加の情報、つまり追加の入力パラメーターを必要とします。

余分な入力パラメーターを使用してオーバーロードされたDoWorkメソッドが必要なようですが、既存のすべてのワーカーはそのメソッドを実装する必要があります。これらのワーカーは本当にそのメソッドを必要としないので間違っているようです。

これをリファクタリングして、どのワーカーが使用されているかをコーディネーターに認識させずに、各ワーカーがジョブを実行するために必要な情報を取得できるようにしますが、不要な作業はワーカーに行わせませんか?

既に多くの既存のワーカーがいます。

新しいWorkerBクラスの要件に対応するために、既存の具象ワーカーを変更する必要はありません。

ここではデコレータパターンが良いと思うかもしれませんが、デコレータが同じメソッドでオブジェクトを異なるパラメータで装飾するのを見たことはありません...

コードの状況:

public class Coordinator
{
    public string GetWorkerResult(string workerName, int a, List<int> b, string c)
    {
        var workerFactor = new WorkerFactory();
        var worker = workerFactor.GetWorker(workerName);

        if(worker!=null)
            return worker.DoWork(a, b);
        else
            return string.Empty;
    }
}

public class WorkerFactory
{
    public IWorker GetWorker(string workerName)
    {
        switch (workerName)
        {
            case "WorkerA":
                return new ConcreteWorkerA();
            case "WorkerB":
                return new ConcreteWorkerB();
            default:
                return null;
        }
    }
}

public interface IWorker
{
    string DoWork(int a, List<int> b);
}

public class ConcreteWorkerA : IWorker
{
    public string DoWork(int a, List<int> b)
    {
        // does the required work
        return "some A worker result";
    }
}

public class ConcreteWorkerB : IWorker
{
    public string DoWork(int a, List<int> b, string c)
    {
        // does some different work based on the value of 'c'
        return "some B worker result";
    }

    public string DoWork(int a, List<int> b)
    {
        // this method isn't really relevant to WorkerB as it is missing variable 'c'
        return "some B worker result";
    }    
}

されたIWorkerインタフェースは、古いバージョンを記載されている、またはことは、追加パラメータを使用して新しいバージョンですか?
-JamesFaix

現在2つのパラメーターを持つIWorkerを使用しているコードベースの場所は、3番目のパラメーターをプラグインする必要がありますか、それとも新しい呼び出しサイトのみが3番目のパラメーターを使用しますか?
-JamesFaix

2
パターンを買おうとする代わりに、パターンが適用されるかどうかに関係なく、デザイン全体に焦点を当ててみてください。推奨読書:「Shopping for Patterns」タイプの質問はどれほど悪いですか?

1
コードによると、IWorkerインスタンスが作成される前に必要なすべてのパラメーターは既にわかっています。したがって、DoWorkメソッドではなく、コンストラクタにこれらの引数を渡す必要があります。IOW、ファクトリクラスを利用します。インスタンスの構築の詳細を隠すことが、ファクトリクラスが存在する主な理由です。そのアプローチを取った場合、ソリューションは簡単です。また、あなたがそれを達成しようとしている方法で達成しようとしていることは、悪いオブジェクト指向です。Liskov Substitution Principleに違反しています。
ダンク

1
別のレベルに戻らなければならないと思います。Coordinatorその追加のパラメーターをそのGetWorkerResult機能に対応させるために、すでに変更する必要がありました-つまり、SOLIDのOpen-Closed-Principleに違反しています。その結果、すべてのコード呼び出しCoordinator.GetWorkerResultも変更する必要がありました。その関数を呼び出す場所を見てください:どのIWorkerを要求するかをどのように決定しますか?それはより良い解決策につながる可能性があります。
ベルンハルトヒラー

回答:


9

引数を一般化して、ベースインターフェイスと可変数のフィールドまたはプロパティを持つ単一のパラメーターに収まるようにする必要があります。このような並べ替え:

public interface IArgs
{
    //Can be empty
}

public interface IWorker
{
    string DoWork(IArgs args);
}

public class ConcreteArgsA : IArgs
{
    public int a;
    public List<int> b;
}

public class ConcreteArgsB : IArgs
{
    public int a;
    public List<int> b;
    public string c;
}

public class ConcreteWorkerA : IWorker
{
    public string DoWork(IArgs args)
    {
        var ConcreteArgs = args as ConcreteArgsA;
        if (args == null) throw new ArgumentException();
        return "some A worker result";
    }
}

public class ConcreteWorkerB : IWorker
{
    public string DoWork(IArgs args)
    {
        var ConcreteArgs = args as ConcreteArgsB;
        if (args == null) throw new ArgumentException();
        return "some B worker result";
    }
} 

nullチェック...

引数の可能なすべての組み合わせに対して具体的なオブジェクトを作成したくない場合は、代わりにタプルを使用できます(最初の選択肢ではありません)。

public string GetWorkerResult(string workerName, object args)
{
    var workerFactor = new WorkerFactory();
    var worker = workerFactor.GetWorker(workerName);

    if(worker!=null)
        return worker.DoWork(args);
    else
        return string.Empty;
}

//Sample call
var args = new Tuple<int, List<int>, string>(1234, 
                                             new List<int>(){1,2}, 
                                             "A string");    
GetWorkerResult("MyWorkerName", args);

1
これは、Windowsフォームアプリケーションがイベントを処理する方法に似ています。1つの「args」パラメーター、および1つの「イベントのソース」パラメーター。すべての「引数」は、EventArgsのサブクラスです:msdn.microsoft.com/en-us/library/…- >このパターンは非常にうまく機能すると思います。私は「タプル」の提案が好きではありません。
マチャド

if (args == null) throw new ArgumentException();これで、IWorkerのすべてのコンシューマーはその具体的な型を知る必要があります-そして、インターフェイスは役に立たなくなります。それを取り除き、代わりに具体的な型を使用することもできます。それは悪い考えですね。
ベルンハルトヒラー

プラグ可能なアーキテクチャのため、IWorkerインターフェイスが必要です(WorkerFactory.GetWorker戻り値の型は1つしか持てません)。この例の範囲外ではありますが、呼び出し元はworkerName; おそらく、適切な引数も考え出すことができます。
ジョン・ウー

2

@Dunkのコメントに基づいてソリューションを再設計しました。

... IWorkerインスタンスが作成される前に必要なすべてのパラメーターを既に知っています。したがって、DoWorkメソッドではなく、コンストラクタにこれらの引数を渡す必要があります。IOW、ファクトリクラスを利用します。インスタンスの構築の詳細を隠すことが、ファクトリクラスが存在する主な理由です。

そのため、IWorkerを作成するために必要なすべての引数をIWorerFactory.GetWorkerメソッドにシフトすると、各ワーカーは既に必要なものを持っているので、コーディネーターはworker.DoWork()を呼び出すことができます。

    public interface IWorkerFactory
    {
        IWorker GetWorker(string workerName, int a, List<int> b, string c);
    }

    public class WorkerFactory : IWorkerFactory
    {
        public IWorker GetWorker(string workerName, int a, List<int> b, string c)
        {
            switch (workerName)
            {
                case "WorkerA":
                    return new ConcreteWorkerA(a, b);
                case "WorkerB":
                    return new ConcreteWorkerB(a, b, c);
                default:
                    return null;
            }
        }
    }

    public class Coordinator
    {
        private readonly IWorkerFactory _workerFactory;

        public Coordinator(IWorkerFactory workerFactory)
        {
            _workerFactory = workerFactory;
        }

        // Adding 'c' breaks Open/Closed principal for the Coordinator and WorkerFactory; but this has to happen somewhere...
        public string GetWorkerResult(string workerName, int a, List<int> b, string c)
        {
            var worker = _workerFactory.GetWorker(workerName, a, b, c);

            if (worker != null)
                return worker.DoWork();
            else
                return string.Empty;
        }
    }

    public interface IWorker
    {
        string DoWork();
    }

    public class ConcreteWorkerA : IWorker
    {
        private readonly int _a;
        private readonly List<int> _b;

        public ConcreteWorkerA(int a, List<int> b)
        {
            _a = a;
            _b = b;
        }

        public string DoWork()
        {
            // does the required work based on 'a' and 'b'
            return "some A worker result";
        }
    }

    public class ConcreteWorkerB : IWorker
    {
        private readonly int _a;
        private readonly List<int> _b;
        private readonly string _c;

        public ConcreteWorkerB(int a, List<int> b, string c)
        {
            _a = a;
            _b = b;
            _c = c;
        }

        public string DoWork()
        {
            // does some different work based on the value of 'a', 'b' and 'c'
            return "some B worker result";
        }
    }

1
すべての状況で3つすべてが使用されているわけではありませんが、3つのパラメーターを受け取るファクトリメソッドがあります。さらに多くのパラメーターを必要とするオブジェクトCがある場合はどうしますか?それらをメソッドシグネチャに追加しますか?このソリューションは拡張可能ではなく、IMOに不適切なアドバイスがあります
アモルフィス

3
さらに引数が必要な新しいConcreteWorkerCが必要な場合は、はい、GetWorkerメソッドに追加されます。はい、ファクトリはOpen / Closedプリンシパルに準拠していません-しかし、どこかでこのようにする必要があり、私の意見ではファクトリが最良の選択肢でした。私の提案は、これが悪いアドバイスだというだけでなく、実際に代替ソリューションを投稿することでコミュニティを支援することです。
-JTech

1

いくつかのことの1つを提案します。

コールサイトがワーカーまたはワーカーファクトリの内部動作について何も知る必要がないようにカプセル化を維持する場合は、追加のパラメーターを持つようにインターフェイスを変更する必要があります。パラメーターにはデフォルト値を設定できるため、一部のコールサイトでは2つのパラメーターのみを使用できます。これには、消費するライブラリを再コンパイルする必要があります。

カプセル化を破り、一般的に悪いOOPであるため、私がお勧めするもう1つのオプションです。これには、少なくとものすべての呼び出しサイトを変更できることが必要ですConcreteWorkerBIWorkerインターフェースを実装するクラスを作成できますDoWorkが、追加のパラメーターを持つメソッドもあります。次に、あなたのcallsitesにキャストしようとするIWorkervar workerB = myIWorker as ConcreteWorkerB;、その後3つのパラメータを使用しDoWork、具体的なタイプに。繰り返しますが、これは悪い考えですが、あなたができることです。


0

@Jtech、params引数の使用を検討しましたか?これにより、さまざまな量のパラメーターを渡すことができます。

https://msdn.microsoft.com/en-us/library/w5zay9db(v=vs.71).aspx


DoWorkメソッドが各引数で同じことを行い、各引数が同じタイプである場合、paramsキーワードが意味をなす場合があります。それ以外の場合、DoWorkメソッドはparams配列の各引数が正しい型であることを確認する必要がありますが、2つの文字列があり、それぞれが異なる目的に使用されているとしましょう。 1 ...配列内の位置に基づいて仮定する必要があります。私の好みにはあまりにも緩い。@JohnWuのソリューションはよりタイトだと感じています。
-JTech
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.