Metodo factory per oggetti con più costruttori complessi

2

Sto refactoring una base di codice legacy.
Ho 4 oggetti molto simili che ho deciso di essere un buon bersaglio per diventare polimorfico, quindi ho spostato tutto il codice comune in una classe base e ho aggiunto un'interfaccia.

Il vero problema sorge quando provi a creare quegli oggetti. Ogni classe ha 3 diversi costruttori con 5-10 parametri ciascuno, usati in diversi casi in base al contesto.
So già che quelle classi hanno troppe cose in corso, e in realtà sarebbe meglio dividere in classi diverse, più piccole, ognuna delle quali si occupa di un caso, tuttavia si tratta di una base di codice legacy e non posso apportare molte modifiche al codice esistente.

Al momento il mio codice è qualcosa del tipo:

public class HandlerFactory
{
    public static IHandler CreateInstance(HandlerType param1, string param2, ..., string param8)
    {
        switch(param1)
        {
            case HandlerType.Type1
                return new Handler1(param2, ..., param8)
            case HandlerType.Type2
                return new Handler2(param2, ..., param8)
            case HandlerType.Type3
                return new Handler3(param2, ..., param8)
            case HandlerType.Type4
                return new Handler4(param2, ..., param8)
        }
    }

    public static IHandler CreateInstance(HandlerType param1, int param2, ..., DateTime param10)
    {
        switch(param1)
        {
            case HandlerType.Type1
                return new Handler1(param2, ..., param10)
            case HandlerType.Type2
                return new Handler2(param2, ..., param10)
            case HandlerType.Type3
                return new Handler3(param2, ..., param10)
            case HandlerType.Type4
                return new Handler4(param2, ..., param10)
        }
    }

    public static IHandler CreateInstance(HandlerType param1, string param2, ..., CustomClass param5)
    {
        switch(param1)
        {
            case HandlerType.Type1
                return new Handler1(param2, ..., param5)
            case HandlerType.Type2
                return new Handler2(param2, ..., param5)
            case HandlerType.Type3
                return new Handler3(param2, ..., param5)
            case HandlerType.Type4
                return new Handler4(param2, ..., param5)
        }
    }
}

Tuttavia, odio avere 3 metodi di fabbrica, poiché ora devo modificarne tre se dovessi aggiungere un altro Handler (il motivo principale per cui sto facendo questo refactoring).

C'è un modo per mantenere un solo metodo factory con un singolo switch?

    
posta BgrWorker 07.04.2017 - 09:12
fonte

3 risposte

2

La risposta fornita da @JohnWu è molto elegante e mi piace molto. Consentitemi di fornire un'alternativa.

Potresti usare Modello di fabbrica astratto . Fondamentalmente avresti una grande fabbrica di fabbriche, che produrrebbe quindi le fabbriche di calcestruzzo per i gestori del calcestruzzo. Quindi, se hai bisogno di un altro gestore, tutto ciò che devi fare è scrivere una fabbrica per quel particolare gestore. Sarebbe qualcosa in questo senso:

public interface IHandler
{
}

public class Handler1 : IHandler
{
    public Handler1(string param1, string param2)
    {
    }

    public Handler1(int param1, int param2)
    {
    }
}

public class Handler2 : IHandler
{
    public Handler2(string param1, string param2)
    {
    }

    public Handler2(int param1, int param2)
    {
    }
}

Per semplicità, ho solo due classi Handler con due diversi costruttori. Credo che la soluzione possa ancora essere applicata al tuo caso. Successivamente, abbiamo fabbriche:

   public interface IHandlerFactory
    {
        IHandler Create(string param1, string param2);
        IHandler Create(int param1, int param2);
    }

    public class Handler1Factory : IHandlerFactory
    {
        public IHandler Create(int param1, int param2)
        {
            return new Handler1(param1, param2);
        }

        public IHandler Create(string param1, string param2)
        {
            return new Handler1(param1, param2);
        }
    }

    public class Handler2Factory : IHandlerFactory
    {
        public IHandler Create(int param1, int param2)
        {
            return new Handler2(param1, param2);
        }

        public IHandler Create(string param1, string param2)
        {
            return new Handler2(param1, param2);
        }
    }

    public class HandlerFactory
    {
        public static IHandler Create(IHandlerFactory factory, string param1, string param2)
        {
            return factory.Create(param1, param2);
        }

        public static IHandler Create(IHandlerFactory factory, int param1, int param2)
        {
            return factory.Create(param1, param2);
        }
    }

