Un singolo oggetto di configurazione è una cattiva idea?


43

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 pensi? Quali sono alcune alternative? E come si avvia il refactoring di un'applicazione che utilizza la configurazione ovunque?


La configurazione ottiene le sue impostazioni leggendo un file sul disco, giusto? Quindi perché non avere semplicemente un file "test.config" che può leggere?
Anon.

Ciò risolve il primo problema, ma non il secondo.
JW01,

@ JW01: Tutti i tuoi test hanno bisogno di una configurazione nettamente diversa, allora? Dovrai impostare quella configurazione da qualche parte durante il test, vero?
Anon.

Vero. Ma se continuo a utilizzare un singolo pool di impostazioni (con un pool diverso per i test), tutti i miei test finiscono per utilizzare le stesse impostazioni. E poiché potrei voler testare il comportamento con impostazioni diverse, questo non è l'ideale.
JW01,

"Quindi in un test, devi impostare la configurazione per la classe da testare, ma anche TUTTE le sue dipendenze. Questo può rendere brutto il tuo codice di test." Hai un esempio? Ho la sensazione che non sia la struttura dell'intera applicazione che si collega alla classe di configurazione, ma piuttosto la struttura della stessa classe di configurazione che sta rendendo le cose "brutte". L'impostazione della configurazione di una classe non dovrebbe configurarne automaticamente le dipendenze?
AndrewKS,

Risposte:


35

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 i principi SOLID , in particolare il principio di segregazione dell'interfaccia e il principio di inversione di dipendenza .


7

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ò essere estratta da un DB o dalla configurazione dell'app o da quello che 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 voglio sempre 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 un rapido pollice verso la 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 praticità (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.


3

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 piacevole, 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 inestimabili trucchi 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 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, purché gli organizzatori lo facciano solo, ad esempio non contengano molto codice testabile. Ciò consente di testare la maggior parte o tutte le funzionalità testabili dell'unità dell'app, senza 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.


Mi è piaciuto molto il tuo approccio e non suggerirei che l'idea di un setter debba essere mantenuta "segreta" o considerata un "trucco rapido". Se accetti la posizione secondo cui i test unitari sono un utente / consumatore / parte importante della tua applicazione, avere test_attributi e metodi non è più una cosa negativa, giusto? Sono d'accordo sul fatto che la vera soluzione al problema sia quella di utilizzare un framework DI, ma in esempi come il tuo, quando si lavora con codice legacy o altri casi semplici, non alienare il codice di test si applica in modo pulito.
Filip Dupanović,

1
@kRON, questi sono trucchi immensamente utili e pratici per rendere testabile l'unità di codice legacy, e li sto usando anche ampiamente. Tuttavia, idealmente il codice di produzione non dovrebbe contenere alcuna funzionalità introdotta esclusivamente a scopo di test. A lungo termine, è qui che sto rifattorizzando.
Péter Török,

2

Nella mia esperienza, nel mondo Java, questo è lo scopo di Spring. Crea e gestisce oggetti (bean) in base a determinate proprietà impostate in fase di esecuzione ed è altrimenti trasparente per l'applicazione. Puoi andare con il file test.config che Anon. menziona oppure puoi includere una logica nel file di configurazione che Spring gestirà per impostare le proprietà in base a qualche altra chiave (ad esempio 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

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 il cui scarso testabilità.

Il modello Singleton è "considerato dannoso". Significato, non è sempre la cosa sbagliata, ma di solito lo è. Se stai pensando di utilizzare un modello singleton per qualsiasi cosa , 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 per questo motivo, 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.


0

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 che 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

La classe che stai descrivendo suona come un oggetto divino antipattern tranne che con i dati invece delle 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

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.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.