C # Design Pattern para trabalhadores com diferentes parâmetros de entrada

14

Não tenho certeza de qual padrão de design pode me ajudar a resolver esse problema.

Eu tenho uma classe, 'Coordinator', que determina qual classe Worker deve ser usada - sem ter que saber sobre todos os diferentes tipos de Workers que existem - ela apenas chama um WorkerFactory e atua na interface comum do IWorker.

Em seguida, define o Worker apropriado para trabalhar e retorna o resultado do método 'DoWork'.

Isso tem sido bom ... até agora; temos um novo requisito para uma nova classe Worker, "WorkerB", que requer uma quantidade adicional de informações, isto é, um parâmetro de entrada adicional, para que ele faça seu trabalho.

É como se precisássemos de um método DoWork sobrecarregado com o parâmetro de entrada extra ... mas todos os Trabalhadores existentes teriam que implementar esse método - o que parece errado, pois esses Trabalhadores realmente não precisam desse método.

Como posso refatorar isso para manter o coordenador inconsciente de qual trabalhador está sendo usado e ainda permitir que cada trabalhador obtenha as informações necessárias para realizar seu trabalho, mas que nenhum trabalhador faça o que não é necessário?

Já existem muitos Trabalhadores.

Não quero alterar nenhum dos Trabalhadores concretos existentes para acomodar os requisitos da nova classe WorkerB.

Eu pensei que talvez um padrão Decorator seria bom aqui, mas eu não vi nenhum Decorator decorar um objeto com o mesmo método, mas com parâmetros diferentes antes ...

Situação no 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
fonte
A IWorkerinterface está listada na versão antiga ou é uma nova versão com um parâmetro adicionado?
JamesFaix 28/03
Os locais em sua base de código que atualmente usam o IWorker com 2 parâmetros precisarão conectar o terceiro parâmetro ou apenas os novos sites de chamada usarão o terceiro parâmetro?
JamesFaix 28/03
2
Em vez de comprar um padrão, tente se concentrar no design geral, independentemente de um padrão se aplicar ou não. Leitura recomendada: Quão ruins são as perguntas do tipo "Compras de padrões"?
1
De acordo com o seu código, você já conhece todos os parâmetros necessários antes da criação da instância do IWorker. Portanto, você deve ter passado esses argumentos para o construtor e não para o método DoWork. IOW, faça uso da sua classe de fábrica. Ocultar os detalhes da construção da instância é praticamente o principal motivo da existência da classe factory. Se você adotou essa abordagem, a solução é trivial. Além disso, o que você está tentando realizar da maneira que está tentando realizar é ruim OO. Viola o princípio da substituição de Liskov.
Dunk
1
Eu acho que você deve voltar outro nível. Coordinatorjá tinha que ser alterado para acomodar esse parâmetro extra em sua GetWorkerResultfunção - isso significa que o princípio de aberto-fechado do SOLID é violado. Como conseqüência, todas as chamadas de código também Coordinator.GetWorkerResultprecisavam ser alteradas. Portanto, observe o local em que você chama essa função: como você decide qual IWorker solicitar? Isso pode levar a uma solução melhor.
Bernhard Hiller

Respostas:

9

Você precisará generalizar os argumentos para que eles se ajustem a um único parâmetro com uma interface base e um número variável de campos ou propriedades. Mais ou menos assim:

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

Observe as verificações nulas ... porque seu sistema é flexível e com ligação tardia, também não é do tipo seguro; portanto, você precisará verificar sua conversão para garantir que os argumentos transmitidos sejam válidos.