L'alternativa qui sarebbe avere una classe HandlerFactory che avrebbe una proprietà di tipo IHandlerFactory , ei metodi non sarebbero statici e non avrebbero il parametro IHandlerFactory in essi. Ovviamente, un oggetto della classe HandlerFactory dovrebbe essere istanziato ad un certo punto.

L'utilizzo sarebbe in qualche modo simile a questo:

public class Example
{
    public void ExampleMethod()
    {
        Handler1Factory factory1 = new Handler1Factory();
        var handler1 = HandlerFactory.Create(factory1, "parameter1", "parameter2");

        Handler2Factory factory2 = new Handler2Factory();
        var handler2 = HandlerFactory.Create(factory2, 1, 2);
    }
}

La soluzione divide chiaramente il codice per scopo e, soprattutto, se si ha la necessità di un altro gestore, non è necessario modificare il codice esistente. Tutto quello che dovresti fare è scrivere una fabbrica per quel particolare gestore.

Ricorda, questo è possibile solo perché tutti i gestori hanno i costruttori con gli stessi elenchi di parametri. Il design sarebbe un po 'diverso se non fosse il caso.

    
risposta data 07.04.2017 - 22:01
fonte
7

Questa soluzione elimina il caso / switch e la sostituisce con un argomento generico. Normalmente non è possibile creare oggetti in questo modo a meno che non si usi il vincolo new , che non consente alcun argomento del costruttore; Ho rubato questa soluzione alternativa da un'altra risposta.

Inoltre, qui c'è solo un metodo factory e gli argomenti del costruttore sono accettati come un elenco di dimensioni variabili usando parametri parola chiave.

public class HandlerFactory
{
    public static T CreateInstance<T>(params object[] constructorArguments) where T : class, IHandler
    {
        return (T)Activator.CreateInstance(typeof(T), constructorArguments);
    }
}

Come puoi vedere, questo metodo è molto più corto, non ha case / switch e richiede solo un prototipo. E la cosa bella è che se aggiungi mai Handler4 o qualsiasi altra sottoclasse, non devi toccare la fabbrica. Assicurati che Handler4 implementa IHandler e sei già tutto pronto.

Puoi chiamarlo così:

var o = HandlerFactory.CreateInstance<Handler3>(param1, param2);

Poiché il tipo di ritorno è T hai la possibilità di dichiarare o come var (e il compilatore dedurrà il tipo concreto) oppure potresti usare

IHandler o = HandlerFactory.CreateInstance<Handler3>(param1, param2);

se preferisci lavorare solo con l'interfaccia.

    
risposta data 07.04.2017 - 12:45
fonte
0

Come hai detto, il codice esistente ha problemi di progettazione così com'è. Primo, avere metodi e costruttori con così tanti (diversi!) Parametri è un odore di design.

Se non si risolve la causa principale della complessità, è possibile semplificare la vita aggiungendo un altro livello di astrazione. Ciò faciliterà il mantenimento della base del codice fino a quando non si avrà il tempo di rifattorizzarlo (o spostarlo in uno nuovo :)).

  1. Crea classi per i parametri passati (Param1, Param2, Param3).
  2. [Hack] Aggiungi un'interfaccia comune (ad esempio IParameter) per le 3 classi.
    • È un hack perché l'interfaccia non è molto comune, ma se vuoi avere un solo switch nella tua Factory, questa è una soluzione (quella che ho trovato).
  3. Crea una factory per l'interfaccia IParameter. La fabbrica stessa decide cosa costruire in base ai parametri impostati.
  4. [Hack] nei costruttori delle classi esistenti, aggiungi un'istruzione switch in base al tipo di classe concreto dell'oggetto IParameter.
    • È un hack perché questo non è ciò per cui le interfacce dovrebbero essere utilizzate. Le interfacce dovrebbero fornire un'astrazione per un comportamento comune.

DonNet Fiddle con esempio funzionante: link

Il codice per la lettura:

using System;

public class Program
{
    public void Main()
    {
        IHandler first = HandlerFactory.CreateInstance(
            HandlerType.Handler1,
            ParamBuilder.start().withOne("x").withTwo("y").build());
        IHandler second = HandlerFactory.CreateInstance(
            HandlerType.Handler2,
            ParamBuilder.start().withAlpha(1).withBeta(new DateTime()).build());
        IHandler third = HandlerFactory.CreateInstance(
            HandlerType.Handler3,
            ParamBuilder.start().withMickey(":)").withMouse(1L).build());

        first.DoStuff();
        second.DoStuff();
        third.DoStuff();
    }

