C # -Designmuster für Mitarbeiter mit unterschiedlichen Eingabeparametern

14

Ich bin mir nicht sicher, welches Entwurfsmuster mir bei der Lösung dieses Problems helfen könnte.

Ich habe eine Klasse, 'Coordinator', die festlegt, welche Worker-Klasse verwendet werden soll - ohne über die verschiedenen Arten von Workern Bescheid wissen zu müssen -, ruft einfach eine WorkerFactory auf und reagiert auf die gemeinsame IWorker-Schnittstelle.

Anschließend setzt es den entsprechenden Worker auf Arbeit und gibt das Ergebnis seiner DoWork-Methode zurück.

Das war in Ordnung ... bis jetzt; Wir haben eine neue Anforderung für eine neue Worker-Klasse, "WorkerB", die eine zusätzliche Menge an Informationen benötigt, dh einen zusätzlichen Eingabeparameter, damit sie ihre Arbeit erledigen kann.

Es ist, als ob wir eine überladene DoWork-Methode mit dem zusätzlichen Eingabeparameter benötigen ... aber dann müssten alle vorhandenen Worker diese Methode implementieren - was falsch erscheint, da diese Worker diese Methode wirklich nicht benötigen.

Wie kann ich dies umgestalten, damit der Koordinator nicht weiß, welcher Mitarbeiter verwendet wird, und jedem Mitarbeiter weiterhin die Informationen zur Verfügung stellt, die er für seine Arbeit benötigt, ohne dass ein Mitarbeiter Dinge tut, die er nicht benötigt?

Es gibt bereits viele existierende Arbeiter.

Ich möchte keinen der vorhandenen konkreten Worker ändern müssen, um den Anforderungen der neuen WorkerB-Klasse gerecht zu werden.

Ich dachte, vielleicht wäre ein Dekorationsmuster hier gut, aber ich habe noch nie gesehen, dass Dekorateure ein Objekt mit derselben Methode, aber unterschiedlichen Parametern dekorieren ...

Situation im Code:

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
quelle
Ist die IWorkerSchnittstelle in der alten Version aufgeführt oder handelt es sich um eine neue Version mit einem hinzugefügten Parameter?
JamesFaix
Müssen die Stellen in Ihrer Codebasis, die derzeit IWorker mit 2 Parametern verwenden, den 3. Parameter einstecken, oder verwenden nur neue Aufrufstellen den 3. Parameter?
JamesFaix
2
Anstatt ein Muster zu kaufen, sollten Sie sich auf das Gesamtdesign konzentrieren, unabhängig davon, ob ein Muster angewendet wird oder nicht. Empfohlene Lektüre: Wie schlecht sind Fragen vom Typ "Shopping for Patterns"?
1
Gemäß Ihrem Code kennen Sie bereits alle Parameter, die vor dem Erstellen der IWorker-Instanz erforderlich sind. Daher sollten Sie diese Argumente an den Konstruktor und nicht an die DoWork-Methode übergeben haben. IOW, nutzen Sie Ihre Fabrikklasse. Das Ausblenden der Details beim Erstellen der Instanz ist so ziemlich der Hauptgrund für die Existenz der Factory-Klasse. Wenn Sie diesen Ansatz gewählt haben, ist die Lösung trivial. Außerdem ist das, was Sie auf die Art und Weise erreichen möchten, wie Sie es erreichen möchten, schlecht. Es verstößt gegen das Liskov-Substitutionsprinzip.
Dunk
1
Ich denke, du musst ein anderes Level zurückgehen. Coordinatormusste bereits geändert werden, um diesen zusätzlichen Parameter in seiner GetWorkerResultFunktion zu berücksichtigen - das bedeutet, dass das Open-Closed-Prinzip von SOLID verletzt wird. Infolgedessen mussten auch alle Codeaufrufe Coordinator.GetWorkerResultgeändert werden. Schauen Sie sich also den Ort an, an dem Sie diese Funktion aufrufen: Wie entscheiden Sie, welchen IWorker Sie anfordern möchten? Das kann zu einer besseren Lösung führen.
Bernhard Hiller

Antworten:

9

Sie müssen die Argumente so verallgemeinern, dass sie in einen einzelnen Parameter mit einer Basisschnittstelle und einer variablen Anzahl von Feldern oder Eigenschaften passen. So ähnlich:

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

Beachten Sie die Nullprüfungen ... da Ihr System flexibel und spät gebunden ist, ist es auch nicht typsicher. Sie müssen daher Ihre Besetzung überprüfen, um sicherzustellen, dass die übergebenen Argumente gültig sind.

