C # Design Pattern per lavoratori con diversi parametri di input


14

Non sono sicuro di quale modello di progettazione potrebbe aiutarmi a risolvere questo problema.

Ho una classe, 'Coordinatore', che determina quale classe di lavoro dovrebbe essere usata - senza dover conoscere tutti i diversi tipi di lavoratori che ci sono - chiama semplicemente una WorkerFactory e agisce sulla comune interfaccia di IWorker.

Quindi imposta il lavoratore appropriato affinché funzioni e restituisce il risultato del suo metodo "DoWork".

Questo è andato bene ... fino ad ora; abbiamo un nuovo requisito per una nuova classe Worker, "WorkerB", che richiede una quantità aggiuntiva di informazioni, ad esempio un parametro di input aggiuntivo, affinché funzioni.

È come se avessimo bisogno di un metodo DoWork sovraccarico con il parametro di input aggiuntivo ... ma poi tutti i Worker esistenti dovrebbero implementare quel metodo - il che sembra sbagliato dato che quei Worker non hanno davvero bisogno di quel metodo.

Come posso fare un refactoring per mantenere il Coordinatore ignaro di quale Lavoratore viene utilizzato e consentire comunque a ciascun Lavoratore di ottenere le informazioni necessarie per svolgere il proprio lavoro, ma senza che un Lavoratore faccia ciò che non è necessario?

Ci sono già molti lavoratori esistenti.

Non voglio cambiare nessuno dei lavoratori concreti esistenti per soddisfare i requisiti della nuova classe WorkerB.

Ho pensato che forse un motivo Decoratore sarebbe stato buono qui, ma non ho visto nessun Decoratore decorare un oggetto con lo stesso metodo ma parametri diversi prima ...

Situazione nel codice:

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

L' IWorkerinterfaccia è elencata nella versione precedente o è una nuova versione con un parametro aggiunto?
JamesFaix,

I posti nella tua base di codice che attualmente utilizzano IWorker con 2 parametri dovranno inserire il 3o parametro o solo i nuovi siti di chiamata useranno il 3o parametro?
JamesFaix,

2
Invece di acquistare uno schema, prova a concentrarti sul design complessivo indipendentemente dal fatto che si applichi o meno uno schema. Lettura consigliata: quanto male sono le domande di tipo "Shopping for Patterns"?

1
Secondo il tuo codice, conosci già tutti i parametri necessari prima che venga creata l'istanza di IWorker. Quindi, avresti dovuto passare questi argomenti al costruttore e non al metodo DoWork. IOW, usa la tua classe di fabbrica. Nascondere i dettagli della costruzione dell'istanza è praticamente il motivo principale dell'esistenza della classe factory. Se hai adottato questo approccio, la soluzione è banale. Inoltre, ciò che stai cercando di ottenere nel modo in cui stai cercando di farlo è una cattiva OO. Viola il principio di sostituzione di Liskov.
Dunk,

1
Penso che tu debba tornare indietro di un altro livello. Coordinatordoveva già essere modificato per accogliere quel parametro aggiuntivo nella sua GetWorkerResultfunzione - ciò significa che il principio aperto-chiuso di SOLID è stato violato. Di conseguenza, è Coordinator.GetWorkerResultstato necessario modificare anche tutte le chiamate in codice . Quindi guarda il luogo in cui chiami quella funzione: come decidi quale IWorker richiedere? Ciò può portare a una soluzione migliore.
Bernhard Hiller,

Risposte:


9

Sarà necessario generalizzare gli argomenti in modo che si adattino a un singolo parametro con un'interfaccia di base e un numero variabile di campi o proprietà. In questo modo:

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

Nota i controlli null ... poiché il tuo sistema è flessibile e in ritardo, inoltre non è sicuro, quindi dovrai controllare il tuo cast per assicurarti che gli argomenti passati siano validi.

Se davvero non vuoi creare oggetti concreti per ogni possibile combinazione di argomenti, potresti usare una tupla (non sarebbe la mia prima scelta).

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
Questo è simile al modo in cui le applicazioni Windows Form gestiscono gli eventi. 1 parametro "args" e un parametro "sorgente dell'evento". Tutti gli "arg" sono sottoclassati da EventArgs: msdn.microsoft.com/en-us/library/… -> Direi che questo modello funziona molto bene. Semplicemente non mi piace il suggerimento "Tupla".
Machado,

