2010-10-30 17 views
19

All'improvviso ho un po 'di crisi OO. Negli ultimi due anni ho fatto un uso abbastanza buono degli oggetti Singleton. Li ho usati in molti posti.Buona progettazione OO - The Singleton Design Pattern

Ad esempio, nella progettazione di un'applicazione MVC Java, creo una classe 'SystemRegistry' di Singleton per archiviare il modello e visualizzare le classi (ho lavorato solo su semplici app e non è mai emersa la necessità di più viste) .

Quando creo il mio modello e visualizzare gli oggetti (che non erano single, solo oggetti normali), mi piacerebbe fare qualcosa di simile:

SystemRegistry.getInstance().setModel(model); 

Nelle mie classi controller (che erano i gestori di eventi più o meno per diversi elementi GUI), avrei avuto accesso alla vista o modello come segue:

SystemRegistry.getInstance().getView(); 

non avrei mai utilizzare la classe SystemRegistry nella porzione modello della mia app, ma sarebbe, a volte, usarlo a mio avviso per accedere (ma raramente, se mai, per modificare) le informazioni dal modello.

Da quello che ho letto (in particolare Steve Yegge's article), questo sembra un modo scadente per progettare la mia applicazione. Qualche idea su modi migliori per strutturare il mio codice.

Inoltre, un altro aspetto di come disegno le classi, che possono o non possono essere correlate a Singletons, è l'uso di classi di tipo Manager. Un esempio è un (molto semplice) motore di gioco basato su OpenGL che ho creato in C++.

La classe principale era GameEngine. Era la classe over-arcing che immagazzinava un gruppo di Manager e gestiva il ciclo principale e cosa no. Alcuni dei gestori memorizzati in questa classe erano cose come: ObjectManager, RenderingManager, LightingManager, EventManager (include input), HUDManager, FrameRateManager, WindowManager, ecc. Probabilmente ce n'erano anche altri.

Fondamentalmente queste classi gestivano i diversi aspetti del motore di gioco. I nomi sono piuttosto semplici, quindi dovresti essere in grado di avere una buona idea di come vengono utilizzati.

Ora, questo doveva essere una base riutilizzabile che potrei utilizzare in diversi progetti con la necessità di modificarlo idealmente.

In ogni nuovo gioco, vorrei creare un'istanza del GameEngine come variabile di classe (la maggior parte della logica di gioco era memorizzata in una singola classe) e impostare i diversi gestori (ad esempio, il caricamento della finestra co -ordina o illumina i dettagli da un file, impostando l'FPS, ecc.). Per registrare un oggetto nel ObjectManager vorrei fare qualcosa di simile:

Player player = new Player(); 
gameEngine.getObjectManager().addObject(player); 

Questo oggetto sarà ora memorizzato in un vettore nella classe ObjectManager e sarà disegnato quando il GameEngine chiama il metodo() in ogni fotogramma i drawObjects ObjectManager .

Potrei essere diventato un po 'paranoico ora dopo quell'articolo su Singletons (e potrebbe non aver avuto abbastanza tempo per girarmi intorno), ma sto iniziando a indovinare e mi chiedo se il modo in cui ho progettato il mio GameEngine era corretto (per mancanza di una parola migliore) e non cadeva nelle stesse insidie ​​condivise dal pattern Singleton.

Qualsiasi commento sul mio post sarebbe molto apprezzato.

Modifica: Grazie per le risposte. Li apprezzo molto.Se possibile, mi piacerebbe se qualcuno potesse darmi qualche suggerimento riguardo ai due scenari di progetto pubblicati sopra. Come avrei potuto evitare l'uso di Singletons/Managers?

Con il primo, DI sarebbe stata la risposta corretta? Dovrei avere anche dato la vista di accesso al modello (questo è probabilmente più di una risposta MVC)? La vista trarrebbe vantaggio dall'implementazione di un'interfaccia (in modo da poter collegare più viste diverse)?

Nel secondo caso, in quale altro modo si sarebbe strutturata l'applicazione? La lamentela è semplicemente l'uso delle classi Manager rispetto a nomi più specifici? O è che, in alcuni casi, le classi possono essere ulteriormente suddivise (ad esempio ObjectHolder, ObjectDrawer, ObjectUpdater)?

risposta

11

Bene, perché hai scritto singleton? Se capisci cosa è andato storto nella tua singletonite, sai a cosa fare attenzione in futuro.

