Patrón de diseño C # para trabajadores con diferentes parámetros de entrada

14

No estoy seguro de qué patrón de diseño podría ayudarme a resolver este problema.

Tengo una clase, 'Coordinador', que determina qué clase de Trabajador se debe usar, sin tener que conocer todos los diferentes tipos de Trabajadores que hay, simplemente llama a WorkerFactory y actúa sobre la interfaz común de IWorker.

Luego establece el Trabajador apropiado para trabajar y devuelve el resultado de su método 'DoWork'.

Esto ha estado bien ... hasta ahora; tenemos un nuevo requisito para una nueva clase de Trabajador, "WorkerB", que requiere una cantidad adicional de información, es decir, un parámetro de entrada adicional, para que pueda hacer su trabajo.

Es como si necesitáramos un método DoWork sobrecargado con el parámetro de entrada adicional ... pero todos los trabajadores existentes tendrían que implementar ese método, lo que parece incorrecto ya que esos trabajadores realmente no necesitan ese método.

¿Cómo puedo refactorizar esto para mantener al Coordinador al tanto de qué Trabajador se está utilizando y aún así permitir que cada Trabajador obtenga la información que necesita para hacer su trabajo pero que ningún Trabajador haga cosas que no necesita?

Ya hay muchos trabajadores existentes.

No quiero tener que cambiar ninguno de los Trabajadores concretos existentes para acomodar los requisitos de la nueva clase WorkerB.

Pensé que tal vez un patrón Decorador sería bueno aquí, pero no he visto ningún Decorador decorar un objeto con el mismo método pero con diferentes parámetros antes ...

Situación en el código:

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";
    }    
}
JTech
fuente
¿La IWorkerinterfaz muestra la versión anterior o es una nueva versión con un parámetro agregado?
JamesFaix
¿Los lugares en su código base que actualmente usan IWorker con 2 parámetros van a necesitar conectar el tercer parámetro, o solo los nuevos sitios de llamadas van a usar el tercer parámetro?
JamesFaix
2
En lugar de ir a comprar un patrón, intente enfocarse en el diseño general independientemente de si se aplica o no un patrón. Lectura recomendada: ¿Qué tan malas son las preguntas del tipo "Comprar patrones"?
1
Según su código, ya conoce todos los parámetros necesarios antes de crear la instancia de IWorker. Por lo tanto, debería haber pasado esos argumentos al constructor y no al método DoWork. IOW, utiliza tu clase de fábrica. Ocultar los detalles de la construcción de la instancia es prácticamente la razón principal de la existencia de la clase de fábrica. Si tomaste ese enfoque, entonces la solución es trivial. Además, lo que está tratando de lograr en la forma en que lo está intentando es malo OO. Viola el principio de sustitución de Liskov.
Dunk
1
Creo que debes volver a otro nivel. Coordinatorya tuvo que cambiarse para acomodar ese parámetro adicional en su GetWorkerResultfunción, eso significa que se viola el Principio de Cerrado Abierto de SOLID. Como consecuencia, todas las llamadas de código Coordinator.GetWorkerResulttuvieron que ser cambiadas también. Mire el lugar donde llama a esa función: ¿cómo decide qué IWorker solicitar? Eso puede conducir a una mejor solución.
Bernhard Hiller

Respuestas:

9

Deberá generalizar los argumentos para que quepan en un solo parámetro con una interfaz base y un número variable de campos o propiedades. Algo así como esto:

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

Tenga en cuenta las verificaciones nulas ... debido a que su sistema es flexible y está sujeto a retraso, tampoco es seguro para escribir, por lo que deberá verificar su conversión para asegurarse de que los argumentos que se pasan sean válidos.

