Questa è in realtà una domanda molto importante e spesso viene fatta in modo errato in quanto non viene data abbastanza importanza anche se è una parte fondamentale di praticamente ogni applicazione. Ecco le mie linee guida:
La tua classe di configurazione, che contiene tutte le impostazioni dovrebbe essere solo un semplice vecchio tipo di dati, struct / class:
class Config {
int prop1;
float prop2;
SubConfig subConfig;
}
Non dovrebbe avere bisogno di metodi e non dovrebbe comportare l'ereditarietà (a meno che non sia l'unica scelta che hai nella tua lingua per implementare un campo variante - vedi paragrafo successivo). Può e deve usare la composizione per raggruppare le impostazioni in classi di configurazione specifiche più piccole (ad es. SubConfig sopra). Se lo fai in questo modo, sarà l'ideale passare in unit test e l'applicazione in generale poiché avrà dipendenze minime.
Probabilmente dovrai utilizzare tipi di varianti, nel caso in cui le configurazioni per configurazioni diverse siano eterogenee nella struttura. È accettato che dovrai inserire un cast dinamico ad un certo punto quando leggi il valore per lanciarlo nella classe di (sotto) configurazione corretta, e senza dubbio questo dipenderà da un'altra impostazione di configurazione.
Non dovresti essere pigro nel digitare tutte le impostazioni come campi semplicemente facendo questo:
class Config {
Dictionary<string, string> values;
};
Questo è allettante perché significa che puoi scrivere una classe di serializzazione generalizzata che non ha bisogno di sapere con quali campi tratta, ma è sbagliata e ti spiego perché tra un momento.
La serializzazione della configurazione viene eseguita in una classe completamente separata. Qualunque sia l'API o la libreria utilizzata per eseguire questa operazione, il corpo della funzione di serializzazione dovrebbe contenere voci che sostanzialmente equivalgono a essere una mappa dal percorso / chiave nel file al campo sull'oggetto. Alcune lingue forniscono una buona introspezione e possono farlo per te immediatamente, altre dovresti scrivere esplicitamente la mappatura, ma la cosa fondamentale è che dovresti scrivere la mappatura una sola volta. Ad esempio, considera questo estratto che ho adattato dalla documentazione del parser delle opzioni del programma boost c ++:
struct Config {
int opt;
} conf;
po::options_description desc("Allowed options");
desc.add_options()
("optimization", po::value<int>(&conf.opt)->default_value(10);
Nota che l'ultima riga in pratica dice che "ottimizzazione" è mappata su Config :: opt e che esiste anche una dichiarazione del tipo che ti aspetti. Volete che la lettura della configurazione fallisca se il tipo non è quello che vi aspettate, se il parametro nel file non è realmente un float o un int, o non esiste. Vale a dire che l'errore dovrebbe verificarsi quando si legge il file perché il problema riguarda il formato / convalida del file e si dovrebbe lanciare un codice di eccezione / ritorno e segnalare il problema esatto. Non dovresti ritardare questo a più tardi nel programma. Ecco perché non dovresti essere tentato di catturare tutte le Conf stile dizionario come menzionato sopra che non falliranno quando il file viene letto - poiché il cast viene ritardato fino a quando non è necessario il valore.
Dovresti rendere la classe Config di sola lettura in qualche modo - impostando il contenuto della classe una volta quando la crei e inizializzi dal file. Se hai bisogno di avere impostazioni dinamiche nella tua applicazione che cambiano, così come quelle costanti che non lo fanno, dovresti avere una classe separata per gestire quelle dinamiche piuttosto che cercare di permettere ai bit della tua classe di configurazione di non essere di sola lettura .
Idealmente, leggi il file in un punto del programma, ad esempio hai solo un'istanza di un " ConfigReader
". Tuttavia, se stai lottando per far passare l'istanza di Config nel punto in cui ti serve, è meglio avere un secondo ConfigReader piuttosto che introdurre una configurazione globale (che suppongo sia ciò che l'OP intende con "statico" "), che mi porta al prossimo punto:
Evita la seducente canzone della sirena del singleton: "Ti risparmierò a passare quella classe in classe, tutti i tuoi costruttori saranno adorabili e puliti. Dai, sarà così facile." La verità è con un'architettura testabile ben progettata che difficilmente sarà necessario passare la classe Config, o parti di essa attraverso quelle classi dell'applicazione. Quello che troverai, nella tua classe di livello superiore, la tua funzione main () o qualsiasi altra cosa, spiegherai la conf in singoli valori, che fornirai alle tue classi di componenti come argomenti che poi rimetterai insieme (dipendenza manuale iniezione). Una configurazione singleton / globale / statica renderà molto più difficile l'implementazione e la comprensione dell'unità testare la tua applicazione, ad esempio confonderà i nuovi sviluppatori del tuo team che non sapranno che devono impostare lo stato globale per testare le cose.
Se la tua lingua supporta le proprietà, dovresti usarle per questo scopo. Il motivo è che sarà molto semplice aggiungere impostazioni di configurazione "derivate" che dipendono da una o più altre impostazioni. per esempio
int Prop1 { get; }
int Prop2 { get; }
int Prop3 { get { return Prop1*Prop2; }
Se la tua lingua non supporta in modo nativo il linguaggio delle proprietà, potrebbe avere una soluzione alternativa per ottenere lo stesso effetto, oppure puoi semplicemente creare una classe wrapper che fornisca le impostazioni del bonus. Se non puoi altrimenti conferire il vantaggio delle proprietà, è altrimenti una perdita di tempo scrivere manualmente e usare getter / setter semplicemente allo scopo di far piacere a qualche dio-dio. Starai meglio con un semplice vecchio campo.
Potrebbe essere necessario un sistema per unire e prendere più configurazioni da luoghi diversi in ordine di precedenza. Tale ordine di precedenza deve essere ben definito e compreso da tutti gli sviluppatori / utenti, ad esempio prendere in considerazione il registro di Windows HKEY_CURRENT_USER / HKEY_LOCAL_MACHINE. Dovresti fare questo stile funzionale in modo da poter mantenere le tue configurazioni di sola lettura cioè:
final_conf = merge(user_conf, machine_conf)
piuttosto che:
conf.update(user_conf)
Dovrei infine aggiungere che, naturalmente, se il framework / linguaggio scelto fornisce i propri meccanismi di configurazione incorporati e ben noti, è necessario considerare i vantaggi dell'utilizzo di questo invece di implementare il proprio.
Così. Molti aspetti da considerare: farlo bene e influenzerà profondamente l'architettura dell'applicazione, riducendo i bug, rendendo le cose facilmente testabili e costringendoti a utilizzare un buon design altrove.