Un singolo oggetto di configurazione è una cattiva idea?

41

Nella maggior parte delle mie applicazioni, ho un oggetto "config" singleton o statico, incaricato di leggere varie impostazioni dal disco. Quasi tutte le classi lo usano, per vari scopi. Essenzialmente è solo una tabella hash di coppie nome / valore. È di sola lettura, quindi non mi sono preoccupato troppo del fatto che ho così tanto stato globale. Ma ora che ho iniziato con i test delle unità, sta iniziando a diventare un problema.

Un problema è che di solito non si desidera testare con la stessa configurazione con cui si esegue. Ci sono un paio di soluzioni a questo:

  • Dai all'oggetto di configurazione un setter che viene utilizzato SOLO per il test, in modo che tu possa passare a impostazioni diverse.
  • Continua a utilizzare un singolo oggetto di configurazione, ma modificalo da un singleton a un'istanza che passi ovunque sia necessario. Quindi puoi costruirlo una volta nella tua applicazione e una volta nei tuoi test, con impostazioni diverse.

Ma in ogni caso, ti rimane ancora un secondo problema: quasi tutte le classi possono usare l'oggetto config. Quindi, in un test, è necessario impostare la configurazione per la classe sottoposta a test, ma anche TUTTE le sue dipendenze. Questo può rendere brutto il tuo codice di test.

Sto iniziando a concludere che questo tipo di oggetto di configurazione è una cattiva idea. Cosa pensi? Quali sono alcune alternative? E come si avvia il refactoring di un'applicazione che usa la configurazione ovunque?

    
posta JW01 26.01.2011 - 21:09
fonte

8 risposte

32

Non ho problemi con un singolo oggetto di configurazione, e posso vedere il vantaggio nel mantenere tutte le impostazioni in un unico posto.

Tuttavia, l'uso di quel singolo oggetto ovunque comporterà un alto livello di accoppiamento tra l'oggetto config e le classi che lo usano. Se hai bisogno di cambiare la classe di configurazione, potresti dover visitare ogni istanza in cui viene utilizzata e controllare di non aver infranto nulla.

Un modo per affrontarlo consiste nel creare più interfacce che espongano parti dell'oggetto config necessarie per diverse parti dell'app. Piuttosto che consentire ad altre classi di accedere all'oggetto config, si passano istanze di un'interfaccia alle classi che ne hanno bisogno. In questo modo, le parti dell'app che usano config dipendono dalla raccolta più piccola di campi nell'interfaccia piuttosto che dall'intera classe di configurazione. Infine, per il test delle unità è possibile creare una classe personalizzata che implementa l'interfaccia.

Se vuoi approfondire ulteriormente queste idee, ti consiglio di leggere su SOLID principles , in particolare l'interfaccia Principio di segregazione e il principio di inversione delle dipendenze .

    
risposta data 26.01.2011 - 21:34
fonte
6

Segrego gruppi di impostazioni correlate con interfacce.

Qualcosa come:

public interface INotificationEmailSettings {
   public string To { get; set; }
}

public interface IMediaFileSettings {
    public string BasePath { get; set; }
}

ecc.

Ora, per un ambiente reale, una singola classe implementerà molte di queste interfacce. Quella classe può prelevare da un DB o dalla configurazione dell'app o cosa hai, ma spesso sa come ottenere la maggior parte delle impostazioni.

Ma la separazione per interfaccia rende le dipendenze reali molto più esplicite e a grana fine, e questo è un evidente miglioramento per la testabilità.

Ma ... Non sempre voglio essere forzato ad iniettare o fornire le impostazioni come dipendenza. C'è un argomento che potrebbe essere fatto che dovrei, per coerenza o chiarezza. Ma nella vita reale sembra una restrizione inutile.

Quindi, userò una classe statica come facciata, fornendo un facile accesso da qualsiasi posizione alle impostazioni, e all'interno di quella classe statica cercherò di localizzare l'implementazione dell'interfaccia e ottenere le impostazioni.

