C # Шаблон проектування для працівників з різними вхідними параметрами


14

Я не впевнений, яка модель дизайну може допомогти мені вирішити цю проблему.

У мене є клас «Координатор», який визначає, який клас Worker слід використовувати - не знаючи про всі різні типи робітників - він просто викликає WorkerFactory і діє за загальним інтерфейсом IWorker.

Потім він встановлює відповідного Worker для роботи та повертає результат свого методу "DoWork".

Це було добре ... досі; у нас є нова вимога до нового класу Worker, "WorkerB", який вимагає додаткової кількості інформації, тобто додаткового вхідного параметра, щоб він міг виконувати свою роботу.

Начебто нам потрібен перевантажений метод DoWork з додатковим вхідним параметром ... але тоді всі існуючі Робітники повинні були реалізувати цей метод - що здається неправильним, оскільки цим Робітникам дійсно не потрібен цей метод.

Як я можу це зробити рефактор, щоб Координатор не знав про те, який працівник використовується, і все ж дозволяв кожному Працівнику отримати інформацію, необхідну для виконання своєї роботи, але не маючи жодного Робітника робити те, що йому не потрібно?

Вже є багато існуючих Робітників.

Мені не хочеться змінювати жодного з існуючих конкретних Workers, щоб вони відповідали вимогам нового класу WorkerB.

Я подумав, що, можливо, тут добре підійде візерунок «Декоратор», але я ще не бачив, щоб декоративники прикрашали об’єкт тим самим методом, але з іншими параметрами раніше ...

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

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

Чи в IWorkerінтерфейсі вказана стара версія чи це нова версія з доданим параметром?
JamesFaix

Чи потрібні місця у вашій кодовій базі, які зараз використовують IWorker з двома параметрами, підключити 3-й параметр, або лише нові сайти викликів збираються використовувати 3-й параметр?
JamesFaix

2
Замість того, щоб ходити по магазинах за шаблоном, спробуйте зосередитись на загальному дизайні незалежно від того, застосовується чи ні шаблон. Рекомендоване читання: Наскільки погані питання типу "Покупки для візерунків"?

1
Відповідно до вашого коду, ви вже знаєте всі параметри, необхідні до створення екземпляра IWorker. Таким чином, ви повинні передати ці аргументи конструктору, а не методу DoWork. IOW, використовуйте свій заводський клас. Приховування деталей побудови екземпляра є значною мірою основною причиною існування фабричного класу. Якщо ви скористалися таким підходом, то рішення тривіальне. Крім того, те, що ви намагаєтеся зробити так, як ви це намагаєтесь зробити, це погано. Це порушує Принцип заміщення Ліскова.
Данк

1
Я думаю, ви повинні повернутися на інший рівень. Coordinatorвже довелося змінити, щоб увімкнути цей додатковий параметр у своїй GetWorkerResultфункції - це означає, що принцип відкритого закритого принципу 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може мати лише один тип повернення). Перебуваючи за межами цього прикладу, ми знаємо, що абонент може створити a workerName; імовірно, він може також викласти відповідні аргументи.
Джон Ву

2

Я переробив рішення на основі коментаря @ Данка:

... ви вже знаєте всі параметри, необхідні до створення екземпляра IWorker. Таким чином, ви повинні передати ці аргументи конструктору, а не методу DoWork. IOW, використовуйте свій заводський клас. Приховування деталей побудови екземпляра є значною мірою основною причиною існування фабричного класу.

Тому я перемістив усі можливі аргументи, необхідні для створення IWorker, в метод IWorerFactory.GetWorker, і тоді кожен працівник вже має те, що йому потрібно, і Координатор може просто викликати працівника.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
у вас є заводський метод, який отримує 3 параметри, хоча не всі 3 використовуються в будь-яких ситуаціях. що ви зробите, якщо у вас є об’єкт C, якому потрібні ще більше параметрів? ви додасте їх до підпису методу? це рішення не поширюється і не рекомендується ІМО
Аморфіс

3
Якщо мені знадобився новий ConcreteWorkerC, якому потрібно більше аргументів, то так, вони будуть додані до методу GetWorker. Так, Фабрика не відповідає принципу "Відкрито / закрито" - але щось десь повинно бути таким, і Фабрика, на мою думку, була найкращим варіантом. Моя пропозиція: замість того, щоб просто сказати, що це недоречно, ви допоможете громаді, опублікувавши альтернативне рішення.
JTech

1

Я б запропонував одну з кількох речей.

Якщо ви хочете підтримувати інкапсуляцію, щоб дзвінки не вимагали нічого знати про внутрішню роботу робітників або фабрику робітників, тоді вам потрібно буде змінити інтерфейс, щоб мати додатковий параметр. Параметр може мати значення за замовчуванням, так що деякі виклики можуть просто використовувати 2 параметри. Для цього знадобиться перекомпілювати всі споживачі бібліотеки.

Інший варіант, який я рекомендував би проти, оскільки він порушує інкапсуляцію і є загалом поганим OOP. Це також вимагає, щоб ви могли принаймні змінити всі виклики для ConcreteWorkerB. Ви можете створити клас, який реалізує IWorkerінтерфейс, але також має DoWorkметод із додатковим параметром. Потім у ваших викликах спробуйте передати команду IWorkerз, var workerB = myIWorker as ConcreteWorkerB;а потім використати три параметри DoWorkконкретного типу. Знову ж таки, це погана ідея, але це щось, що можна зробити.


0

@Jtech, ти розглядав можливість використання paramsаргументу? Це дозволяє передавати змінну кількість параметрів.

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


Ключове слово params може мати сенс, якщо метод DoWork робив те саме з кожним аргументом і якщо кожен аргумент був одного типу. В іншому випадку методом DoWork потрібно було б перевірити, чи кожен аргумент у масиві парамів був правильного типу - але скажемо, що у нас є два рядки, і кожен з них використовувався з іншою метою, як DoWork може переконатися, що він має правильну один ... він повинен був би припустити, виходячи з положення в масиві. Все занадто вільно, на мій смак. Я відчуваю, що рішення JohnWu жорсткіше.
JTech
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.