Quindi i Singleton sono cattivi, e allora?


554

Ultimamente si è discusso molto dei problemi legati all'uso (e all'uso eccessivo) dei singleton. Sono stato una di quelle persone all'inizio della mia carriera. Riesco a vedere qual è il problema adesso, eppure ci sono ancora molti casi in cui non riesco a vedere una buona alternativa - e non molte delle discussioni anti-Singleton ne forniscono davvero una.

Ecco un esempio reale di un grande progetto recente in cui sono stato coinvolto:

L'applicazione era un client denso con molte schermate e componenti separati che utilizza enormi quantità di dati da uno stato del server che non viene aggiornato troppo spesso. Questi dati sono stati sostanzialmente memorizzati nella cache in un oggetto "manager" Singleton - il temuto "stato globale". L'idea era quella di avere questo unico posto nell'app che mantenga i dati archiviati e sincronizzati, e quindi qualsiasi nuova schermata che viene aperta può semplicemente interrogare la maggior parte di ciò di cui hanno bisogno da lì, senza fare richieste ripetitive per vari dati di supporto dal server. La richiesta costante al server richiederebbe troppa larghezza di banda - e sto parlando di migliaia di dollari di fatture Internet extra a settimana, quindi era inaccettabile.

Esiste un altro approccio che potrebbe essere appropriato qui che fondamentalmente avere questo tipo di oggetto cache del gestore dati globale? Naturalmente questo oggetto non deve essere ufficialmente un "Singleton", ma concettualmente ha senso esserlo. Qual è una bella alternativa pulita qui?


10
Quale problema dovrebbe risolvere l'uso di un Singleton? In che modo è meglio risolvere quel problema rispetto alle alternative (come una classe statica)?
Anon.

14
@Anon: In che modo l'utilizzo di una classe statica migliora la situazione. C'è ancora un accoppiamento stretto?
Martin York,

5
@Martin: non sto suggerendo che lo renda "migliore". Sto suggerendo che nella maggior parte dei casi, un singleton è una soluzione alla ricerca di un problema.
Anon.

9
@Anon: non è vero. Le classi statiche non ti danno (quasi) alcun controllo sull'istanza e rendono il multi-threading ancora più difficile di quanto facciano i Singleton (dal momento che devi serializzare l'accesso a ogni singolo metodo invece che solo all'istanza). I singleton possono anche almeno implementare un'interfaccia, che le classi statiche non possono. Le classi statiche hanno certamente i loro vantaggi, ma in questo caso il Singleton è sicuramente il minore di due mali considerevoli. Una classe statica che implementa qualsiasi stato mutevole di sorta è come un grande neon lampeggiante "ATTENZIONE: PROGETTAZIONE DISPONIBILE!" cartello.
Aaronaught,

7
@Aaronaught: se stai solo sincronizzando l'accesso al singleton, la tua concorrenza è interrotta . Il tuo thread potrebbe essere interrotto subito dopo aver recuperato l'oggetto singleton, un altro thread si accende, e blam, condizioni di gara. L'uso di un Singleton invece di una classe statica, nella maggior parte dei casi, sta semplicemente togliendo i segnali di avvertimento e pensando che risolva il problema .
Anon.

Risposte:


809

È importante distinguere qui tra singole istanze e il modello di progettazione Singleton .

Le singole istanze sono semplicemente una realtà. La maggior parte delle app è progettata per funzionare solo con una configurazione alla volta, un'interfaccia utente alla volta, un file system alla volta e così via. Se ci sono molti stati o dati da mantenere, allora sicuramente vorresti avere solo un'istanza e tenerlo in vita il più a lungo possibile.