So che la posizione del servizio si riduce di molto, ma ammettiamolo, fornire una dipendenza attraverso un costruttore è un peso, ea volte quel peso è più di quanto tengo a sopportare. Service Location è la soluzione per mantenere la testabilità attraverso la programmazione di interfacce e consentire molteplici implementazioni pur fornendo la comodità (in situazioni attentamente misurate e in modo appropriato poche) di un entry point statico single.

public static class AllSettings {
    public INotificationEmailSettings NotificationEmailSettings {
        get {
            return ServiceLocator.Get<INotificationEmailSettings>();
        }
    }
}

Trovo che questo mix sia il migliore di tutti i mondi.

    
risposta data 26.01.2011 - 21:31
fonte
3

Sì, come hai capito, un oggetto di configurazione globale rende difficile il test delle unità. Avere un setter "segreto" per il test delle unità è un trucco rapido, che sebbene non sia piacevole, può essere molto utile: consente di iniziare a scrivere test unitari, in modo da poter ridefinire il proprio codice con un design migliore nel tempo.

(Riferimento obbligatorio: Funzionante in modo efficace con codice legacy . Contiene questo e molti altri trucchi inestimabili per il test delle unità codice legacy.)

Nella mia esperienza, la cosa migliore è avere meno dipendenze sulla configurazione globale possibile. Il che non è necessariamente zero, dipende tutto dalle circostanze. Avere poche classi di "organizzatore" di alto livello che accedono alla configurazione globale e trasmettono le proprietà di configurazione effettive agli oggetti che creano e usano possono funzionare bene, a condizione che gli organizzatori facciano solo questo, ad es. non contengono molto codice testabile Ciò consente di testare la maggior parte o tutte le importanti funzionalità testabili dell'unità dell'app, senza tuttavia interrompere completamente lo schema di configurazione fisica esistente.

Questo si sta già avvicinando ad una vera soluzione per l'iniezione delle dipendenze, però, come Spring for Java. Se riesci a migrare verso un simile framework, bene. Ma nella vita reale, e specialmente quando si tratta di un'app legacy, spesso il lento e meticoloso refactoring verso DI è il miglior compromesso raggiungibile.

    
risposta data 26.01.2011 - 21:24
fonte
2

Nella mia esperienza, nel mondo di Java, questo è ciò per cui è destinata la primavera. Crea e gestisce oggetti (bean) basati su determinate proprietà impostate in fase di runtime ed è altrimenti trasparente per l'applicazione. Puoi andare con il file test.config che Anon. menziona o puoi includere alcune logiche nel file di configurazione che Spring gestirà per impostare le proprietà in base ad altre chiavi (ad es. nome host o ambiente).

Per quanto riguarda il tuo secondo problema, puoi aggirare il problema con qualche rearchitecture ma non è così grave come sembra. Ciò significa che nel tuo caso non dovresti, ad esempio, avere un oggetto Config globale utilizzato da varie altre classi. Dovresti semplicemente configurare quelle altre classi tramite Spring e quindi non avere alcun oggetto config perché tutto ciò che necessitava di configurazione l'ha ottenuto tramite Spring, quindi puoi usare quelle classi direttamente nel tuo codice.

    
risposta data 26.01.2011 - 21:21
fonte
2

Questa domanda riguarda davvero un problema più generale della configurazione. Non appena ho letto la parola "singleton" ho immediatamente pensato a tutti i problemi associati a quel modello, tra cui la scarsa testabilità.

Il modello Singleton è "considerato dannoso". Significa che non è sempre la cosa sbagliata, ma è di solito . Se stai pensando di utilizzare un modello singleton per qualsiasi , smetti di considerare:

  • Avrai mai bisogno di sottoclassi?
  • Avrai mai bisogno di programmare su un'interfaccia?
  • Hai bisogno di eseguire test unitari contro di esso?
  • Dovrai modificarlo frequentemente?
  • La tua applicazione ha lo scopo di supportare più piattaforme / ambienti di produzione?
  • Sei anche un po 'preoccupato per l'utilizzo della memoria?