Se você realmente não deseja criar objetos concretos para todas as combinações possíveis de argumentos, use uma tupla (não seria minha primeira escolha).

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
fonte
1
É semelhante à maneira como os aplicativos Windows Forms lidam com eventos. 1 parâmetro "args" e um parâmetro "fonte do evento". Todos os "args" são subclassificados de EventArgs: msdn.microsoft.com/en-us/library/… -> Eu diria que esse padrão funciona muito bem. Eu simplesmente não gosto da sugestão "Tuple".
Machado
if (args == null) throw new ArgumentException();Agora, todo consumidor de um IWorker deve conhecer seu tipo de concreto - e a interface é inútil: você também pode se livrar dele e usar os tipos de concreto. E isso é uma péssima ideia, não é?
Bernhard Hiller
A interface do IWorker é necessária devido à arquitetura conectável ( WorkerFactory.GetWorkerpode ter apenas um tipo de retorno). Enquanto estiver fora do escopo deste exemplo, sabemos que o chamador pode criar um workerName; presumivelmente, ele também pode apresentar argumentos apropriados.
John Wu
2

Eu reprojetei a solução com base no comentário de @ Dunk:

... você já conhece todos os parâmetros necessários antes da criação da instância do IWorker. Portanto, você deve ter passado esses argumentos para o construtor e não para o método DoWork. IOW, faça uso da sua classe de fábrica. Ocultar os detalhes da construção da instância é praticamente o principal motivo da existência da classe factory.

Então, mudei todos os argumentos possíveis necessários para criar um IWorker no método IWorerFactory.GetWorker e, em seguida, cada trabalhador já possui o que precisa e o Coordenador pode chamar apenas 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
fonte
1
você tem um método de fábrica que recebe 3 parâmetros, embora nem todos os 3 estejam sendo usados ​​em todas as situações. o que você fará se tiver um objeto C que precise de ainda mais parâmetros? você os adicionará à assinatura do método? esta solução não é extensível e mal aconselhado IMO
Amorphis
3
Se eu precisasse de um novo ConcreteWorkerC que necessitasse de mais argumentos, sim, eles seriam adicionados ao método GetWorker. Sim, a Fábrica não está em conformidade com o princípio Aberto / Fechado - mas algo em algum lugar deve ser assim e a Fábrica, na minha opinião, era a melhor opção. Minha sugestão é: em vez de apenas dizer que isso não é aconselhável, você ajudará a comunidade publicando uma solução alternativa.
JTech
1

Eu sugeriria uma de várias coisas.

Se você deseja manter o encapsulamento, para que os callites não precisem saber nada sobre o funcionamento interno dos trabalhadores ou da fábrica de trabalhadores, será necessário alterar a interface para obter o parâmetro extra. O parâmetro pode ter um valor padrão, para que alguns locais de chamada ainda possam usar apenas 2 parâmetros. Isso exigirá que todas as bibliotecas consumidoras sejam recompiladas.

A outra opção que eu recomendaria, uma vez que quebra o encapsulamento e geralmente é um POO ruim. Isso também requer que você possa pelo menos modificar todos os chamados ConcreteWorkerB. Você pode criar uma classe que implementa a IWorkerinterface, mas também possui um DoWorkmétodo com um parâmetro extra. Em seus callites, tente converter o IWorkerwith var workerB = myIWorker as ConcreteWorkerB;e use o parâmetro three DoWorkno tipo concreto. Novamente, essa é uma péssima idéia, mas é algo que você poderia fazer.

JamesFaix
fonte
0

@Jtech, você considerou o uso do paramsargumento? Isso permite que uma quantidade variável de parâmetros seja passada.

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

Jon Raynor
fonte
A palavra-chave params pode fazer sentido se o método DoWork fizer a mesma coisa com cada argumento e se cada argumento for do mesmo tipo. Caso contrário, o método DoWork precisaria verificar se cada argumento na matriz params era do tipo correto - mas digamos que temos duas strings e cada uma delas foi usada para uma finalidade diferente, como o DoWork pode garantir que ele tenha o correto um ... teria que assumir com base na posição na matriz. Tudo muito frouxo para o meu gosto. Sinto que a solução da @ JohnWu é mais rígida.
JTech 30/03