Il modello di progettazione Singleton è un tipo molto specifico di singola istanza, in particolare uno che è:

  • Accessibile tramite un campo di istanza globale e statico;
  • Creato all'inizializzazione del programma o al primo accesso;
  • Nessun costruttore pubblico (impossibile creare un'istanza direttamente);
  • Mai liberato esplicitamente (liberato implicitamente al termine del programma).

È a causa di questa specifica scelta progettuale che il modello introduce diversi potenziali problemi a lungo termine:

  • Incapacità di usare classi astratte o di interfaccia;
  • Incapacità di sottoclasse;
  • Elevato accoppiamento attraverso l'applicazione (difficile da modificare);
  • Difficile da testare (impossibile falsificare / simulare nei test unitari);
  • Difficile parallelizzare in caso di stato mutevole (richiede un blocco esteso);
  • e così via.

Nessuno di questi sintomi è in realtà endemico in singole istanze, solo il modello Singleton.

Cosa puoi fare invece? Semplicemente non usare il modello Singleton.

Citando dalla domanda:

L'idea era quella di avere questo unico posto nell'app che mantenga i dati archiviati e sincronizzati, e quindi qualsiasi nuova schermata che viene aperta può semplicemente interrogare la maggior parte di ciò di cui hanno bisogno da lì, senza fare richieste ripetitive per vari dati di supporto dal server. La richiesta costante al server richiederebbe troppa larghezza di banda - e sto parlando di migliaia di dollari di fatture Internet extra a settimana, quindi era inaccettabile.

Questo concetto ha un nome, dato che in qualche modo accenni a ma suona incerto. Si chiama cache . Se vuoi divertirti, puoi chiamarlo "cache offline" o solo una copia offline di dati remoti.

Una cache non deve essere un singleton. Potrebbe essere necessario essere una singola istanza se si desidera evitare di recuperare gli stessi dati per più istanze della cache; ma ciò non significa che devi davvero esporre tutto a tutti .

La prima cosa che farei è separare le diverse aree funzionali della cache in interfacce separate. Ad esempio, supponiamo che stiate realizzando il peggior clone di YouTube al mondo basato su Microsoft Access:

                          MSAccessCache
                                ▲
                                |
              + ----------------- + ----------------- +
              | | |
         IMediaCache IProfileCache IPageCache
              | | |
              | | |
          VideoPage MyAccountPage MostPopularPage

Qui ci sono diverse interfacce che descrivono i tipi specifici di dati a cui una particolare classe potrebbe aver bisogno di accedere: media, profili utente e pagine statiche (come la prima pagina). Tutto ciò è implementato da una mega-cache, ma tu progetti le tue singole classi per accettare invece le interfacce, quindi a loro non importa che tipo di istanza abbiano. Si inizializza l'istanza fisica una volta, all'avvio del programma, quindi si inizia a passare le istanze (trasmettere a un particolare tipo di interfaccia) tramite costruttori e proprietà pubbliche.

A proposito, questo si chiama Iniezione delle dipendenze ; non è necessario utilizzare Spring o alcun contenitore IoC speciale, purché il progetto di classe generale accetti le sue dipendenze dal chiamante invece di istanziarle da solo o fare riferimento allo stato globale .

Perché dovresti usare il design basato sull'interfaccia? Tre motivi:

  1. Rende il codice più facile da leggere; puoi capire chiaramente dalle interfacce da quali dati dipendono le classi dipendenti.

  2. Se e quando ti rendi conto che Microsoft Access non è stata la scelta migliore per un back-end di dati, puoi sostituirlo con qualcosa di meglio - diciamo SQL Server.

  3. Se e quando ti rendi conto che SQL Server non è la scelta migliore per i media in particolare , puoi interrompere l'implementazione senza influire su qualsiasi altra parte del sistema . È qui che entra in gioco il vero potere dell'astrazione.

Se vuoi fare un passo avanti, puoi usare un contenitore IoC (DI framework) come Spring (Java) o Unity (.NET). Quasi ogni framework DI eseguirà la propria gestione a vita e specificatamente consentirà di definire un particolare servizio come singola istanza (spesso chiamandolo "singleton", ma è solo per familiarità). Fondamentalmente questi framework ti risparmiano la maggior parte del lavoro delle scimmie nel passare manualmente le istanze, ma non sono strettamente necessari. Non hai bisogno di strumenti speciali per implementare questo progetto.

Per completezza, devo sottolineare che il progetto sopra non è nemmeno l'ideale. Quando si tratta di una cache (come siete), si dovrebbe effettivamente avere una completamente separata strato . In altre parole, un design come questo:

                                                        + - IMediaRepository
                                                        |
                          Cache (generico) --------------- + - IProfileRepository
                                ▲ |
                                | + - IPageRepository
              + ----------------- + ----------------- +
              | | |
         IMediaCache IProfileCache IPageCache
              | | |
              | | |
          VideoPage MyAccountPage MostPopularPage

Il vantaggio di questo è che non hai nemmeno bisogno di interrompere la tua Cacheistanza se decidi di eseguire il refactoring; puoi cambiare il modo in cui i media vengono archiviati semplicemente alimentandoli con un'implementazione alternativa di IMediaRepository. Se pensi a come si adatta, vedrai che crea sempre e solo un'istanza fisica di una cache, quindi non dovrai mai recuperare gli stessi dati due volte.

Niente di tutto ciò significa che ogni singolo software al mondo debba essere progettato secondo questi rigorosi standard di alta coesione e accoppiamento lento; dipende dalle dimensioni e dalla portata del progetto, dal tuo team, dal tuo budget, dalle scadenze, ecc. Ma se stai chiedendo quale sia il miglior design (da usare al posto di un singleton), allora è così.

PS Come altri hanno già detto, probabilmente non è la migliore idea per le classi dipendenti di essere consapevoli del fatto che stanno usando una cache - questo è un dettaglio di implementazione di cui semplicemente non dovrebbero mai preoccuparsi. Detto questo, l'architettura complessiva sembrerebbe ancora molto simile a quanto sopra, non si farebbe riferimento alle singole interfacce come cache . Invece li chiameresti servizi o qualcosa di simile.


131
Primo post che abbia mai letto, che in realtà spiega DI come alternativa allo stato globale. Grazie per il tempo e l'impegno profusi. Stiamo tutti meglio come risultato di questo post.
MrLane,

4
Perché la cache non può essere un singleton? Non è un singleton se lo passi in giro e usi l'iniezione di dipendenza? Singleton si limita a limitarci a un'istanza, non a come si accede, giusto? Vedi la mia opinione
Erik Engheim,

29
@AdamSmith: Hai veramente letto qualsiasi di questa risposta? Risposte alla tua domanda nei primi due paragrafi. Singleton Pattern! == Istanza singola.
Aaronaught,

5
@Cawas e Adam Smith - Leggendo i tuoi link ho la sensazione che tu non abbia davvero letto questa risposta - tutto è già lì.
Wilbert,

19
@Cawas Sento che la carne di questa risposta è la distinzione tra single-istanza e singleton. Singleton è cattivo, non lo è per singola istanza. Dependency Injection è un modo carino e generale di usare istanze singole senza dover usare i singoli.
Wilbert,

48

Nel caso che dai, sembra che l'uso di un Singleton non sia il problema, ma il sintomo di un problema - un problema più grande, architettonico.

Perché le schermate interrogano l'oggetto cache per i dati? La memorizzazione nella cache deve essere trasparente per il client. Dovrebbe esserci un'astrazione appropriata per fornire i dati e l'implementazione di tale astrazione potrebbe utilizzare la memorizzazione nella cache.

È probabile che il problema sia che le dipendenze tra parti del sistema non siano impostate correttamente e questo è probabilmente sistemico.

Perché gli schermi devono avere conoscenza di dove ottengono i loro dati? Perché le schermate non vengono fornite con un oggetto in grado di soddisfare le loro richieste di dati (dietro le quali è nascosta una cache)? Spesso la responsabilità per la creazione di schermate non è centralizzata, quindi non esiste un punto chiaro per l'iniezione delle dipendenze.

Ancora una volta, stiamo esaminando problemi di architettura e design su larga scala.

Inoltre, è molto importante capire che la durata di un oggetto può essere completamente separata da come l'oggetto viene trovato per l'uso.

Una cache dovrà vivere per tutta la durata dell'applicazione (per essere utile), quindi la durata dell'oggetto è quella di un Singleton.

Ma il problema con Singleton (almeno l'implementazione comune di Singleton come classe / proprietà statica), è come le altre classi che lo usano vanno a cercarlo.

Con un'implementazione statica di Singleton, la convenzione è semplicemente usarla ovunque sia necessario. Ma ciò nasconde completamente la dipendenza e accoppia strettamente le due classi.

Se forniamo la dipendenza alla classe, tale dipendenza è esplicita e tutta la classe che consuma deve conoscere il contratto disponibile per l'uso.


2
Esiste una gigantesca quantità di dati di cui alcuni schermi potrebbero aver bisogno, ma non necessariamente necessari. E non sai fino a quando non saranno state prese le azioni dell'utente che lo definiscono - e ci sono molte, molte combinazioni. Quindi il modo in cui è stato fatto è stato avere alcuni dati globali comuni che venivano mantenuti nella cache e sincronizzati nel client (principalmente ottenuti al momento dell'accesso), quindi le richieste successive aumentano ulteriormente la cache, poiché i dati richiesti esplicitamente tendono a essere riutilizzati nuovamente in la stessa sessione. L'attenzione si concentra sulla riduzione delle richieste al server, quindi la necessità di una cache lato client. <cont>
Bobby Tables

1
<cont> È essenzialmente trasparente. Nel senso che c'è un callback dal server se alcuni dati richiesti non sono ancora memorizzati nella cache. Ma l'implementazione (logicamente e fisicamente) di quel gestore di cache è un Singleton.
Tavoli Bobby,

6
Sono qui con qstarin: gli oggetti che accedono ai dati non devono sapere (o devono sapere) che i dati sono memorizzati nella cache (ovvero un dettaglio di implementazione). Gli utenti dei dati semplicemente richiedono i dati (o chiedono un'interfaccia per recuperare i dati).
Martin York,

1
La memorizzazione nella cache è essenzialmente un dettaglio di implementazione. Esiste un'interfaccia attraverso la quale vengono interrogati i dati e gli oggetti che li ottengono non sanno se provengono dalla cache o meno. Ma sotto questo gestore di cache è un Singleton.
Tavoli Bobby,

2
@Bobby Tables: la tua situazione non è così terribile come sembrava. Quel Singleton (supponendo che tu intenda una classe statica, non solo un oggetto con un'istanza che dura finché l'app) è ancora problematico. Sta nascondendo il fatto che i tuoi dati che forniscono oggetto hanno una dipendenza da un provider di cache. È meglio se questo è esplicito ed esternalizzato. Disaccoppiarli. È essenziale per la testabilità che è possibile sostituire facilmente i componenti e un provider di cache è un ottimo esempio di tale componente (con quale frequenza un provider di cache è supportato da ASP.Net).
Quentin-starin,

45

Ho scritto un intero capitolo solo su questa domanda. Principalmente nel contesto dei giochi, ma la maggior parte dovrebbe applicarsi al di fuori dei giochi.

tl; dr:

Il modello Gang of Four Singleton fa due cose: offrire un comodo accesso a un oggetto da qualsiasi luogo e assicurarsi che sia possibile crearne solo un'istanza. Il 99% delle volte, tutto ciò che ti interessa è la prima metà, e il trasporto lungo la seconda metà per ottenerlo aggiunge una limitazione non necessaria.

Non solo, ma ci sono soluzioni migliori per un accesso conveniente. Rendere globale un oggetto è l'opzione nucleare per risolverlo e rende facile distruggere l'incapsulamento. Tutto ciò che è negativo sui globali si applica completamente ai singoli.

Se lo stai usando solo perché hai molti posti nel codice che devono toccare lo stesso oggetto, prova a trovare un modo migliore per dargli solo quegli oggetti senza esporlo all'intera base di codice. Altre soluzioni:

  • Abbandonalo del tutto. Ho visto molte classi singleton che non hanno alcuno stato e sono solo borse di funzioni di supporto. Quelli non hanno affatto bisogno di un'istanza. Basta renderle funzioni statiche o spostarle in una delle classi che la funzione accetta come argomento. Non avresti bisogno di un corso speciale Mathse solo potessi farlo 123.Abs().

  • Passalo in giro. La semplice soluzione se un metodo ha bisogno di qualche altro oggetto è semplicemente passarlo. Non c'è niente di sbagliato nel far passare alcuni oggetti.

  • Mettilo nella classe base. Se hai molte classi che hanno tutti bisogno di accedere ad un oggetto speciale e condividono una classe base, puoi rendere quell'oggetto un membro sulla base. Quando lo costruisci, passa l'oggetto. Ora tutti gli oggetti derivati ​​possono ottenerlo quando ne hanno bisogno. Se lo rendi protetto, assicurati che l'oggetto rimanga incapsulato.


1
Puoi riassumere qui?
Nicole,

1
Fatto, ma ti incoraggio ancora a leggere l'intero capitolo.
munifico

4
un'altra alternativa: Iniezione delle dipendenze!
Brad Cupit,

1
@BradCupit ne parla anche sul link ... Devo dire che sto ancora cercando di digerire tutto. Ma è stata la lettura più chiara sui singoli che abbia mai letto. Fino ad ora, sono stato sono stati necessari singletons positivi, proprio come vars globali, e stavo promuovendo il Toolbox . Ora non lo so più. Signor munificoent, può dirmi se il localizzatore di servizi è solo una cassetta degli attrezzi statica ? Non sarebbe meglio renderlo un singleton (quindi, una cassetta degli attrezzi)?
Cregox,

1
"Toolbox" sembra abbastanza simile a "Service Locator" per me. Sia che tu usi una statica per farlo, sia che tu la trasformi in singleton non è, secondo me, così importante per la maggior parte dei programmi. Tendo a puntare sulla statica perché perché affrontare l'inizializzazione lenta e l'allocazione degli heap se non è necessario?
munifico

21

Non è lo stato globale di per sé che è il problema.

Davvero devi solo preoccuparti global mutable state. Lo stato costante non è influenzato da effetti collaterali e quindi è meno problematico.

La principale preoccupazione con singleton è che aggiunge l'accoppiamento e quindi rende le cose più difficili. È possibile ridurre l'accoppiamento ottenendo il singleton da un'altra fonte (ad esempio una fabbrica). Ciò ti consentirà di disaccoppiare il codice da un'istanza particolare (anche se diventi più accoppiato alla fabbrica (ma almeno la fabbrica può avere implementazioni alternative per fasi diverse)).

Nella tua situazione penso che tu possa cavartela fintanto che il tuo singleton implementa effettivamente un'interfaccia (in modo che un'alternativa possa essere utilizzata in altre situazioni).

Ma un altro grande svantaggio dei singleton è che una volta sul posto rimuoverli dal codice e sostituirli con qualcos'altro diventa un vero e proprio compito difficile (c'è di nuovo quell'accoppiamento).

// Example from 5 minutes (con't be too critical)
class ServerFactory
{
    public:
        // By default return a RealServer
        ServerInterface& getServer();

        // Set a non default server:
        void setServer(ServerInterface& server);
};

class ServerInterface { /* define Interface */ };

class RealServer: public ServerInterface {}; // This is a singleton (potentially)

class TestServer: public ServerInterface {}; // This need not be.

Questo ha senso. Questo mi fa anche pensare che non ho mai veramente abusato di Singletons, ma ho appena iniziato a dubitare di QUALSIASI uso di loro. Ma non riesco a pensare a nessun vero abuso che ho fatto, secondo questi punti. :)
Bobby Tables,