Perché hai creduto per un secondo che i singleton fossero una buona soluzione per il tipo di ? Perché un libro ha detto così? Non fidarti ciecamente dei libri. I libri devono giustificare le loro affermazioni come chiunque altro.Se ti dico che il tuo codice sembra molto meglio se capovolgi il monitor, l'onere della prova è su di me. Anche se sono una specie di codice dio. Anche se milioni di sviluppatori in tutto il mondo mi adorano ogni giorno, il mio consiglio non vale ancora nulla se non riesco a giustificare e se non riesco a farti andare "ok, questo ha senso. e non riesco a pensare a un modo migliore di farlo ".

E lo stesso vale per un determinato libro di modelli di progettazione. Così hanno scritto che "i modelli di design sono fantastici" e che "il singleton è un modello di design, e quindi anche questo è fantastico". E allora? Possono giustificare questa affermazione (no, non possono, almeno non nel caso singleton), quindi ignorarla.

Se qualcun altro ha suggerito di utilizzare un singleton, la logica è la stessa: queste persone hanno effettivamente presentato una buona argomentazione sul perché fosse una buona idea?

E se ti venisse in mente una giustificazione, puoi, oggi, vedere cosa c'è che non va? Cosa hai dimenticato di prendere in considerazione?

L'unico modo per evitare di fare troppi errori in futuro è imparare dagli errori che hai già fatto. In che modo singletons o manager class sono scivolati dalle tue difese? A cosa avresti dovuto prestare attenzione quando li hai conosciuti per la prima volta? O del resto, più tardi, quando li stavi usando liberamente nel tuo codice. Quali segni premonitori erano lì che dovevano farti chiedere "questi singleton sono davvero la strada giusta da percorrere?" Come programmatore, devi fidarti della tua stessa mente. Devi fidarti che prenderai decisioni sensate. E l'unico modo per farlo è imparare dalle decisioni sbagliate quando le fai. Perché lo facciamo tutti, troppo spesso. Il meglio che possiamo fare è assicurarci che non prendiamo le stesse cattive decisioni ripetutamente.

Per quanto riguarda le classi di manager, il loro problema è che ogni classe dovrebbe avere esattamente una sola responsabilità. Qual è la responsabilità di una "classe dirigente"? E ... uh ... gestisce le cose. Se non è possibile definire una concisa area di responsabilità, allora la classe è sbagliata. Che cosa ha esattamente bisogno di gestire questi oggetti? Che cosa significa significa per gestirli? La libreria standard fornisce già classi di contenitore per la memorizzazione di un gruppo di oggetti.

Quindi è necessario un po 'di codice con la responsabilità di disegnare tutti gli oggetti memorizzati lì. Ma questo non deve essere lo stesso oggetto dei negozi .

Che altro deve essere gestito? Scopri cosa significa "gestire" questi oggetti, e quindi sai quali classi devi gestire. Molto probabilmente non è un singolo compito monolitico, ma diversi tipi di responsabilità, che dovrebbero essere suddivisi in classi diverse.

Per quanto riguarda i singleton, non ripeterò quello che ho già detto tante volte, quindi ecco uno link.

Un ultimo consiglio:

vite OOP. Veramente. Il buon codice non è sinonimo di OOP. A volte le lezioni sono uno strumento utile per il lavoro. A volte, ingombrano tutto e seppelliscono i più semplici pezzi di codice dietro infiniti strati di astrazione.

Cerca in altri paradigmi. Se lavori in C++, devi conoscere la programmazione generica.STL e Boost sono dei buoni esempi di come sfruttare la programmazione generica per scrivere codice per risolvere molti problemi che sono entrambi più puliti, migliori e più efficienti rispetto al codice OOP equivalente.

E indipendentemente dalla lingua, ci sono molte lezioni preziose da apprendere dalla programmazione funzionale.

E a volte, la semplice programmazione procedurale vecchia è solo lo strumento bello e semplice di cui hai bisogno.

La chiave per "un buon design" è non per tentare "buona progettazione OO". Se lo fai, ti chiudi in quel paradigma. Potresti anche cercare di trovare un buon modo per costruire una casa usando un martello. Non sto dicendo che non è possibile, ma ci sono modi migliori per costruire case molto migliori se ti permetti anche di usare altri strumenti.

quanto riguarda il tuo Edit:

Con la prima, sarebbe DI potuto essere la risposta giusta? Dovrei avere anche dato la vista di accesso al modello (questo è probabilmente più di una risposta MVC)? La vista trarrebbe vantaggio dall'implementazione di un'interfaccia (in modo da poter collegare più viste diverse)?

DI sarebbe stato uno opzione per evitare single. Ma non dimenticare l'opzione low-tech vecchio stile: passa semplicemente un riferimento agli oggetti che devono conoscere il tuo "ex" singleton.

