È 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:
Rende il codice più facile da leggere; puoi capire chiaramente dalle interfacce da quali dati dipendono le classi dipendenti.
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.
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 Cache
istanza 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.