2
(probabilmente intendi di per sé non "per dire")
no,

4
@nohat: sono madrelingua del "Queens English" e quindi rifiuto qualsiasi aspetto francese a meno che non lo facciamo meglio (come le weekendoops che è uno dei nostri). Grazie :-)
Martin York,

21
di per sé è latino.
Anon.

2
@Anon: OK. Non è poi così male ;-)
Martin York,

19

E allora? Dal momento che nessuno lo ha detto: Toolbox . Cioè se vuoi variabili globali .

L' abuso di Singleton può essere evitato guardando il problema da una prospettiva diversa. Supponiamo che un'applicazione abbia bisogno solo di un'istanza di una classe e l'applicazione configura tale classe all'avvio: Perché la classe stessa dovrebbe essere responsabile di essere un singleton? Sembra abbastanza logico che l'applicazione si assuma questa responsabilità, poiché l'applicazione richiede questo tipo di comportamento. L'applicazione, non il componente, dovrebbe essere il singleton. L'applicazione quindi rende disponibile un'istanza del componente che può essere utilizzata da qualsiasi codice specifico dell'applicazione. Quando un'applicazione utilizza diversi di questi componenti, può aggregarli in quella che abbiamo chiamato una cassetta degli attrezzi.

In parole povere, la casella degli strumenti dell'applicazione è un singleton che è responsabile della configurazione di se stesso o di consentire al meccanismo di avvio dell'applicazione di configurarlo ...