Il tuo renderer deve conoscere l'elenco degli oggetti di gioco. (O lo fa davvero? Forse ha solo un singolo metodo che ha bisogno di essere dato l'elenco degli oggetti. Forse la classe di renderer in sé non ne ha bisogno) Quindi il costruttore del renderer dovrebbe avere un riferimento a quella lista. Questo è davvero tutto quello che c'è da fare. Quando X ha bisogno di accedere a Y, si passa a un riferimento a Y in un costruttore di parametri di funzione. La cosa bella di questo approccio, rispetto a DI, è che rendi le tue dipendenze dolorosamente esplicite. È noto al che il programma di rendering conosce l'elenco di oggetti renderizzabili, poiché è possibile vedere il riferimento passato al costruttore. E hai dovuto scrivere a questa dipendenza, dandoti una grande opportunità di fermarti e chiedere "è necessario?" E tu hai una motivazione per eliminare le dipendenze: significa meno battitura!

Molte delle dipendenze che si ottengono quando si utilizzano singleton o DI non sono necessarie. Entrambi questi strumenti rendono semplice e facile propagare le dipendenze tra diversi moduli, e questo è ciò che fai. E poi finisci con un disegno in cui il tuo renderer è a conoscenza dell'input da tastiera e il gestore di input deve sapere come salvare il gioco.

Un'altra possibile mancanza di DI è una semplice questione di tecnologia. Non tutte le lingue hanno buone librerie DI disponibili. In alcune lingue è quasi impossibile scrivere una libreria DI robusta e generica.

Nel secondo caso, in quale altro modo si sarebbe strutturata l'applicazione? La lamentela è semplicemente l'uso delle classi Manager rispetto a nomi più specifici? O è che, in alcuni casi, le classi possono essere ulteriormente suddivise (ad esempio ObjectHolder, ObjectDrawer, ObjectUpdater)?

Penso che rinominarli semplicemente sia un buon inizio, sì. Come ho detto sopra, cosa significa "gestire" i tuoi oggetti? Se gestisco quegli oggetti, cosa dovrei fare? Le tre classi che hai menzionato sembrano una buona divisione.Naturalmente, quando si scava nella progettazione di queste classi, ci si potrebbe chiedere perché persino abbia bisogno diun ObjectHolder. Non è esattamente quello che fanno le classi standard di contenitore/raccolta di libreria? Hai bisogno di un corso dedicato per "custodire oggetti" o puoi semplicemente usare lo List<GameObject>?

Quindi penso che entrambe le opzioni siano davvero la stessa cosa. Se tu puoi dare alla classe un nome più specifico, probabilmente non avrai bisogno di dividerlo in più classi più piccole. Ma se non si riesce a pensare ad un singolo nome che chiarisca cosa dovrebbe fare la classe, allora probabilmente deve essere scomposto in più classi.

Immagina di mettere da parte il tuo codice per sei mesi. Quando ritorni ad esso, avrai qualsiasi idea di quale fosse lo scopo della classe "ObjectManager"? Probabilmente no, ma avrete una buona idea su cosa sia "ObjectRenderer". Rende gli oggetti.

+4

+1 "Screw OOP" è una boccata d'aria fresca. Troppe persone identificano automaticamente e incondizionatamente "OOP" con "buono". Bjarne Stroustrup ha scritto su questo più di una volta. – fredoverflow

+0

Non capisco perché "la cosa bella di questo approccio, rispetto a DI, è che rendi le tue dipendenze dolorosamente esplicite". Questo è quello che fai in DI; le tue classi specificano le loro dipendenze, sia in parametri costruttore, metodi setter o (yick) variabili private.Puoi farlo senza DI _framework_, ma ciò può essere complicato. Lo fa manualmente porta spesso alla classe A prendendo in classe B nel costruttore quando l'unico uso di B è quello di passarlo al costruttore della classe C. – NamshubWriter

+0

@NamshubWriter: il mio punto è che con un framework DI, la classe registra le sue dipendenze , certo, e poi tipo di "apparire magicamente". Non è necessario passarli da dove sono stati creati dove sono necessari, in modo da non far "sentire" la dipendenza grap come si fa se l'unico modo per propagare le dipendenze è passandole come parametri. DI rende difficile vedere da dove viene una dipendenza * *. E non ti fornisce un modo chiaro per tracciare dove un certo oggetto è usato come dipendenza. Tutto dipende da una grande scatola nera di stato globale, che passa liberamente le dipendenze. – jalf

3

Steve Yegge ha ragione. Google va anche oltre: hanno uno Singleton detector per aiutarli a estirparli.

penso che ci sono alcuni problemi con Singletons:

  1. Più difficile da testare.
  2. Stateful Singletons devono fare attenzione alla sicurezza del filo; facile diventare un collo di bottiglia.

Ma a volte davvero fare deve avere solo uno. Non mi farei prendere la mano dalla sindrome di un bambino con un modello.