Se la tua risposta è "sì" a qualcuno di questi (e probabilmente a molte altre cose a cui non pensavo), probabilmente non vuoi usare un singleton. Le configurazioni spesso richiedono molta più flessibilità di quella che può fornire un singleton (o comunque una classe instanceless).

Se vuoi quasi tutti i benefici di un singleton senza alcun dolore, usa una struttura di dipendenze come Spring o Castle o qualsiasi altra cosa disponibile per il tuo ambiente. In questo modo devi solo dichiararlo una volta sola e il contenitore fornirà automaticamente un'istanza per qualsiasi necessità.

    
risposta data 26.01.2011 - 21:27
fonte
0

Un modo in cui ho gestito questo in C # è di avere un oggetto singleton che si blocca durante la creazione dell'istanza e inizializza tutti i dati contemporaneamente per l'utilizzo da parte di tutti gli oggetti client. Se questo singleton è in grado di gestire coppie chiave / valori, è possibile memorizzare qualsiasi tipo di dati, comprese le chiavi complesse da utilizzare da molti client e tipi di client diversi. Se si desidera mantenerlo dinamico e caricare i nuovi dati del client secondo necessità, è possibile verificare l'esistenza di una chiave principale del client e, se mancante, caricare i dati per quel client, aggiungendolo al dizionario principale. È anche possibile che il config singleton primario contenga insiemi di dati client in cui multipli dello stesso tipo di client utilizzano gli stessi dati a cui tutti hanno accesso tramite il config singleton primario. Gran parte di questa organizzazione di oggetti di configurazione può dipendere dal modo in cui i client di configurazione devono accedere a tali informazioni e se tali informazioni sono statiche o dinamiche. Ho usato sia le configurazioni chiave / valore sia le API specifiche degli oggetti a seconda di quale era necessario.

Uno dei miei singlet di configurazione può ricevere messaggi da ricaricare da un database. In questo caso, carico in una seconda raccolta di oggetti e blocca solo la raccolta principale per scambiare le raccolte. Questo impedisce il blocco delle letture su altri thread.

Se si carica da file di configurazione, è possibile avere una gerarchia di file. Le impostazioni in un file possono specificare quale degli altri file caricare. Ho usato questo meccanismo con i servizi Windows C # che hanno più componenti opzionali, ognuno con le proprie impostazioni di configurazione. I pattern della struttura dei file erano gli stessi tra i file, ma erano caricati o meno in base alle impostazioni del file principale.

    
risposta data 26.01.2011 - 21:49
fonte
0

La classe che stai descrivendo sembra un God object antipattern tranne che con i dati anziché le funzioni. Questo è un problema Probabilmente dovresti leggere e archiviare i dati di configurazione nell'oggetto appropriato mentre leggi di nuovo i dati solo se è necessario per qualche motivo.

Inoltre stai usando un Singleton per una ragione che non è appropriata. I single possono essere usati solo quando l'esistenza di più oggetti creerà uno stato negativo. L'uso di un Singleton in questo caso è inappropriato poiché avere più di un lettore di configurazione non dovrebbe causare immediatamente uno stato di errore. Se ne hai più di uno, probabilmente stai sbagliando, ma non è assolutamente necessario che tu abbia solo un lettore di configurazione.

Infine, la creazione di uno stato globale di questo tipo costituisce una violazione dell'incapsulamento poiché consente più classi rispetto alla necessità di conoscere l'accesso diretto ai dati.

    
risposta data 26.01.2011 - 22:27
fonte
0

Penso che se ci sono molte impostazioni, con "blocchi" di quelli correlati, ha senso dividerli in interfacce separate con le rispettive implementazioni - quindi iniettare quelle interfacce dove necessario con DI. Ottieni testabilità, accoppiamento inferiore e SRP.

Sono stato ispirato a questo riguardo da Joshua Flanagan di Los Techies; ha scritto un articolo sul importa qualche tempo fa.

    
risposta data 26.01.2011 - 23:06
fonte

Leggi altre domande sui tag