public class Toolbox {
     private static Toolbox _instance; 

     public static Toolbox Instance {
         get {
             if (_instance == null) {
                 _instance = new Toolbox(); 
             }
             return _instance; 
         }
     }

     protected Toolbox() {
         Initialize(); 
     }

     protected void Initialize() {
         // Your code here
     }

     private MyComponent _myComponent; 

     public MyComponent MyComponent() {
         get {
             return _myComponent(); 
         }
     }
     ... 

     // Optional: standard extension allowing
     // runtime registration of global objects. 
     private Map components; 

     public Object GetComponent (String componentName) {
         return components.Get(componentName); 
     }

     public void RegisterComponent(String componentName, Object component) 
     {
         components.Put(componentName, component); 
     }

     public void DeregisterComponent(String componentName) {
         components.Remove(componentName); 
     }

}

Ma indovina un po? È un singleton!

E cos'è un singleton?

Forse è qui che inizia la confusione.

Per me, il singleton è un oggetto obbligato ad avere un'unica istanza e sempre. Puoi accedervi ovunque, in qualsiasi momento, senza bisogno di istanziarlo. Ecco perché è così strettamente correlato static. Per fare un confronto, staticè sostanzialmente la stessa cosa, tranne che non è un'istanza. Non abbiamo bisogno di istanziarlo, non possiamo nemmeno, perché è allocato automagicamente. E questo può e può causare problemi.

