Mi sono imbattuto in qualcosa che trovo decisamente frustrante aggiungendo nuove funzionalità al nostro ampio code base esistente.
Prefazione
Abbiamo una varietà di classi (ItemA, ItemB, ItemC ...) che ereditano da una classe base (TheBaseClass). Diremo che ci sono circa 10 classi di oggetti. Tratteniamo una raccolta di istanze di oggetti misti di magnitudini da poche centinaia a poche migliaia.
List<TheBaseClass> items = new List<TheBaseClass>()
Questo elenco è l'output di un passaggio precedente che assembla gli elementi in base ad alcuni dati di input, di solito alcuni elementi di pattern factory in corso per ottenere l'Item. Quindi prendiamo questo elenco e passiamo attraverso una serie di fasi di elaborazione e calcolo. Alcuni di questi metodi modificano i valori sugli oggetti esistenti, altri modificano la raccolta stessa (di solito inserendo nuovi elementi in una varietà di condizioni, questo può includere look-ahead e behinds durante l'iterazione). I passaggi a volte agiscono semplicemente su TheBaseClass, ma molti eseguono l'iterazione attraverso l'individuazione di specifiche implementazioni di Item o agiscono su oggetti diversi in modi diversi.
Ecco un esempio di una fase del processo che agisce su tutti i tipi utilizzando un dizionario con i puntatori di funzione per ogni tipo:
public class ProcessingStep1()
{
Dictionary<Type, CalculateDelegate> lookupTable = new Dictionary<Type, CalculateDelegate>();
public ProcessingStep1()
{
lookupTable.Add(typeof(ItemA), ProcessItemA);
lookupTable.Add(typeof(ItemB), ProcessItemB);
// ...
}
public method CalculateAThing(IEnumerable<TheBaseClass> items)
{
foreach (TheBaseClass item in items)
{
// desired to throw an exception if the key is not found
CalculateDelegate calculate = lookupTable[item.GetType()];
}
}
private ProcessItemA(TheBaseClass item)
{
// need to cast to access a property from the derived type
var something = (item as ItemA).PropertyOnItemA;
item.BaseClassProperty = doMath(something);
}
private ProcessItemB(TheBaseClass item)
{
// no need to cast, but the behavior is different for ItemB than others
item.BaseClassProperty = somethingSpecific;
}
// ...
}
Separazione del codice dai dati
Questi passaggi di elaborazione variano in base alle impostazioni e agli input nel super processo, oltre a pochi "percorsi" definiti di passaggi da eseguire, i passaggi stessi possono essere implementazioni di un'interfaccia e possono variare singolarmente.
Questo, insieme al fatto che le fasi di elaborazione possono modificare la raccolta stessa, è un argomento strong per mantenere questi metodi come entità separate piuttosto che come membri dei tipi Item e TheBaseClass, come questo
public abstract class TheBaseClass
{
public abstract void CalculateAThing();
}
Le lotte
Ora ho dovuto aggiungere un nuovo elemento, ItemD. Se ItemD dovesse implementare un'interfaccia o metodi astratti, otterrei dei controlli del tempo compilati relativi al comportamento previsto di cui è richiesto un articolo. Tuttavia qui ho bisogno di fare affidamento sulla mia precedente conoscenza della nostra base di codice (se ce l'ho) o su un ciclo di sviluppo continuo di cercare di ottenere una collezione con ItemD presente attraverso tutti i rami possibili che ProcessingSteps può fare, facendo affidamento sulle eccezioni ( dal dizionario in alto) o da me catturando e identificando i bug di runtime dell'output.
Sono bloccato in questa architettura e nella base di codice, e non identificherò i miei altri limiti perché penso che limiterà la discussione, ma sto cercando come è gestito in altri domini o anche in funzioni lingue in modo tale da evitarlo in futuro se un nuovo design si stava dirigendo nella stessa direzione.