Estendere un progetto polimorfico già esistente oltre le aspettative iniziali

0

Sono un po 'intrappolato nel mio progetto. Devo estenderlo ben oltre le aspettative iniziali.

In primo luogo mostrerò come stanno le cose adesso e quali comportamenti voglio aggiungere ma non so come.

Attualmente, il seguente codice funziona correttamente. Alcune cose possono essere discusse (come la classe dei costruttori, che esiste per il Principio di Responsabilità Unica, ma forse non è il modo più intelligente per farlo, o la strana soluzione di covarianza del tipo di ritorno). Scusa per il muro di codice in arrivo, ho provato il più possibile per mostrarti il codice minimo per capire il problema.

public class QueryManager
{
    // filled with different instances of Query in constructor depending on parameters
    public List<Query> QueryList {get; set;} 

    public List<IRecords> RecordsList { get; set; }

    public virtual void Queries()
    {
        foreach (var query in QueryList)
        {       
            query.ExecutePreparedQuery();
            var listResult = query.GetRecordsFromResults();

            // i'm aware of the possibility of NullArgumentException here, 
            // i just simplify the actual thing
            RecordsList.AddRange(listResult); 
        }
    }
}


public abstract class Query
{
    public FrameworkTypeHoldingResults Result { get; protected set; }

    // Workaround for C# return type covariance.
    protected virtual IRecordBuilder RecordBuilder
    {
       get { return GetRecordBuilder(); }
    }
    protected abstract IRecordBuilder GetRecordBuilder();



    protected WebObject Web {get; set;}

    public virtual void ExecutePreparedQuery()
    {
        var query = new FrameworkTypeQuery { ... }
        Result = FrameworkType.GetItems(query)
    }

    protected virtual string DateFormatting() { ... }

    public abstract IList<IRecords> GetRecordsFromResults();
    public abstract string QueryDescription();
}


public class RealQuery : Query
{
    protected override IRecordBuilder GetRecordBuilder()
    {
        return RecordBuilder;
    }
    protected new ConcreteRecordBuilderA RecordBuilder { get; set; }

    public override string QueryDescription() => "I'm a RealQuery";

    public override IList<IRecords> GetRecordsFromResults()
    {
        var recordsList = new List<IRecords>();

            if (Result != null && Result.Count != 0)
            {
                RecordBuilder.Value = Result.Count;
                RecordBuilder.Querytype = QueryDescription();
                RecordBuilder.Dateformatted = DateFormatting();
                RecordBuilder.WebTitle = Web.Title;

                recordsList.Add(RecordBuilder.BuildRecord());

                return recordsList;
            }

            RecordBuilder.Value = 0;
            RecordBuilder.Querytype = QueryDescription();
            RecordBuilder.Dateformatted = DateFormatting();
            RecordBuilder.WebTitle = Web.Title;

            recordsList.Add(RecordBuilder.BuildRecord());
            return recordsList;
    }
}

public interface IRecords{}

public interface IRecordBuilder
{
    IRecords BuildRecord();
}

public class RecordsA 
{
    public RecordsA(int? Value, string QueryType, string Type, string Month) {...}
    public int? Value {get; private set;}
    public string QueryType {get; private set;}
    public string Web {get; private set;}

    public string Month {get; private set;}
}

public class RecordsB 
{
    public RecordsB(int? Value, string QueryType, string Type, string Month) {...}
    public int? Value {get; private set;}
    public string QueryType {get; private set;}
    public string Web {get; private set;}

    public string User {get; private set;}
}

public class RecordABuilder
{
    public int? Value {get; set;}
    public string QueryType {get; set;}
    public string Web {get; set;}

    public string Month {get; set;}

    public IRecord BuildRecord() => new RecordA(Value, QueryType, Type, Month)
}

public class RecordBBuilder
{
   // pretty much the same logic
}

Possiamo notare che sto solo utilizzando i metadati del risultato della mia query e non i risultati della query stessi.

Ora ecco il mio vero problema. Ho bisogno di un nuovo tipo di query. Il problema con questa query, a differenza del precedente, è che non conosco la natura degli IRecords che otterrò. Ti mostrerò dove interroga i suoi dati.