Dalla mia esperienza, la semplice sostituzione staticdi Singleton ha risolto molti problemi in un progetto di borsa patchwork di medie dimensioni in cui mi trovo. Ciò significa solo che ha un certo utilizzo per progetti mal progettati. Penso che ci sia troppa discussione se il modello singleton è utile o meno e non posso davvero discutere se sia davvero cattivo . Ma ci sono ancora buoni argomenti a favore di singleton rispetto ai metodi statici, in generale .

L'unica cosa di cui sono certo è negativa per i singoli, è quando li usiamo ignorando le buone pratiche. Questo è davvero qualcosa di non così facile da affrontare. Ma le cattive pratiche possono essere applicate a qualsiasi modello. E, lo so, è troppo generico per dire che ... Voglio dire, c'è davvero troppo.

Non fraintendetemi!

In parole povere, proprio come i variegati globali , i singoli dovrebbero essere comunque evitati in ogni momento . Soprattutto perché sono abusati eccessivamente. Ma i var globali non possono essere sempre evitati e dovremmo usarli in quest'ultimo caso.

Ad ogni modo, ci sono molti altri suggerimenti oltre a Toolbox, e proprio come la toolbox, ognuno ha la sua applicazione ...

Altre alternative

  • Il miglior articolo che ho appena letto su singletons suggerisce Service Locator come alternativa. Per me è fondamentalmente una " cassetta degli attrezzi statica ", se vuoi. In altre parole, rendi il Service Locator un Singleton e avrai una Toolbox. Ciò va contro il suo suggerimento iniziale di evitare il singleton, ovviamente, ma questo è solo per far rispettare il problema del singleton: come viene usato, non lo schema in sé.

  • Altri suggeriscono Factory Pattern come alternativa. È stata la prima alternativa che ho sentito da un collega e l'abbiamo rapidamente eliminata per il nostro utilizzo come var globale . Di sicuro ha il suo utilizzo, ma anche i singoli.

Entrambe le alternative sopra sono buone alternative. Ma tutto dipende dal tuo utilizzo.

Ora, insinuare singleton dovrebbe essere evitato a tutti i costi è semplicemente sbagliato ...

  • La risposta di Aaronaught suggerisce di non usare mai i singoli , per una serie di ragioni. Ma sono tutte ragioni contro il modo in cui è mal utilizzato e abusato, non direttamente contro il modello stesso. Sono d'accordo con tutte le preoccupazioni su questi punti, come posso? Penso solo che sia fuorviante.

Le incapacità (di astrarre o sottoclasse) sono davvero lì, ma allora? Non è pensato per questo. Non c'è incapacità di interfacciarsi , per quanto ne so . L' accoppiamento elevato può anche essere lì, ma è solo perché viene comunemente utilizzato. Non deve . In effetti, l'accoppiamento in sé non ha nulla a che fare con il modello singleton. Ciò premesso, elimina anche la difficoltà di testare. Per quanto riguarda la difficoltà di parallelizzare, ciò dipende dal linguaggio e dalla piattaforma, quindi, ancora una volta, non è un problema sul modello.

