Шаблон проектирования C # для рабочих с разными входными параметрами

14

Я не уверен, какой шаблон проектирования может помочь мне решить эту проблему.

У меня есть класс Coordinator, который определяет, какой класс Worker должен использоваться - без необходимости знать обо всех различных типах Workers - он просто вызывает WorkerFactory и действует на общий интерфейс IWorker.

Затем он устанавливает соответствующий Worker и возвращает результат его метода DoWork.

Это было хорошо ... до сих пор; у нас есть новое требование для нового класса Worker, «WorkerB», для которого требуется дополнительный объем информации, то есть дополнительный входной параметр, для выполнения своей работы.

Это похоже на то, что нам нужен перегруженный метод DoWork с дополнительным входным параметром ... но тогда все существующие работники должны будут реализовать этот метод - что кажется неправильным, поскольку этим рабочим действительно не нужен этот метод.

Как я могу реорганизовать это так, чтобы Координатор не знал, какой работник используется, и при этом позволял каждому работнику получать информацию, необходимую ему для выполнения своей работы, но не заставлять какого-либо работника делать то, что ему не нужно?

Уже существует много рабочих.

Я не хочу менять ни одного из существующих конкретных рабочих, чтобы соответствовать требованиям нового класса WorkerB.

Я подумал, может быть, здесь хорошо подойдет шаблон Decorator, но я не видел, чтобы декораторы украшали объект одним и тем же методом, но разными параметрами раньше ...

Ситуация в коде:

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
источник
Указан ли в IWorkerинтерфейсе старая версия или это новая версия с добавленным параметром?
JamesFaix
Должны ли места в вашей кодовой базе, которые в настоящее время используют IWorker с 2 параметрами, подключать третий параметр, или только третий сайт вызовов будет использовать третий параметр?
JamesFaix
2
Вместо того, чтобы делать покупки для шаблона, попробуйте сосредоточиться на общем дизайне, независимо от того, применяется шаблон или нет. Рекомендуемое чтение: Насколько плохи вопросы типа «Покупки по шаблонам»?
1
Согласно вашему коду, вы уже знаете все параметры, необходимые до создания экземпляра IWorker. Таким образом, вы должны были передать эти аргументы конструктору, а не методу DoWork. IOW, используйте свой фабричный класс. Сокрытие деталей конструирования экземпляра является основной причиной существования фабричного класса. Если вы выбрали такой подход, то решение тривиально. Кроме того, то, что вы пытаетесь достичь таким же образом, как и вы, это плохая ОО. Это нарушает принцип подстановки Лискова.
Данк
1
Я думаю, что вы должны вернуться на другой уровень. Coordinatorуже пришлось изменить, чтобы учесть этот дополнительный параметр в своей GetWorkerResultфункции - это означает, что принцип Open-Closed-принцип SOLID нарушается. Как следствие, весь код вызова 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";
    }
} 

Обратите внимание на пустые проверки ... потому что ваша система гибкая и с поздним связыванием, она также не является безопасной для типов, поэтому вам нужно будет проверить свое приведение, чтобы убедиться, что переданные аргументы действительны.

Если вы действительно не хотите создавать конкретные объекты для каждой возможной комбинации аргументов, вы можете вместо этого использовать кортеж (не будет моим первым выбором).

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 Forms обрабатывают события. 1 параметр «args» и один параметр «источник события». Все «аргументы» являются подклассами из EventArgs: msdn.microsoft.com/en-us/library/… -> Я бы сказал, что этот шаблон работает очень хорошо. Мне просто не нравится предложение "Tuple".
Мачадо
if (args == null) throw new ArgumentException();Теперь каждый потребитель IWorker должен знать его конкретный тип - и интерфейс бесполезен: вы также можете избавиться от него и использовать вместо этого конкретные типы. И это плохая идея, не правда ли?
Бернхард Хиллер
Интерфейс IWorker требуется из-за подключаемой архитектуры ( WorkerFactory.GetWorkerможет иметь только один тип возврата). Несмотря на то, что за пределами этого примера мы знаем, что вызывающая сторона может придумать 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";
        }
    }
JTech
источник
1
у вас есть фабричный метод, который получает 3 параметра, хотя не все 3 используются во всех ситуациях. что вы будете делать, если у вас есть объект C, которому нужно еще больше параметров? вы добавите их в подпись метода? это решение не является расширяемым и плохо советуют ИМО
Amorphis
3
Если бы мне нужен был новый ConcreteWorkerC, которому нужно больше аргументов, то да, они были бы добавлены в метод GetWorker. Да, Фабрика не соответствует принципу Open / Closed - но где-то должно быть что-то вроде этого, и Фабрика, по моему мнению, была лучшим вариантом. Я предлагаю следующее: вместо того, чтобы просто сказать, что это не рекомендуется, вы поможете сообществу, фактически разместив альтернативное решение.
JTech
1

Я хотел бы предложить одну из нескольких вещей.

Если вы хотите поддерживать инкапсуляцию, чтобы узлы вызовов не должны были ничего знать о внутренней работе работников или фабрики работников, то вам нужно изменить интерфейс, чтобы иметь дополнительный параметр. Параметр может иметь значение по умолчанию, поэтому некоторые места вызова могут по-прежнему использовать только 2 параметра. Это потребует перекомпиляции любых библиотек-потребителей.

Другой вариант, против которого я бы порекомендовал, так как он нарушает инкапсуляцию и является просто плохим ООП. Это также требует, чтобы вы могли по крайней мере изменить все места вызова для ConcreteWorkerB. Вы можете создать класс, который реализует IWorkerинтерфейс, но также имеет DoWorkметод с дополнительным параметром. Затем на ваших сайтах вызовов попытайтесь привести значение IWorkerwith, var workerB = myIWorker as ConcreteWorkerB;а затем использовать три параметра DoWorkдля конкретного типа. Опять же, это плохая идея, но это то, что вы могли бы сделать.

JamesFaix
источник
0

@Jtech, вы рассмотрели использование paramsаргумента? Это позволяет передавать переменное количество параметров.

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

Джон Рейнор
источник
Ключевое слово params может иметь смысл, если метод DoWork делает то же самое с каждым аргументом и если каждый аргумент имеет один и тот же тип. В противном случае метод DoWork должен будет проверить, что каждый аргумент в массиве params имеет правильный тип - но допустим, что у нас есть две строки, и каждая использовалась для разных целей, как DoWork может гарантировать, что он имеет правильный один ... он должен был бы предположить основанный на положении в массиве. Все слишком свободно на мой вкус. Я чувствую, что решение @ JohnWu труднее.
JTech