Web  | QueryType | Value | Month       | User
foo  | B type    |    42 | April 2020  |
bar  | A type    |   777 |             | "John Doe"

Come vedi qui, quando il mese è pieno, l'utente non è (ans il contrario). Se la query ottiene la prima riga, avrò bisogno di un'istanza RecordA, ma se la query ottiene il secondo, avrò bisogno di un'istanza RecordB. La query, questa volta, utilizza direttamente i risultati della query e la classe non sa cosa otterrà. Il problema con if-condition è che se vengono aggiunte più colonne e più IRecords, se sarà grande un albero gigante di if-condition.

Finora, questa è l'implementazione GetRecordsFromResults che ho, ma funziona solo con un'implementazione di IRecords, non con entrambi:

public override IList<IRecords> GetRecordsFromResults()
{
    var recordsList = new List<IRecords>();

    if (Result != null && Result.Count != 0)
    {
        for (var index = 0; index < Result.Count; index++)
        {
            var it = Result[index];

            RecordBuilder.Value = int.Parse(it["Value"].ToString());
            RecordBuilder.Querytype = it["QueryType"] as string;

            // But maybe i want the User column instead...
            RecordBuilder.Month= it["Month"] as string; 
            RecordBuilder.WebTitle = it["Web"] as string;

            recordsList.Add(RecordBuilder.BuildRecord());

            // would a  RecordsBBuilder in some cases
            RecordBuilder = new RecordsABuilder(); 
        }
        return recordsList;
    }

    RecordBuilder.Value = 0;
    RecordBuilder.Querytype = QueryDescription();

    // But maybe i want the User column instead...
    RecordBuilder.Month= DateFormatting(); 
    RecordBuilder.Web = Web.Title;

    recordsList.Add(RecordBuilder.BuildRecord());

    // would a  RecordsBBuilder in some cases
    RecordBuilder = new RecordsPerMonthBuilder(); 

    return recordsList;
}

Sono un po 'bloccato qui, hai qualche idea su come posso implementare questo nuovo tipo di Query concreto?

EDIT: non ho chiarito che la relazione tra Query implementazione e IRecords non è una per una. RecordA è per una famiglia di implementazioni Query (identificate da una sottoclasse astratta tra Query e le sue implementazioni) e RecordB per implementazioni dirette (senza sottoclassi intermedie). Potrebbero esserci anche RecordC in futuro. Ogni implementazione di Query ha il proprio metodo di descrizione che viene utilizzato per riempire la proprietà QueryType in IRecords implementazioni.

    
posta Ythio Csi 18.05.2017 - 18:30
fonte

1 risposta

2

Penso che invece di RecordABuilder hai bisogno di un RecordBuilder con codice un po 'come questo:

public IRecord BuildRecord() => QueryType == "A" ? new RecordA(Value, QueryType, Type, Month) 
                                                 : new RecordB(Value, QueryType, Type, Month);

Se si pianificano più di due tipi di record, l'operatore ternario non lo farà. Lambdas in nostro soccorso:

public class RecordBuilder
{
    public int? Value {get; set;}
    public string QueryType {get; set;}
    public string Web {get; set;}
    public string Month {get; set;}

    private readonly Dictionary<
        string, 
        Func<int, string, string, string, IRecord>> activators = 

        new Dictionary<
        string, 
        Func<int, string, string, string, IRecord>>();

    public RecordBuilder()
    {
        activators.Add("A",(v,q,t,m) => new RecordA(v, q, t, m));
        activators.Add("B",(v,q,t,m) => new RecordB(v, q, t, m));
        activators.Add("C",(v,q,t,m) => new RecordC(v, q, t, m));
    }

    public IRecord BuildRecord() => activators[QueryType](
        Value, 
        QueryType, 
        Type, 
        Month);
}

Ora sorge la domanda: cosa succede se i costruttori per RecordA e RecordB sono diversi? Possiamo adattarlo popolando gli argomenti del costruttore dalle chiusure anziché gli argomenti al lambda, in questo modo:

public class RecordBuilder
{
    public int? Value {get; set;}
    public string QueryType {get; set;}
    public string Web {get; set;}
    public string Month {get; set;}

