Wzorzec projektowy C # dla pracowników o różnych parametrach wejściowych

14

Nie jestem pewien, który wzór może pomóc mi rozwiązać ten problem.

Mam klasę „Koordynator”, która określa, która klasa Worker powinna zostać użyta - bez konieczności znajomości wszystkich różnych rodzajów Workerów - po prostu wywołuje WorkerFactory i działa na wspólnym interfejsie IWorker.

Następnie ustawia odpowiedniego Workera do pracy i zwraca wynik metody „DoWork”.

Było dobrze ... do tej pory; mamy nowy wymóg dotyczący nowej klasy Worker, „WorkerB”, która wymaga dodatkowej ilości informacji, tj. dodatkowego parametru wejściowego, aby mógł on wykonać swoją pracę.

To tak, jakbyśmy potrzebowali przeciążonej metody DoWork z dodatkowym parametrem wejściowym ... ale wtedy wszyscy istniejący pracownicy musieliby zaimplementować tę metodę - co wydaje się błędne, ponieważ ci pracownicy naprawdę nie potrzebują tej metody.

Jak mogę to zmienić, aby Koordynator nie był świadomy, z którego Pracownika korzysta, a jednocześnie pozwalał każdemu Pracownikowi na uzyskanie informacji potrzebnych mu do wykonywania swojej pracy, ale nie pozwalał, aby Żaden Pracownik robił rzeczy, których nie potrzebuje?

Istnieje już wielu pracowników.

Nie chcę zmieniać żadnego z istniejących konkretnych Workerów, aby dostosować je do wymagań nowej klasy WorkerB.

Myślałem, że być może wzór Dekoratora byłby tutaj dobry, ale nie widziałem żadnych Dekoratorów dekorujących obiekt tą samą metodą, ale o różnych parametrach przed ...

Sytuacja w kodzie:

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
źródło
Czy IWorkerinterfejs zawiera starą wersję, czy jest to nowa wersja z dodanym parametrem?
JamesFaix
Czy miejsca w Twojej bazie kodu, które obecnie używają IWorker z 2 parametrami, będą musiały podłączyć trzeci parametr, czy też tylko nowe witryny wywołujące będą używać trzeciego parametru?
JamesFaix
2
Zamiast kupować wzór, spróbuj skupić się na ogólnym projekcie, niezależnie od tego, czy wzór ma zastosowanie. Zalecana lektura: Jak złe są pytania typu „Zakupy dla wzorów”?
1
Zgodnie z Twoim kodem znasz już wszystkie parametry potrzebne przed utworzeniem instancji IWorker. Dlatego powinieneś przekazać te argumenty do konstruktora, a nie do metody DoWork. IOW, skorzystaj z klasy fabrycznej. Ukrywanie szczegółów budowy instancji jest właściwie głównym powodem istnienia klasy fabryki. Jeśli zastosujesz to podejście, rozwiązanie jest trywialne. To, co próbujesz osiągnąć w sposób, w jaki próbujesz to osiągnąć, jest złe, OO. Narusza to zasadę substytucji Liskowa.
Dunk
1
Myślę, że musisz cofnąć się o kolejny poziom. Coordinatorjuż musiał zostać zmieniony, aby uwzględnić ten dodatkowy parametr w swojej GetWorkerResultfunkcji - oznacza to, że naruszono zasadę otwartego-zamkniętego SOLID. W rezultacie wszystkie wywołania kodu również Coordinator.GetWorkerResultmusiały zostać zmienione. Spójrz więc na miejsce, w którym wywołujesz tę funkcję: w jaki sposób decydujesz, którego IWorkera poprosić? Może to prowadzić do lepszego rozwiązania.
Bernhard Hiller

Odpowiedzi:

9

Trzeba będzie uogólnić argumenty, aby pasowały do ​​jednego parametru z interfejsem podstawowym i zmienną liczbą pól lub właściwości. Coś w stylu:

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

Zwróć uwagę na kontrole zerowe ... ponieważ twój system jest elastyczny i spóźniony, nie jest również bezpieczny pod względem typu, więc musisz sprawdzić rzutowanie, aby upewnić się, że przekazane argumenty są poprawne.