Esempi pratici

Vedo spesso 2 in uso, sia a favore che contro i single. Cache Web (il mio caso) e servizio di registro .

La registrazione, alcuni sosterranno , è un perfetto esempio singleton, perché, e cito:

  • I richiedenti hanno bisogno di un oggetto ben noto a cui inviare richieste per accedere. Ciò significa un punto di accesso globale.
  • Poiché il servizio di registrazione è una singola fonte di eventi a cui possono registrarsi più listener, deve esserci solo un'istanza.
  • Sebbene diverse applicazioni possano accedere a diversi dispositivi di output, il modo in cui registrano i loro ascoltatori è sempre lo stesso. Tutta la personalizzazione viene eseguita tramite gli ascoltatori. I clienti possono richiedere la registrazione senza sapere come o dove verrà registrato il testo. Pertanto, ogni applicazione utilizzerà il servizio di registrazione esattamente allo stesso modo.
  • Qualsiasi applicazione dovrebbe essere in grado di cavarsela con una sola istanza del servizio di registrazione.
  • Qualsiasi oggetto può essere un richiedente registrazione, compresi i componenti riutilizzabili, in modo che non debbano essere accoppiati a nessuna particolare applicazione.

Mentre altri sosterranno che è difficile espandere il servizio di registro una volta che ti rendi conto che in realtà non dovrebbe essere solo un'istanza.

Bene, dico che entrambi gli argomenti sono validi. Il problema qui, di nuovo, non è nel modello singleton. È sulle decisioni architettoniche e sulla ponderazione se il refactoring è un rischio praticabile. È un ulteriore problema quando, di solito, il refactoring è l'ultima misura correttiva necessaria.


@gnat Grazie! Stavo solo pensando di modificare la risposta per aggiungere un avvertimento sull'uso di Singletons ... Il tuo preventivo si adatta perfettamente!
Cregox,

2
felice che ti sia piaciuto. Non sono sicuro che ciò contribuirà ad evitare i voti negativi - probabilmente i lettori stanno avendo difficoltà a collegare il punto sollevato in questo post al problema concreto esposto nella domanda, specialmente alla luce dell'eccezionale analisi fornita nella risposta precedente
moscerino

@gnat sì, sapevo che questa era una lunga battaglia. Spero che il tempo lo dirà. ;-)
cregox,

1
Sono d'accordo con la tua direzione generale qui, anche se forse è un po 'troppo entusiasta di una libreria che sembra non essere molto più di un contenitore IoC estremamente spoglio (anche più semplice rispetto ad esempio a Funq ). Service Locator è in realtà un anti-schema; è uno strumento utile principalmente nei progetti legacy / brownfield, insieme alla "DI di un uomo povero", dove sarebbe troppo costoso refactificare tutto per usare correttamente un contenitore IoC.
Aaronaught,