+0

+1 Ma se si ha la necessità di uno solo, l'iniezione delle dipendenze è la strada da percorrere, e con i moderni framework DI aggiunge solo poco in termini di complessità. – helpermethod

+0

Esattamente. La primavera ha Singletons, ma non sono la stessa cosa che scrivere la tua. – duffymo

+0

Anche quando "devi averne uno solo", ciò non significa che hai bisogno di un singleton. Anche senza DI, passare un argomento in più al costruttore di solito non è un grosso problema come potrebbe sembrare in anticipo. – jalf

1

Ho avuto un simile tipo di realizzazione un po 'di recente quando ho visto uno dei miei progetti e visto la proliferazione di classi "manager" (per lo più singleton). Sembra che le vostre preoccupazioni siano duplici: in primo luogo, se utilizzare lo stato globale (portando a classi di tipo manager) e, in secondo luogo, se i singleton siano la scelta migliore per lo stato globale.

Non penso che sia davvero in bianco e nero. A volte hai entità che dovrebbero avere esattamente un'istanza e dovrebbero essere accessibili da qualsiasi parte del tuo programma, nel qual caso, una soluzione statale globale ha senso. E può essere conveniente se l'alternativa non globale prevede la creazione di un'istanza della propria entità e il passaggio di riferimenti ad esso attraverso strati su strati di metodi e costruttori (e gerarchie di costruttori di classi ugh).

D'altra parte, bisogna considerare i mali dello stato globale. Nasconde le dipendenze. Non appena si introducono i globali, le funzioni non sono più caselle nere il cui input è limitato ai propri parametri. E se si dispone di uno stato globale mutabile a cui è possibile accedere da più classi in più thread in qualsiasi ordine, è possibile avere un incubo di debug sulle proprie mani. E tutto ciò rende la comprensione/test più difficile.

Quando si opta per lo stato globale, a volte una classe statica è una scelta migliore di un singleton (esistono anche altre opzioni). Per prima cosa, salva le invocazioni extra di GetInstance() ... e per me è semplicemente più naturale. Ma vale la pena notare che i singletons consentono automaticamente il caricamento lento, che può essere importante se l'inizializzazione della propria entità globale è piuttosto pesante. Inoltre (e in qualche modo inventato qui), se dovessi fornire varianti del tuo comportamento globale, puoi semplicemente sottoclassi il tuo singleton.

0

Il modo in cui decido se voglio un singleton o no è quando rispondo alla domanda: "Se dovessi istanziare la mia applicazione due volte nello stesso runtime, avrebbe senso?", Se la risposta è sì, allora usalo

0

A due fasi-risposta:

1) http://misko.hevery.com/2008/08/17/singletons-are-pathological-liars/

Singoletti sono buon design se vengono creati per garantire esiste solo un esempio. Sono un cattivo design se vengono creati per garantire l'accesso globale.

2) http://misko.hevery.com/2008/08/21/where-have-all-the-singletons-gone/

accesso globale viene neutralizzata mediante iniezione di dipendenza.

+1

puoi darmi un esempio di quando è necessario "assicurarsi che esista solo un'istanza"? È solo un buon design garantire che quando effettivamente * hai * bisogno di quella garanzia. E non riesco a pensare a un singolo caso in cui ne hai bisogno. – jalf

+0

I soliti esempi sono "impostazioni dell'applicazione" - non si desidera che una parte dell'app cambi il colore di sfondo in un'istanza, ma un'altra parte dell'app cerca il colore di sfondo in un'istanza diversa - "inventario", "sistema log "e così via. –

+0

@jalf un altro esempio tipico è una connessione al database – koen

4

Per quanto riguarda la tua domanda iniziale tutto è stato detto, quindi questo sulla domanda successiva nella tua modifica.

Non confondere di avere solo un'istanza di una classe con il modello di singleton. Nel tuo caso, va bene avere una classe manager che gestisce tutti i tipi di cose. È anche fantastico avere solo un'istanza per l'intera applicazione. Ma le classi che utilizzano il gestore non devono fare affidamento sull'esistenza di esattamente un oggetto globale (sia tramite una variabile statica per la classe o una variabile globale a cui accede tutto). Per mitigarlo, fagli richiedere un riferimento a un oggetto manager quando vengono costruiti. Possono tutti ricevere un riferimento allo stesso oggetto, a loro non importa. Questo ha due grandi effetti collaterali. Innanzitutto, puoi testare meglio le tue classi, perché puoi facilmente iniettare un oggetto mock-manager. Il secondo vantaggio è che, se si decide in seguito, è comunque possibile utilizzare più di un oggetto gestore (ad esempio in scenari multithreading) senza dover modificare i client.