web-dev-qa-db-fra.com

Modèle de conception C # pour les travailleurs avec différents paramètres d'entrée

Je ne sais pas quel modèle de conception pourrait m'aider à résoudre ce problème.

J'ai une classe, `` Coordinator '', qui détermine quelle classe Worker doit être utilisée - sans avoir à connaître tous les différents types de travailleurs qu'il existe - elle appelle simplement une WorkerFactory et agit sur l'interface IWorker commune.

Il définit ensuite le Worker approprié pour fonctionner et renvoie le résultat de sa méthode "DoWork".

Cela s'est bien passé ... jusqu'à maintenant; nous avons une nouvelle exigence pour une nouvelle classe Worker, "WorkerB" qui nécessite une quantité supplémentaire d'informations, c'est-à-dire un paramètre d'entrée supplémentaire, pour qu'elle puisse faire son travail.

C'est comme si nous avions besoin d'une méthode DoWork surchargée avec le paramètre d'entrée supplémentaire ... mais alors tous les Workers existants devraient implémenter cette méthode - ce qui semble faux car ces Workers n'ont vraiment pas besoin de cette méthode.

Comment puis-je refactoriser cela pour que le coordinateur ne sache pas quel travailleur est utilisé et qu'il permette toujours à chaque travailleur d'obtenir les informations dont il a besoin pour faire son travail, mais qu'aucun travailleur ne fasse les choses dont il n'a pas besoin?

Il y a déjà beaucoup de travailleurs existants.

Je ne veux pas avoir à changer l'un des travailleurs en béton existants pour répondre aux exigences de la nouvelle classe WorkerB.

Je pensais que peut-être un motif de décorateur serait bien ici, mais je n'ai vu aucun décorateur décorer un objet avec la même méthode mais des paramètres différents auparavant ...

Situation dans le code:

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";
    }    
}
14
JTech

J'ai repensé la solution en fonction du commentaire de @ Dunk:

... vous connaissez déjà tous les paramètres nécessaires avant la création de l'instance IWorker. Ainsi, vous devez avoir transmis ces arguments au constructeur et non à la méthode DoWork. IOW, utilisez votre classe d'usine. Masquer les détails de la construction de l'instance est à peu près la principale raison de l'existence de la classe d'usine.

J'ai donc déplacé tous les arguments possibles nécessaires pour créer un IWorker dans la méthode IWorerFactory.GetWorker, puis chaque travailleur a déjà ce dont il a besoin et le coordinateur peut simplement appeler 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";
        }
    }
2
JTech

Vous aurez besoin de généraliser les arguments afin qu'ils tiennent dans un seul paramètre avec une interface de base et un nombre variable de champs ou de propriétés. Un peu comme ça:

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

Notez les vérifications nulles ... car votre système est flexible et à liaison tardive, il n'est pas non plus sûr de type, vous devrez donc vérifier votre conversion pour vous assurer que les arguments qui sont passés sont valides.

Si vous ne voulez vraiment pas créer d'objets concrets pour chaque combinaison possible d'arguments, vous pouvez utiliser un Tuple à la place (ce ne serait pas mon premier choix.)

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);
9
John Wu

Je suggérerais plusieurs choses.

Si vous souhaitez conserver l'encapsulation, afin que les sites d'appels n'aient rien à savoir sur le fonctionnement interne des travailleurs ou de la fabrique de travailleurs, vous devrez modifier l'interface pour avoir le paramètre supplémentaire. Le paramètre peut avoir une valeur par défaut, de sorte que certains sites d'appels peuvent toujours utiliser 2 paramètres. Cela nécessitera que toutes les bibliothèques consommatrices soient recompilées.

L'autre option que je recommanderais contre, car elle rompt l'encapsulation et est généralement mauvaise POO. Cela nécessite également que vous puissiez au moins modifier tous les appels pour ConcreteWorkerB. Vous pouvez créer une classe qui implémente l'interface IWorker, mais possède également une méthode DoWork avec un paramètre supplémentaire. Ensuite, dans vos appels, essayez de convertir le IWorker avec var workerB = myIWorker as ConcreteWorkerB; puis utilisez les trois paramètres DoWork sur le type de béton. Encore une fois, c'est une mauvaise idée, mais c'est quelque chose que vous pourriez faire.

1
JamesFaix

@Jtech, avez-vous envisagé d'utiliser l'argument params? Cela permet de transmettre une quantité variable de paramètres.

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

0
Jon Raynor