Wenn Sie wirklich nicht für jede mögliche Kombination von Argumenten konkrete Objekte erstellen möchten, können Sie stattdessen ein Tupel verwenden (wäre nicht meine erste Wahl.)

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
quelle
1
Dies ähnelt dem Umgang von Windows Forms-Anwendungen mit Ereignissen. 1 Parameter "args" und ein Parameter "source of the event". Alle "Argumente" werden von EventArgs in Unterklassen unterteilt: msdn.microsoft.com/en-us/library/… -> Ich würde sagen, dass dieses Muster sehr gut funktioniert. Ich mag den "Tupel" -Vorschlag einfach nicht.
Machado
if (args == null) throw new ArgumentException();Jetzt muss jeder Verbraucher eines IWorker seinen konkreten Typ kennen - und die Schnittstelle ist nutzlos: Sie können ihn auch entfernen und stattdessen die konkreten Typen verwenden. Und das ist eine schlechte Idee, nicht wahr?
Bernhard Hiller
Die IWorker-Schnittstelle ist aufgrund der steckbaren Architektur erforderlich ( WorkerFactory.GetWorkerkann nur einen Rückgabetyp haben). Außerhalb des Bereichs dieses Beispiels wissen wir, dass der Anrufer in der Lage ist, ein workerName; vermutlich kann es auch entsprechende Argumente liefern.
John Wu
2

Ich habe die Lösung basierend auf dem Kommentar von @ Dunk überarbeitet:

... Sie kennen bereits alle Parameter, die vor dem Erstellen der IWorker-Instanz benötigt werden. Daher sollten Sie diese Argumente an den Konstruktor und nicht an die DoWork-Methode übergeben haben. IOW, nutzen Sie Ihre Fabrikklasse. Das Ausblenden der Details beim Erstellen der Instanz ist so ziemlich der Hauptgrund für die Existenz der Factory-Klasse.

Daher habe ich alle möglichen Argumente, die zum Erstellen eines IWorker erforderlich sind, in die IWorerFactory.GetWorker-Methode verschoben. Dann hat jeder Worker bereits das, was er benötigt, und der Koordinator kann einfach worker.DoWork () aufrufen.

    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
quelle
1
Sie haben eine Factory-Methode, die 3 Parameter empfängt, obwohl nicht alle 3 in allen Situationen verwendet werden. Was tun Sie, wenn Sie ein Objekt C haben, das noch mehr Parameter benötigt? Fügen Sie sie der Methodensignatur hinzu? Diese Lösung ist nicht erweiterbar und schlecht beraten IMO
Amorphis
3
Wenn ich einen neuen ConcreteWorkerC benötige, der mehr Argumente benötigt, werden diese zur GetWorker-Methode hinzugefügt. Ja, die Fabrik entspricht nicht dem Open / Closed-Prinzip - aber irgendwo muss etwas so sein, und die Fabrik war meiner Meinung nach die beste Option. Mein Vorschlag lautet: Anstatt nur zu sagen, dass dies schlecht beraten ist, helfen Sie der Community, indem Sie tatsächlich eine alternative Lösung veröffentlichen.
JTech
1

Ich würde eines von mehreren Dingen vorschlagen.

Wenn Sie die Kapselung beibehalten möchten, damit die Callsites nichts über das Innenleben der Arbeiter oder der Arbeiterfabrik wissen müssen, müssen Sie die Schnittstelle ändern, um den zusätzlichen Parameter zu erhalten. Der Parameter kann einen Standardwert haben, sodass einige Call-Sites immer noch nur 2 Parameter verwenden können. Dies erfordert, dass alle verbrauchenden Bibliotheken neu kompiliert werden.

Die andere Option würde ich dagegen empfehlen, da sie die Kapselung unterbricht und im Allgemeinen nur eine schlechte OOP ist. Dies erfordert auch, dass Sie mindestens alle Call-Sites für ändern können ConcreteWorkerB. Sie können eine Klasse erstellen, die die IWorkerSchnittstelle implementiert , aber auch eine DoWorkMethode mit einem zusätzlichen Parameter hat. Versuchen Sie dann auf Ihren Callsites, das IWorkerwith zu konvertieren, var workerB = myIWorker as ConcreteWorkerB;und verwenden Sie dann die drei Parameter DoWorkfür den konkreten Typ. Auch dies ist eine schlechte Idee, aber es ist etwas, was Sie tun könnten .

JamesFaix
quelle
0

@Jtech, hast du über die Verwendung des paramsArguments nachgedacht ? Dadurch kann eine variable Anzahl von Parametern übergeben werden.

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

Jon Raynor
quelle
Das Schlüsselwort params kann sinnvoll sein, wenn die DoWork-Methode mit jedem Argument dasselbe tat und wenn jedes Argument vom gleichen Typ war. Andernfalls müsste die DoWork-Methode überprüfen, ob jedes Argument im params-Array vom richtigen Typ ist. Nehmen wir jedoch an, wir haben zwei Zeichenfolgen und jede wurde für einen anderen Zweck verwendet. Wie könnte DoWork sicherstellen, dass es das richtige hat? Eins ... es müsste basierend auf der Position im Array angenommen werden. Allzu locker für meinen Geschmack. Ich bin der Meinung, dass die Lösung von @ JohnWu enger ist.
JTech