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


14

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

L' IWorkerinterface est-elle répertoriée dans l'ancienne version ou s'agit-il d'une nouvelle version avec un paramètre ajouté?
JamesFaix

Les emplacements de votre base de code qui utilisent actuellement IWorker avec 2 paramètres devront-ils brancher le 3ème paramètre, ou seuls les nouveaux sites d'appel utiliseront-ils le 3ème paramètre?
JamesFaix

2
Au lieu d'aller magasiner pour un motif, essayez de vous concentrer sur la conception globale, que le motif s'applique ou non. Lecture recommandée: Quelle est la gravité des questions de type «Shopping for Patterns»?

1
Selon votre code, vous connaissez déjà tous les paramètres nécessaires avant la création de l'instance IWorker. Ainsi, vous devriez 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. Si vous avez adopté cette approche, la solution est triviale. En outre, ce que vous essayez d'accomplir de la façon dont vous essayez d'accomplir, c'est mauvais OO. Il viole le principe de substitution de Liskov.
Dunk

1
Je pense que vous devez remonter d'un autre niveau. Coordinatora déjà dû être modifié pour tenir compte de ce paramètre supplémentaire dans sa GetWorkerResultfonction - cela signifie que le principe ouvert-fermé de SOLID est violé. En conséquence, tous les appels de code Coordinator.GetWorkerResultdevaient également être modifiés. Regardez donc l'endroit où vous appelez cette fonction: comment décidez-vous quel IWorker demander? Cela pourrait conduire à une meilleure solution.
Bernhard Hiller

Réponses:


9

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 pour le 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 toutes les combinaisons possibles 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);

1
Cela est similaire à la façon dont les applications Windows Forms traitent les événements. 1 paramètre "args" et un paramètre "source de l'événement". Tous les "arguments" sont sous-classés à partir d'EventArgs: msdn.microsoft.com/en-us/library/… -> Je dirais que ce modèle fonctionne très bien. Je n'aime tout simplement pas la suggestion "Tuple".
Machado

if (args == null) throw new ArgumentException();Désormais, chaque consommateur d'un IWorker doit connaître son type de béton - et l'interface est inutile: vous pouvez également vous en débarrasser et utiliser les types de béton à la place. Et c'est une mauvaise idée, non?
Bernhard Hiller

L'interface IWorker est requise en raison de l'architecture enfichable ( WorkerFactory.GetWorkerne peut avoir qu'un seul type de retour). Bien qu'en dehors de la portée de cet exemple, nous savons que l'appelant est capable de trouver un workerName; on peut supposer qu'il peut également présenter des arguments appropriés.
John Wu

2

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

1
vous avez une méthode d'usine qui reçoit 3 paramètres même si les 3 ne sont pas tous utilisés dans toutes les situations. que ferez-vous si vous avez un objet C qui nécessite encore plus de paramètres? les ajouterez-vous à la signature de la méthode? cette solution n'est pas extensible et mal conseillée IMO
Amorphis

3
Si j'avais besoin d'un nouveau ConcreteWorkerC qui a besoin de plus d'arguments, alors oui, ils seraient ajoutés à la méthode GetWorker. Oui, l'Usine n'est pas conforme au principe Open / Closed - mais quelque chose quelque part doit être comme ça et l'Usine à mon avis était la meilleure option. Ma suggestion est la suivante: plutôt que de simplement dire que c'est mal avisé, vous aiderez la communauté en affichant une solution alternative.
JTech

1

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' IWorkerinterface, mais possède également une DoWorkméthode avec un paramètre supplémentaire. Ensuite, dans vos appels, essayez de lancer le IWorkeravec var workerB = myIWorker as ConcreteWorkerB;, puis utilisez les trois paramètres DoWorksur le type concret. Encore une fois, c'est une mauvaise idée, mais c'est quelque chose que vous pourriez faire.


0

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

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


Le mot-clé params pourrait avoir un sens si la méthode DoWork faisait la même chose avec chaque argument et si chaque argument était du même type. Sinon, la méthode DoWork devrait vérifier que chaque argument du tableau params était du type correct - mais disons que nous avons deux chaînes dedans et que chacune a été utilisée dans un but différent, comment DoWork pourrait-il s'assurer qu'il a le bon un ... il devrait assumer en fonction de la position dans le tableau. Trop lâche à mon goût. Je pense que la solution de @ JohnWu est plus stricte.
JTech
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.