1
@Cawas: Ah, scusa, mi sono confuso java.awt.Toolkit. Il mio punto è lo stesso però: Toolboxsuona come un sacco di pezzi non collegati, piuttosto che una classe coerente con un unico scopo. A me non sembra un buon design. (Si noti che l'articolo a cui si fa riferimento è del 2001, prima che l'iniezione di dipendenza e i contenitori DI fossero diventati all'ordine del giorno.)
Jon Skeet,

5

Il mio problema principale con il modello di progettazione singleton è che è molto difficile scrivere buoni test unitari per la tua applicazione.

Ogni componente che ha una dipendenza da questo "gestore" lo fa interrogando la sua istanza singleton. E se si desidera scrivere un test unitario per tale componente, è necessario immettere dati in questa istanza singleton, che potrebbe non essere facile.

Se d'altra parte il tuo "manager" viene iniettato nei componenti dipendenti attraverso un parametro costruttore, e il componente non conosce il tipo concreto del gestore, solo un'interfaccia o una classe base astratta implementata dal gestore, quindi un'unità test potrebbe fornire implementazioni alternative del gestore durante il test delle dipendenze.

Se si utilizzano i contenitori IOC per configurare e creare un'istanza dei componenti che compongono la propria applicazione, è possibile configurare facilmente il proprio contenitore IOC per creare solo un'istanza del "gestore", consentendo di ottenere lo stesso, solo un'istanza che controlla la cache dell'applicazione globale .

Ma se non ti interessa i test unitari, un modello di progettazione singleton può andare perfettamente bene. (ma non lo farei comunque)


Ben spiegato, questa è la risposta che meglio spiega la questione del test dei Singletons
José Tomás Tocino,

4

Un singleton non è in alcun modo fondamentale male , nel senso che qualsiasi cosa il design computing può essere buono o cattivo. Può sempre e solo essere corretto (fornisce i risultati previsti) oppure no. Può anche essere utile o meno, se rende il codice più chiaro o più efficiente.

Un caso in cui i singoli sono utili è quando rappresentano un'entità che è davvero unica. Nella maggior parte degli ambienti, i database sono unici, esiste davvero solo un database. La connessione a quel database può essere complicata perché richiede autorizzazioni speciali o l'attraversamento di diversi tipi di connessione. Organizzare quella connessione in un singleton probabilmente ha molto senso solo per questo motivo.

Ma devi anche essere sicuro che il singleton sia davvero un singleton e non una variabile globale. Ciò è importante quando il singolo database unico è in realtà 4 database, uno ciascuno per dispositivi di produzione, stadiazione, sviluppo e test. Un Database Singleton capirà a quali di esse dovrebbe connettersi, afferrare la singola istanza per quel database, collegarlo se necessario e restituirlo al chiamante.

Quando un singleton non è in realtà un singleton (questo è quando la maggior parte dei programmatori si arrabbiano), è un globale pigramente istanziato, non c'è possibilità di iniettare un'istanza corretta.

Un'altra caratteristica utile di un modello singleton ben progettato è che spesso non è osservabile. Il chiamante chiede una connessione. Il servizio che lo fornisce può restituire un oggetto in pool o, se sta eseguendo un test, può crearne uno nuovo per ogni chiamante o fornire invece un oggetto simulato.


3

L'uso del modello singleton che rappresenta oggetti reali è perfettamente accettabile. Scrivo per iPhone e ci sono molti singoli nel framework Cocoa Touch. L'applicazione stessa è rappresentata da un singleton della classe UIApplication. Esiste una sola applicazione, quindi è opportuno rappresentarla con un singleton.

L'uso di un singleton come classe di gestione dei dati va bene purché sia ​​progettato correttamente. Se è un bucket di proprietà dei dati, non è meglio dell'ambito globale. Se è un insieme di getter e setter, è meglio, ma non ancora eccezionale. Se è una classe che gestisce davvero tutte le interfacce con i dati, incluso forse il recupero di dati remoti, la memorizzazione nella cache, l'installazione e lo smontaggio ... Potrebbe essere molto utile.


2

I singoli sono solo la proiezione di un'architettura orientata ai servizi in un programma.

Un'API è un esempio di singleton a livello di protocollo. Si accede a Twitter, Google ecc. Attraverso quelli che sono essenzialmente singoli. Allora perché i singoli diventano cattivi all'interno di un programma?

Dipende da come pensi a un programma. Se pensi a un programma come a una società di servizi piuttosto che a istanze memorizzate nella cache in modo casuale, allora i singoli hanno perfettamente senso.

I singoli sono un punto di accesso al servizio. L'interfaccia pubblica a una libreria di funzionalità strettamente legata che nasconde forse un'architettura interna molto sofisticata.

Quindi non vedo un singleton così diverso da una fabbrica. Il singleton può avere parametri di costruzione passati. Può essere creato da un contesto che sa come risolvere la stampante predefinita contro tutti i possibili meccanismi di selezione, ad esempio. Per i test puoi inserire il tuo finto. Quindi può essere abbastanza flessibile.

La chiave è internamente in un programma quando eseguo e ho bisogno di un po 'di funzionalità posso accedere al singleton con piena fiducia che il servizio è attivo e pronto per l'uso. Questa è la chiave quando ci sono diversi thread che iniziano in un processo che deve passare attraverso una macchina a stati per essere considerato pronto.

In genere avvolgo una XxxServiceclasse che avvolge un singleton attorno alla classe Xxx. Singleton non è in classe Xxxaffatto, è riportata in un'altra classe, XxxService. Questo perché Xxxpuò avere più istanze, anche se non è probabile, ma vogliamo comunque avere Xxxun'istanza accessibile a livello globale su ciascun sistema. XxxServicefornisce una buona separazione delle preoccupazioni. Xxxnon è necessario applicare una politica singleton, ma possiamo usarla Xxxcome singleton quando ne abbiamo bisogno.

Qualcosa di simile a:

//XxxService.h:
/**
 * Provide singleton wrapper for Xxx object. This wrapper
 * can be autogenerated so is not made part of the object.
 */

#include "Xxx/Xxx.h"


class XxxService
{
    public:
    /**
     * Return a Xxx object as a singleton. The double check
     * singleton algorithm is used. A 0 return means there was
     * an error. Developers should use this as the access point to
     * get the Xxx object.
     *
     * <PRE>
     * @@ #include "Xxx/XxxService.h"
     * @@ Xxx* xxx= XxxService::Singleton();
     * <PRE>
     */

     static Xxx*     Singleton();

     private:
         static Mutex  mProtection;
};


//XxxService.cpp:

#include "Xxx/XxxService.h"                   // class implemented
#include "LockGuard.h"     

// CLASS SCOPE
//
Mutex XxxService::mProtection;

Xxx* XxxService::Singleton()
{
    static Xxx* singleton;  // the variable holding the singleton

    // First check to see if the singleton has been created.
    //
    if (singleton == 0)
    {
        // Block all but the first creator.
        //
        LockGuard lock(mProtection);

        // Check again just in case someone had created it
        // while we were blocked.
        //
        if (singleton == 0)
        {
            // Create the singleton Xxx object. It's assigned
            // to a temporary so other accessors don't see
            // the singleton as created before it really is.
            //
            Xxx* inprocess_singleton= new Xxx;

            // Move the singleton to state online so we know that is has
            // been created and it ready for use.
            //
            if (inprocess_singleton->MoveOnline())
            {
                LOG(0, "XxxService:Service: FAIL MoveOnline");
                return 0;
            }

            // Wait until the module says it's in online state.
            //
            if (inprocess_singleton->WaitTil(Module::MODULE_STATE_ONLINE))
            {
                LOG(0, "XxxService:Service: FAIL move to online");
                return 0;
            }

            // The singleton is created successfully so assign it.
            //
            singleton= inprocess_singleton;


        }// still not created
    }// not created

    // Return the created singleton.
    //
    return singleton;

}// Singleton  

1

Prima domanda, trovi molti bug nell'applicazione? forse dimenticando di aggiornare la cache o cache errata o trovare difficile cambiare? (Ricordo che un'app non cambierebbe dimensioni a meno che non cambiassi anche il colore ... puoi comunque cambiare il colore indietro e mantenere le dimensioni).