    class HandlerFactory
    {
        public static IHandler CreateInstance(HandlerType type, IParam param)
        {
            switch (type) {
            case HandlerType.Handler1: return new Handler1(param);
            case HandlerType.Handler2: return new Handler2(param);
            case HandlerType.Handler3: return new Handler3(param);
            default: throw new NotSupportedException();
            }
        }
    }

    enum HandlerType { Handler1, Handler2, Handler3 
    }

    interface IHandler { void DoStuff(); }

    class Handler1 : IHandler {
        private Param1 numbers; private Param2 letters; private Param3 disney;

        public Handler1(IParam param) {
            if (param is Param1) numbers = (Param1) param;
            if (param is Param2) letters = (Param2) param;
            if (param is Param3) disney = (Param3) param;
        }

        public void DoStuff() { Console.WriteLine("I am alive - Handler 1");}
    }

    class Handler2 : IHandler {
        private Param1 numbers; private Param2 letters; private Param3 disney;

        public Handler2(IParam param) {
            if (param is Param1) numbers = (Param1) param;
            if (param is Param2) letters = (Param2) param;
            if (param is Param3) disney = (Param3) param;
        }

        public void DoStuff() { Console.WriteLine("I am alive - Handler 2");}
    }

    class Handler3 : IHandler {
        private Param1 numbers;
        private Param2 letters;
        private Param3 disney;

        public Handler3(IParam param) {
            if (param is Param1) numbers = (Param1) param;
            if (param is Param2) letters = (Param2) param;
            if (param is Param3) disney = (Param3) param;
        }

        public void DoStuff() { Console.WriteLine("I am alive - Handler 3");}
    }

    interface IParam {
        string One { get; } string Two { get; } // first param class

        int Alpha { get; } DateTime Beta { get; } // second param class

        string Mickey { get; } long Mouse { get; } // third param class
    }

    class ParamBuilder {
        private string one, two;
        private int? alpha; private DateTime? beta;
        private  string mickey; private long? mouse;

        public static ParamBuilder start() { return new ParamBuilder(); }

        public IParam build() {
            if (!string.IsNullOrEmpty(one) && !string.IsNullOrEmpty(one)) {
                return new Param1(one, two);
            }
            if (alpha.HasValue && beta.HasValue) {
                return new Param2(alpha.Value, beta.Value);
            }
            if (!string.IsNullOrEmpty(mickey) && mouse.HasValue) {
                return new Param3(mickey, mouse.Value);
            }

            throw new NotSupportedException();
        }

        public ParamBuilder withOne(string one) { this.one = one; return this; }
        public ParamBuilder withTwo(string two) { this.two = two; return this; }
        public ParamBuilder withAlpha(int alpha) { this.alpha = alpha; return this; }
        public ParamBuilder withBeta(DateTime beta) { this.beta = beta; return this; }
        public ParamBuilder withMickey(string m) { this.mickey = m; return this; }
        public ParamBuilder withMouse(long m) { this.mouse = m; return this; }
    }

    class Param1 : IParam {
        public string One { private set; get; }
        public string Two { private set; get; }

        public int Alpha { get {throw new NotImplementedException();} }
        public DateTime Beta { get {throw new NotImplementedException();} }
        public string Mickey { get {throw new NotImplementedException();} }
        public long Mouse { get {throw new NotImplementedException();} }

        public Param1(string one, string two) {
            One = one; Two = two;
        }
    }

    class Param2 : IParam {
        public int Alpha { private set; get; }
        public DateTime Beta { private set; get; }

        public string One { get {throw new NotImplementedException();} }
        public string Two { get {throw new NotImplementedException();} }
        public string Mickey { get {throw new NotImplementedException();} }
        public long Mouse { get {throw new NotImplementedException();} }

        public Param2(int alpha, DateTime beta) {
            Alpha = alpha; Beta = beta;
        }
    }

    class Param3 : IParam {
        public string Mickey { private set; get; }
        public long Mouse { private set; get; }

        public string One { get {throw new NotImplementedException();} }
        public string Two { get {throw new NotImplementedException();} }
        public int Alpha { get {throw new NotImplementedException();} }
        public DateTime Beta { get {throw new NotImplementedException();} }

        public Param3(string mi, long mo) {
            Mickey = mi; Mouse = mo;
        }
    }
}
    
risposta data 07.04.2017 - 11:12
fonte

Leggi altre domande sui tag