Alcuni sostengono che il Singleton Pattern è sempre un anti-pattern. Cosa ne pensi?
Alcuni sostengono che il Singleton Pattern è sempre un anti-pattern. Cosa ne pensi?
Risposte:
Le due principali critiche a Singleton rientrano in due campi da ciò che ho osservato:
Come risultato di entrambi, un approccio comune consiste nell'utilizzare la creazione di un oggetto container ampio per contenere una singola istanza di queste classi e solo l'oggetto container modifica questi tipi di classi mentre a molte altre classi può essere concesso l'accesso da cui utilizzare l'oggetto contenitore.
Sono d'accordo che si tratta di un anti-pattern. Perché? Perché consente al tuo codice di mentire sulle sue dipendenze e non puoi fidarti che altri programmatori non introducano uno stato mutabile nei tuoi singleton precedentemente immutabili.
Una classe potrebbe avere un costruttore che accetta solo una stringa, quindi pensi che sia istanziato in isolamento e non abbia effetti collaterali. Tuttavia, silenziosamente, sta comunicando con una sorta di oggetto singleton pubblico a livello globale, in modo che ogni volta che crei un'istanza della classe, contenga dati diversi. Questo è un grosso problema, non solo per gli utenti della tua API, ma anche per la testabilità del codice. Per testare correttamente il codice dell'unità, è necessario micro-gestire ed essere consapevoli dello stato globale nel singleton, per ottenere risultati di test coerenti.
Il modello Singleton è fondamentalmente solo una variabile globale inizializzata pigramente. Le variabili globali sono generalmente e giustamente considerate malvagie perché consentono un'azione spettrale a distanza tra parti di un programma apparentemente non correlate. Tuttavia, in IMHO non c'è nulla di sbagliato nelle variabili globali che vengono impostate una volta, da un punto, come parte della routine di inizializzazione di un programma (ad esempio, leggendo un file di configurazione o argomenti della riga di comando) e successivamente trattati come costanti. Tale uso delle variabili globali è diverso solo nella lettera, non nello spirito, dall'avere una costante nominata dichiarata al momento della compilazione.
Allo stesso modo, la mia opinione su Singletons è che sono cattivi se e solo se vengono utilizzati per passare lo stato mutevole tra parti apparentemente non correlate di un programma. Se non contengono uno stato mutabile o se lo stato mutabile che contengono è completamente incapsulato in modo che gli utenti dell'oggetto non debbano conoscerlo nemmeno in un ambiente multithread, non c'è nulla di sbagliato in loro.
Ho visto un certo numero di singoli nel mondo PHP. Non ricordo alcun caso d'uso in cui ho trovato il modello giustificato. Ma penso di avere un'idea della motivazione per cui la gente l'ha usata.
Singola istanza .
"Usa una singola istanza di classe C in tutta l'applicazione."
Questo è un requisito ragionevole, ad es. Per la "connessione al database predefinita". Ciò non significa che non creerai mai una seconda connessione db, ma significa che di solito lavori con quella predefinita.
Singola istanza .
"Non consentire l'istanza di classe C più di una volta (per processo, per richiesta, ecc.)."
Ciò è rilevante solo se l'istanza della classe avrebbe effetti collaterali che sono in conflitto con altre istanze.
Spesso questi conflitti possono essere evitati riprogettando il componente, ad esempio eliminando gli effetti collaterali dal costruttore della classe. Oppure possono essere risolti in altri modi. Ma potrebbero esserci ancora alcuni casi d'uso legittimi.
Dovresti anche pensare se il requisito "solo uno" significhi davvero "uno per processo". Ad esempio per la concorrenza delle risorse, il requisito è piuttosto "uno per intero sistema, tra processi" e non "uno per processo". E per altre cose è piuttosto per "contesto applicativo", e ti capita di avere un solo contesto applicativo per processo.
Altrimenti, non è necessario applicare questa ipotesi. Applicare ciò significa anche che non è possibile creare un'istanza separata per i test unitari.
Accesso globale.
Ciò è legittimo solo se non si dispone di un'infrastruttura adeguata per passare gli oggetti nel luogo in cui vengono utilizzati. Ciò potrebbe significare che il tuo framework o ambiente fa schifo, ma potrebbe non essere in tuo potere risolverlo.
Il prezzo è un accoppiamento stretto, dipendenze nascoste e tutto ciò che è negativo per lo stato globale. Ma probabilmente stai già soffrendo questi.
Istanza pigra.
Questa non è una parte necessaria di un singleton, ma sembra il modo più popolare per implementarli. Ma, sebbene l'istanza pigra sia una cosa carina da avere, non hai davvero bisogno di un singleton per raggiungerlo.
L'implementazione tipica è una classe con un costruttore privato, una variabile di istanza statica e un metodo getInstance () statico con istanza lazy.
Oltre ai problemi sopra menzionati, questo morde con il principio della singola responsabilità , perché la classe controlla il proprio ciclo di vita e istanza , oltre alle altre responsabilità che la classe ha già.
In molti casi, è possibile ottenere lo stesso risultato senza singleton e senza stato globale. Invece, dovresti usare l'iniezione di dipendenza e potresti prendere in considerazione un contenitore di iniezione di dipendenza .
Tuttavia, ci sono casi d'uso in cui rimangono i seguenti requisiti validi:
Quindi, ecco cosa puoi fare in questo caso:
Crea una classe C che desideri creare un'istanza, con un costruttore pubblico.
Creare una classe S separata con una variabile di istanza statica e un metodo S :: getInstance () statico con istanza lazy, che utilizzerà la classe C per l'istanza.
Elimina tutti gli effetti collaterali dal costruttore di C. Inserisci invece questi effetti collaterali nel metodo S :: getInstance ().
Se si dispone di più di una classe con i requisiti di cui sopra, è possibile considerare di gestire le istanze di classe con un contenitore di servizi locale di piccole dimensioni e utilizzare l'istanza statica solo per il contenitore. Quindi, S :: getContainer () ti darà un contenitore di servizi con istanza pigra e otterrai gli altri oggetti dal contenitore.
Evita di chiamare la getInstance () statica dove puoi. Utilizzare invece l'iniezione di dipendenza, quando possibile. In particolare, se si utilizza l'approccio contenitore con più oggetti che dipendono l'uno dall'altro, nessuno di questi dovrebbe chiamare S :: getContainer ().
Facoltativamente, creare un'interfaccia implementata dalla classe C e utilizzarla per documentare il valore di ritorno di S :: getInstance ().
(Lo chiamiamo ancora singleton? Lascio questo alla sezione dei commenti ..)
Benefici:
È possibile creare un'istanza separata di C per unit test, senza toccare alcuno stato globale.
La gestione delle istanze è separata dalla classe stessa -> separazione delle preoccupazioni, principio di responsabilità unica.
Sarebbe abbastanza facile lasciare che S :: getInstance () utilizzi una classe diversa per l'istanza, o addirittura determinare dinamicamente quale classe usare.
Personalmente userò i singleton quando avrò bisogno di 1, 2 o 3, o di un numero limitato di oggetti per la particolare classe in questione. Oppure desidero comunicare all'utente della mia classe che non voglio che vengano create più istanze della mia classe affinché funzioni correttamente.
Inoltre lo userò solo quando dovrò usarlo quasi ovunque nel mio codice e non voglio passare un oggetto come parametro a ogni classe o funzione che ne ha bisogno.
Inoltre userò un singleton solo se non romperà la trasparenza referenziale di un'altra funzione. Il significato dato un certo input produrrà sempre lo stesso output. Cioè non lo uso per lo stato globale. A meno che non sia possibile che lo stato globale sia inizializzato una volta e non sia mai cambiato.
Per quanto riguarda quando non usarlo, vedere i 3 precedenti e cambiarli nel contrario.