Jeśli naprawdę nie chcesz tworzyć konkretnych obiektów dla każdej możliwej kombinacji argumentów, możesz zamiast tego użyć krotki (nie byłbym moim pierwszym wyborem).

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
źródło
1
Jest to podobne do sposobu, w jaki aplikacje Windows Forms radzą sobie ze zdarzeniami. 1 parametr „args” i jeden parametr „źródło zdarzenia”. Wszystkie „argumenty” są podklasowane z EventArgs: msdn.microsoft.com/en-us/library/… -> Powiedziałbym, że ten wzór działa bardzo dobrze. Po prostu nie podoba mi się sugestia „Tuple”.
Machado
if (args == null) throw new ArgumentException();Teraz każdy konsument IWorker musi znać swój konkretny typ - a interfejs jest bezużyteczny: możesz go również pozbyć i zamiast tego użyć konkretnych typów. A to zły pomysł, prawda?
Bernhard Hiller
Interfejs IWorker jest wymagany ze względu na architekturę wtykową ( WorkerFactory.GetWorkermoże mieć tylko jeden typ zwrotu). Chociaż poza zakresem tego przykładu, wiemy, że dzwoniący może wymyślić workerName; przypuszczalnie może również wymyślić odpowiednie argumenty.
John Wu
2

Przeprojektowałem rozwiązanie w oparciu o komentarz @ Dunk:

... znasz już wszystkie parametry potrzebne przed utworzeniem instancji IWorker. Dlatego powinieneś przekazać te argumenty do konstruktora, a nie do metody DoWork. IOW, skorzystaj z klasy fabrycznej. Ukrywanie szczegółów budowy instancji jest właściwie głównym powodem istnienia klasy fabryki.

Przesunąłem więc wszystkie możliwe argumenty wymagane do utworzenia IWorkera do metody IWorerFactory.GetWorker, a następnie każdy pracownik ma już to, czego potrzebuje, a Koordynator może po prostu wywoł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
źródło
1
masz metodę fabryczną, która otrzymuje 3 parametry, chociaż nie wszystkie 3 są używane we wszystkich sytuacjach. co zrobisz, jeśli masz obiekt C, który potrzebuje jeszcze więcej parametrów? dodasz je do podpisu metody? to rozwiązanie nie jest rozszerzalne i źle doradzone IMO
Amorphis
3
Gdybym potrzebował nowego ConcreteWorkerC, który potrzebuje więcej argumentów, to tak, zostałyby dodane do metody GetWorker. Tak, Fabryka nie jest zgodna z zasadą Otwarta / Zamknięta - ale coś musi tak być i Fabryka moim zdaniem była najlepszą opcją. Moja sugestia jest następująca: zamiast po prostu powiedzieć, że jest to niewskazane, pomożesz społeczności, publikując alternatywne rozwiązanie.
JTech
1

Sugerowałbym jedną z kilku rzeczy.

Jeśli chcesz zachować enkapsulację, aby strony wywoławcze nie musiały nic wiedzieć o wewnętrznych działaniach pracowników lub fabryki pracowników, musisz zmienić interfejs, aby mieć dodatkowy parametr. Parametr może mieć wartość domyślną, dzięki czemu niektóre strony wywoławcze mogą nadal używać tylko 2 parametrów. Będzie to wymagało ponownej kompilacji wszystkich zużywających się bibliotek.

Inną opcję, której odradzam, ponieważ łamie enkapsulację i jest po prostu złym OOP. Wymaga to również, abyś mógł przynajmniej zmodyfikować wszystkie strony wywoławcze ConcreteWorkerB. Możesz stworzyć klasę, która implementuje IWorkerinterfejs, ale ma także DoWorkmetodę z dodatkowym parametrem. Następnie w twoich połączeniach próbuj rzutować za IWorkerpomocą, var workerB = myIWorker as ConcreteWorkerB;a następnie użyj trzech parametrów DoWorkna konkretny typ. Ponownie jest to zły pomysł, ale można to zrobić.

JamesFaix
źródło
0

@Jech, czy zastanawiałeś się nad użyciem paramsargumentu? Pozwala to na przekazanie zmiennej liczby parametrów.

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

Jon Raynor
źródło
Słowo kluczowe params może mieć sens, jeśli metoda DoWork zrobi to samo z każdym argumentem i jeśli każdy argument będzie tego samego typu. W przeciwnym razie metoda DoWork musiałaby sprawdzić, czy każdy argument w tablicy params był poprawnego typu - ale powiedzmy, że mamy tam dwa ciągi znaków i każdy z nich został użyty w innym celu, w jaki sposób DoWork mógłby upewnić się, że ma poprawny jeden ... musiałby przyjąć na podstawie pozycji w tablicy. Zbyt luźne jak na mój gust. Uważam, że rozwiązanie @ JohnWu jest ściślejsze.
JTech