if (args == null) throw new ArgumentException();Ora ogni consumatore di un IWorker deve conoscere il suo tipo concreto - e l'interfaccia è inutile: puoi anche sbarazzartene e usare invece i tipi concreti. E questa è una cattiva idea, vero?
Bernhard Hiller,

L'interfaccia IWorker è necessaria a causa dell'architettura collegabile ( WorkerFactory.GetWorkerpuò avere solo un tipo di ritorno). Sebbene al di fuori dell'ambito di questo esempio, sappiamo che il chiamante è in grado di trovare un workerName; presumibilmente può anche presentare argomenti appropriati.
John Wu,

2

Ho riprogettato la soluzione in base al commento di @ Dunk:

... conosci già tutti i parametri necessari prima di creare l'istanza di IWorker. Quindi, avresti dovuto passare questi argomenti al costruttore e non al metodo DoWork. IOW, usa la tua classe di fabbrica. Nascondere i dettagli della costruzione dell'istanza è praticamente il motivo principale dell'esistenza della classe factory.

Quindi ho spostato tutti i possibili argomenti richiesti per creare un IWorker nel metodo IWorerFactory.GetWorker e quindi ogni lavoratore ha già ciò di cui ha bisogno e il coordinatore può semplicemente chiamare 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";
        }
    }

1
hai un metodo di fabbrica che riceve 3 parametri anche se non tutti e 3 vengono utilizzati in tutte le situazioni. cosa farai se hai un oggetto C che richiede ancora più parametri? li aggiungerai alla firma del metodo? questa soluzione non è estensibile e sconsigliata dall'IMO
Amorphis,

3
Se avessi bisogno di un nuovo ConcreteWorkerC che richiedesse più argomenti, sì, sarebbero stati aggiunti al metodo GetWorker. Sì, la fabbrica non è conforme al principio aperto / chiuso, ma qualcosa da qualche parte deve essere così e la fabbrica secondo me era l'opzione migliore. Il mio suggerimento è: piuttosto che dire semplicemente che questo è sconsigliato, aiuterai la community pubblicando una soluzione alternativa.
JTech,

1

Vorrei suggerire una delle diverse cose.

Se si desidera mantenere l'incapsulamento, in modo che i siti di chiamata non debbano sapere nulla sul funzionamento interno dei lavoratori o della fabbrica dei lavoratori, sarà necessario modificare l'interfaccia per avere il parametro aggiuntivo. Il parametro può avere un valore predefinito, in modo che alcuni siti di chiamata possano comunque utilizzare solo 2 parametri. Ciò richiederà che tutte le librerie che consumano vengano ricompilate.

L'altra opzione che consiglierei contro, poiché rompe l'incapsulamento ed è generalmente OOP cattiva. Ciò richiede anche che tu possa almeno modificare tutti i callites per ConcreteWorkerB. È possibile creare una classe che implementa l' IWorkerinterfaccia, ma ha anche un DoWorkmetodo con un parametro aggiuntivo. Quindi nei tuoi siti di chiamata prova a eseguire il cast di IWorkerwith var workerB = myIWorker as ConcreteWorkerB;e quindi utilizza i tre parametri DoWorksul tipo concreto. Ancora una volta, questa è una cattiva idea, ma è qualcosa che potresti fare.


0

@Jtech, hai considerato l'uso paramsdell'argomento? Ciò consente il passaggio di una quantità variabile di parametri.

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


La parola chiave params potrebbe avere senso se il metodo DoWork ha fatto la stessa cosa con ogni argomento e se ogni argomento era dello stesso tipo. Altrimenti, il metodo DoWork dovrebbe verificare che ogni argomento nell'array params sia del tipo corretto - ma supponiamo che ci siano due stringhe e ognuna sia stata utilizzata per uno scopo diverso, in che modo DoWork può garantire che abbia il corretto uno ... dovrebbe assumere in base alla posizione nella matrice. Tutto troppo sciolto per i miei gusti. Sento che la soluzione di @ JohnWu è più stretta.
JTech
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.