Quello che faresti è avere quella classe ma RIMUOVI TUTTI I MEMBRI STATICI. Ok questo non è nessacary ma lo consiglio. In realtà basta inizializzare la classe come una classe normale e PASSARE il puntatore. Non dire Frigen ClassIWant.APtr (). LetMeChange.ANYTHINGATALL (). Andhave_no_structure ()

È più lavoro ma in realtà, è meno confuso. Alcuni luoghi in cui non dovresti cambiare cose che ora non puoi, poiché non sono più globali. Tutte le mie lezioni da manager sono classi regolari, trattala come tale.


1

IMO, il tuo esempio suona bene. Suggerirei il factoring come segue: oggetto cache per ciascun oggetto dati (e dietro ciascuno); gli oggetti cache e gli oggetti db accessor hanno la stessa interfaccia. Questo dà la possibilità di scambiare le cache dentro e fuori il codice; inoltre offre un facile percorso di espansione.

Grafico:

DB
|
DB Accessor for OBJ A
| 
Cache for OBJ A
|
OBJ A Client requesting

L'accessorio DB e la cache possono ereditare dallo stesso oggetto o tipo duck in assomigliare allo stesso oggetto, qualunque cosa. Finché è possibile collegare / compilare / test e funziona ancora.

Questo disaccoppia le cose in modo da poter aggiungere nuove cache senza dover accedere e modificare alcuni oggetti Uber-Cache. YMMV. IANAL. ECCETERA.


1

Un po 'tardi alla festa, ma comunque.

Singleton è uno strumento in una cassetta degli attrezzi, proprio come qualsiasi altra cosa. Spero che tu abbia più nella tua cassetta degli attrezzi che un solo martello.

Considera questo:

public void DoSomething()
{
    MySingleton.Instance.Work();
}

vs

public void DoSomething(MySingleton singleton)
{
    singleton.Work();
}
DoSomething(MySingleton.instance);

Il primo caso porta ad un alto accoppiamento ecc .; Il 2 ° modo non ha problemi che @Aaronaught sta descrivendo, per quanto ne so. È tutto su come lo usi.


Disaccordo. Sebbene il secondo modo sia "migliore" (riduzione dell'accoppiamento), presenta ancora problemi risolti da DI. Non devi fare affidamento sul consumatore della tua classe per fornire l'implementazione dei tuoi servizi, cosa che è meglio fare nel costruttore quando la classe viene creata. L'interfaccia dovrebbe richiedere solo il minimo indispensabile in termini di argomenti. C'è anche una buona possibilità che la tua classe richieda una singola istanza su cui operare - ancora una volta, fare affidamento sul consumatore per far rispettare questa regola è rischioso e inutile.
AlexFoxGill

A volte Di è un problema per un determinato compito. Il metodo in un codice di esempio potrebbe essere un costruttore, ma non necessariamente - senza guardare un esempio concreto è un argomento controverso. Inoltre, DoSomething potrebbe prendere ISomething e MySingleton potrebbe implementare quell'interfaccia - ok, non è in un campione ... ma è solo un esempio.
Evgeni,

1

Chiedi a ogni schermata di visualizzare Manager nel proprio costruttore.

Quando avvii la tua app, crei un'istanza del gestore e la passi in giro.

Questo si chiama Inversion of Control e consente di sostituire il controller quando la configurazione cambia e nei test. Inoltre, puoi eseguire diverse istanze dell'applicazione o parti dell'applicazione in parallelo (buono per il test!). Infine il tuo manager morirà con il suo oggetto proprietario (la classe di avvio).

Quindi struttura la tua app come un albero, dove le cose sopra possiedono tutto ciò che viene usato sotto di esse. Non implementare un'app come una mesh, dove tutti conoscono tutti e si trovano l'un l'altro attraverso metodi globali.

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.