    private readonly Dictionary<string, Func<IRecord>> activators = 
                 new Dictionary<string, Func<IRecord>>();

    public RecordBuilder()
    {
        activators.Add("A",() => new RecordA(Value, QueryType, Web, Month));
        activators.Add("B",() => new RecordB(Value, QueryType));
        activators.Add("C",() => new RecordC(Value, QueryType, Month));
    }

    public IRecord BuildRecord() => activators[QueryType]();
}

Nelle espressioni lambda di cui sopra, la chiamata a new trarrà le sue variabili dai campi nell'ambito della classe. Le variabili saranno racchiuse in una chiusura così che lambdas otterrà sempre il valore corrente quando eseguito.

E ovviamente potresti sempre usare un caso / interruttore. Anche se questo sembra un po 'poco elegante, è un modo perfettamente ragionevole per andare in una fabbrica.

public class RecordBuilder
{
    public int? Value {get; set;}
    public string QueryType {get; set;}
    public string Web {get; set;}
    public string Month {get; set;}

    public IRecord BuildRecord()
    {
        switch (QueryType)
        {
            case "A": return new RecordA(Value, QueryType, Web, Month);
            case "B": return new RecordB(Value, QueryType);
            case "C": return new RecordC(Value, QueryType, Month);
            default: throw new InvalidOperationException();
        }
    }
}

Si noti che quanto sopra richiede alla fabbrica di sapere come mappare una stringa contenente un tipo di query a un'istanza concreta. Questo è comune e consueto per una fabbrica. Ma, se sei veramente sposato con l'idea che la fabbrica non dovrebbe sapere in anticipo quali tipi sono disponibili o come costruirli, potresti fare qualcosa con la riflessione per ottenere un inventario di tutti i tipi che implementano IRecord e usa un attributo per determinare quale classe va con quale codice. Per affrontare il problema di un costruttore di variabili, puoi semplicemente passare il builder stesso come argomento del costruttore e lasciare che la classe concreta tiri tutto ciò di cui ha bisogno.

Questo è un po 'più complicato, ma qui va:

class RecordTypeAttribute : System.Attribute
{
    public string QueryType { get; private set; }
    public RecordTypeAttribute(string queryType)
    {
        QueryType = queryType;
    }
}
interface IRecord
{
    string QueryType { get; }
}

[RecordType("A")]
class RecordA : IRecord
{
    public string QueryType { get; private set; }

    public RecordA(RecordBuilder dataSource)
    {
        QueryType = dataSource.QueryType;
    }
}

[RecordType("B")]
class RecordB : IRecord
{
    public string QueryType { get; private set; }
    public int? Value { get; set; }

    public RecordB(RecordBuilder dataSource)
    {
        QueryType = dataSource.QueryType;
        Value = dataSource.Value;
    }
}

class RecordBuilder
{
    public string QueryType { get; set; }

    private Dictionary<string,Type> GetAvailableRecordTypes()
    {
        var list = new Dictionary<string, Type>();
        foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies())
        {
            foreach (Type t in asm.GetExportedTypes())
            {
                if (typeof(IRecord).IsAssignableFrom(t))
                {
                    RecordTypeAttribute a = System.Attribute.GetCustomAttributes(t).OfType<RecordTypeAttribute>().FirstOrDefault();
                    if (a != null)
                    {
                        list.Add(a.QueryType, t);
                    }
                }
            }
        }
        return list;
    }
    public IRecord BuildRecord()
    {
        var typesAvailable = GetAvailableRecordTypes();
        var type = typesAvailable[QueryType];
        return System.Activator.CreateInstance(type, new [] { this }) as IRecord;
    }
}

Quanto sopra è eccessivo nella maggior parte dei casi, anche se è un modello accurato quando si desidera un'architettura collegabile.

Nota: questa classe dovrebbe probabilmente essere chiamata RecordFactory invece di RecordBuilder in quanto non segue il modello del builder ed è praticamente una fabbrica. Una volta realizzato il misnaming, la domanda su dove implementare la logica che decide quale record creare viene immediatamente risolta.

    
risposta data 18.05.2017 - 19:28
fonte

Leggi altre domande sui tag