it-swarm.it

Un singolo oggetto di configurazione è una cattiva idea?

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 di avere così tanto stato globale. Ma ora che sto iniziando con i test unitari, sta iniziando a diventare un problema.

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

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

Ma in entrambi i casi, 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 da testare, ma anche TUTTE le sue dipendenze. Questo può rendere brutto il tuo codice di test.

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

45
JW01

Non ho problemi con un singolo oggetto di configurazione e posso vedere il vantaggio di 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 utilizzano. Se è necessario modificare la classe di configurazione, potrebbe essere necessario visitare tutte le istanze in cui viene utilizzato e verificare di non aver rotto nulla.

Un modo per gestirlo è creare più interfacce che espongano parti dell'oggetto di configurazione che sono necessarie a diverse parti dell'app. Invece di 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 utilizzano la configurazione dipendono dalla raccolta più piccola di campi nell'interfaccia piuttosto che dall'intera classe di configurazione. Infine, per unit test è possibile creare una classe personalizzata che implementa l'interfaccia.

Se vuoi approfondire ulteriormente queste idee, ti consiglio di leggere SOLIDO principi , in particolare Principio di segregazione dell'interfaccia e principio di inversione di dipendenza .

39
Kramii

Separare gruppi di impostazioni correlate con le interfacce.

Qualcosa di simile a:

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

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

eccetera.

Ora, per un ambiente reale, una singola classe implementerà molte di queste interfacce. Quella classe può estrarre 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 effettive molto più esplicite e ben definite, e questo è un evidente miglioramento per la testabilità.

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

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

So che l'ubicazione del servizio ottiene una rapida riduzione dalla maggior parte, ma ammettiamolo, fornire una dipendenza attraverso un costruttore è un peso, e talvolta quel peso è più di quello che mi interessa sopportare. Service Location è la soluzione per mantenere la testabilità attraverso la programmazione alle interfacce e consentendo molteplici implementazioni pur offrendo la comodità (in situazioni attentamente misurate e opportunamente poche) di un punto di ingresso singleton statico.

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

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

8
quentin-starin

Sì, come hai capito, un oggetto di configurazione globale rende difficile il test di unità. Avere un setter "segreto" per test unitari è un trucco rapido, che sebbene non sia bello, può essere molto utile: ti consente di iniziare a scrivere test unit, in modo da poter refactificare il tuo codice verso una progettazione migliore nel tempo.

(Riferimento obbligatorio: Lavorare in modo efficace con il codice legacy . Contiene questo e molti altri trucchi preziosi per il codice legacy di unit test.

Nella mia esperienza, il migliore è avere il minor numero possibile di dipendenze dalla configurazione globale. Il che non è necessariamente zero però - tutto dipende dalle circostanze. Avere alcune classi "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, purché gli organizzatori facciano solo ciò, ad es. non contengono molto codice testabile. Ciò consente di testare la maggior parte o tutte le funzionalità testabili dell'unità importanti dell'app, senza tuttavia interrompere completamente lo schema di configurazione fisica esistente.

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

4
Péter Török

Nella mia esperienza, nel mondo Java, questo è lo scopo di Spring. Crea e gestisce oggetti (bean) basati su determinate proprietà impostate in fase di esecuzione ed è altrimenti trasparente per il tuo Puoi usare il file test.config menzionato da Anon oppure puoi includere una logica nel file di configurazione che Spring gestirà per impostare le proprietà in base a qualche altra chiave (es. nome host o ambiente).

Per quanto riguarda il tuo secondo problema, puoi risolverlo con qualche ricerca, ma non è così grave come sembra. Ciò significa che, nel tuo caso, non avresti, ad esempio, un oggetto Config globale utilizzato da varie altre classi. Dovresti semplicemente configurare quelle altre classi attraverso Spring e quindi non avere alcun oggetto di configurazione perché tutto ciò che aveva bisogno di configurazione lo ha ottenuto attraverso Spring, quindi puoi usare quelle classi direttamente nel tuo codice.

2
Justin Beal

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, non ultimo dei quali è scarsa testabilità.

Il modello Singleton è "considerato dannoso". Significato, non è sempre la cosa sbagliata, ma di solito lo è. Se hai mai considerato di usare un modello singleton per nulla, smetti di considerare:

  • Avrai mai bisogno di sottoclassarlo?
  • Avrai mai bisogno di programmare su un'interfaccia?
  • Devi eseguire test unitari contro di essa?
  • Dovrai modificarlo frequentemente?
  • La tua applicazione è destinata a supportare più piattaforme/ambienti di produzione?
  • Sei anche un po 'preoccupato per l'utilizzo della memoria?

Se la tua risposta è "sì" a una di queste (e probabilmente a molte altre cose a cui non ho pensato), probabilmente non vorrai usare un singleton. Le configurazioni richiedono spesso molta più flessibilità di quella che può fornire un singleton (o, del resto, qualsiasi classe senza istanza).

Se vuoi quasi tutti i vantaggi di un singleton senza alcun dolore, usa un framework di iniezione di dipendenza come Spring o Castle o qualsiasi cosa sia disponibile per il tuo ambiente. In questo modo devi sempre dichiararlo una sola volta e il contenitore fornirà automaticamente un'istanza a qualsiasi cosa ne abbia bisogno.

2
Aaronaught

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

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

Se si carica da file di configurazione, è possibile disporre di 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 modelli di struttura dei file erano gli stessi tra i file, ma venivano caricati o meno in base alle impostazioni del file primario.

0
Tim Butterfield

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

Inoltre stai usando un Singleton per un motivo che non è appropriato. I singleton dovrebbero essere usati solo quando l'esistenza di più oggetti creerà un cattivo stato. L'uso di un Singleton in questo caso non è appropriato 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 così globale è una violazione dell'incapsulamento poiché stai consentendo a più classi di quelle che devono conoscere l'accesso diretto ai dati.

0
indyK1ng

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 su questo argomento da Joshua Flanagan di Los Techies; ha scritto un articolo sull'argomento qualche tempo fa.

0
Grant Palin