Si realmente no desea crear objetos concretos para cada combinación posible de argumentos, puede usar una tupla en su lugar (no sería mi primera opción).

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);
John Wu
fuente
1
Esto es similar a cómo las aplicaciones de Windows Forms manejan los eventos. 1 parámetro "args" y un parámetro "fuente del evento". Todos los "args" se subclasifican de EventArgs: msdn.microsoft.com/en-us/library/… -> Yo diría que este patrón funciona muy bien. Simplemente no me gusta la sugerencia de "Tupla".
Machado
if (args == null) throw new ArgumentException();Ahora cada consumidor de un IWorker debe conocer su tipo concreto, y la interfaz es inútil: también puede deshacerse de él y utilizar los tipos concretos. Y esa es una mala idea, ¿no?
Bernhard Hiller
La interfaz IWorker es necesaria debido a la arquitectura conectable ( WorkerFactory.GetWorkersolo puede tener un tipo de retorno). Si bien está fuera del alcance de este ejemplo, sabemos que la persona que llama puede encontrar un workerName; presumiblemente también puede presentar argumentos apropiados.
John Wu
2

He rediseñado la solución en función del comentario de @ Dunk:

... ya conoce todos los parámetros necesarios antes de crear la instancia de IWorker. Por lo tanto, debería haber pasado esos argumentos al constructor y no al método DoWork. IOW, utiliza tu clase de fábrica. Ocultar los detalles de la construcción de la instancia es prácticamente la razón principal de la existencia de la clase de fábrica.

Así que cambié todos los argumentos posibles necesarios para crear un IWorker en el método IWorerFactory.GetWorker y luego cada trabajador ya tiene lo que necesita y el Coordinador puede simplemente llamar a 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";
        }
    }
JTech
fuente
1
tiene un método de fábrica que recibe 3 parámetros, aunque no todos se utilizan en todas las situaciones. ¿Qué harás si tienes un objeto C que necesita aún más parámetros? ¿los agregará a la firma del método? esta solución no es extensible y mal aconsejada OMI
Amorphis
3
Si necesitaba un nuevo ConcreteWorkerC que necesita más argumentos, entonces sí, se agregarían al método GetWorker. Sí, la Fábrica no se ajusta al principio Abierto / Cerrado, pero algo en algún lugar tiene que ser así y, en mi opinión, la Fábrica era la mejor opción. Mi sugerencia es: en lugar de decir que esto no es aconsejable, ayudarás a la comunidad publicando una solución alternativa.
JTech
1

Sugeriría una de varias cosas.

Si desea mantener la encapsulación, de modo que los sitios de llamadas no tengan que saber nada sobre el funcionamiento interno de los trabajadores o la fábrica de trabajadores, deberá cambiar la interfaz para tener el parámetro adicional. El parámetro puede tener un valor predeterminado, por lo que algunos sitios de llamadas aún pueden usar solo 2 parámetros. Esto requerirá que las bibliotecas consumidoras se vuelvan a compilar.

La otra opción que recomendaría contra, ya que rompe la encapsulación y generalmente es una mala POO. Esto también requiere que al menos pueda modificar todos los sitios de llamadas ConcreteWorkerB. Puede crear una clase que implemente la IWorkerinterfaz, pero que también tenga un DoWorkmétodo con un parámetro adicional. Luego, en sus sitios de llamadas, intente lanzar el IWorkercon var workerB = myIWorker as ConcreteWorkerB;y luego use los tres parámetros DoWorken el tipo concreto. Nuevamente, esta es una mala idea, pero es algo que podrías hacer.

JamesFaix
fuente
0

@Jtech, ¿has considerado el uso del paramsargumento? Esto permite que se pase una cantidad variable de parámetros.

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

Jon Raynor
fuente
La palabra clave params podría tener sentido si el método DoWork hizo lo mismo con cada argumento y si cada argumento era del mismo tipo. De lo contrario, el método DoWork necesitaría verificar que cada argumento en la matriz de parámetros sea del tipo correcto, pero digamos que tenemos dos cadenas allí y cada una se usó para un propósito diferente, ¿cómo podría DoWork asegurarse de que tenga el correcto? uno ... tendría que asumir en función de la posición en la matriz. Todo demasiado suelto para mi gusto. Siento que la solución de @ JohnWu es más estricta.
JTech