Se i singleton sono cattivi, perché un contenitore di servizi è buono?


91

Sappiamo tutti quanto siano cattivi i Singleton perché nascondono dipendenze e per altri motivi .

Ma in un framework, potrebbero esserci molti oggetti che devono essere istanziati solo una volta e chiamati da qualsiasi luogo (logger, db ecc.).

Per risolvere questo problema mi è stato detto di usare un cosiddetto "Gestore oggetti" (o contenitore di servizi come symfony) che memorizza internamente ogni riferimento ai servizi (logger, ecc.).

Ma perché un fornitore di servizi non è così cattivo come un Singleton puro?

Anche il fornitore di servizi nasconde le dipendenze e si limita a concludere la creazione della prima istanza. Quindi sto davvero lottando per capire perché dovremmo usare un fornitore di servizi invece di single.

PS. So che per non nascondere le dipendenze dovrei usare DI (come dichiarato da Misko)

Inserisci

Aggiungerei: di questi tempi i single non sono poi così malvagi, il creatore di PHPUnit lo ha spiegato qui:

DI + Singleton risolve il problema:

<?php
class Client {

    public function doSomething(Singleton $singleton = NULL){

        if ($singleton === NULL) {
            $singleton = Singleton::getInstance();
        }

        // ...
    }
}
?>

è abbastanza intelligente anche se questo non risolve affatto tutti i problemi.

Oltre a DI e Service Container, esistono soluzioni accettabili per accedere a questi oggetti helper?


2
@yes La tua modifica sta facendo false supposizioni. Sebastian non suggerisce in alcun modo che lo snippet di codice sta rendendo l'utilizzo di Singleon meno problematico. È solo un modo per rendere più testabile un codice che altrimenti sarebbe impossibile testare. Ma è ancora un codice problematico. Infatti, osserva esplicitamente: "Solo perché puoi, non significa che dovresti". La soluzione corretta sarebbe comunque quella di non usare affatto Singleton.
Gordon

3
@yes segue il principio SOLID.
Gordon

19
Contesto l'affermazione che i single sono cattivi. Possono essere utilizzati in modo improprio, sì, ma lo stesso vale per qualsiasi strumento. Un bisturi può essere utilizzato per salvare una vita o per porvi fine. Una motosega può disboscare le foreste per prevenire gli incendi o può tagliare una parte considerevole del tuo braccio se non sai cosa stai facendo. Impara a usare i tuoi strumenti con saggezza e non trattare i consigli come vangelo: in questo modo giace la mente irriflessiva.
paxdiablo

4
@paxdiablo ma sono cattivi. I singleton violano SRP, OCP e DIP. Introducono lo stato globale e l'accoppiamento stretto nella tua applicazione e faranno mentire la tua API sulle sue dipendenze. Tutto ciò influirà negativamente sulla manutenibilità, leggibilità e testabilità del codice. Potrebbero esserci rari casi in cui questi inconvenienti superano i piccoli benefici, ma direi che nel 99% non hai bisogno di un Singleton. Soprattutto in PHP dove i singleton sono comunque unici per la richiesta ed è semplicissimo assemblare i grafici dei collaboratori da un builder.
Gordon

5
No, non credo proprio. Uno strumentoèun mezzo per eseguire una funzione, di solito rendendola più semplice in qualche modo, sebbene alcuni (emacs?) Abbiano la rara distinzione di renderla più difficile :-) In questo, un singleton non è diverso da un albero bilanciato o da un compilatore . Se è necessario garantire solo una copia di un oggetto, un singleton lo fa. Se lo fa bene può essere discusso, ma non credo che si possa sostenere che non lo fa affatto. E potrebbero esserci modi migliori, come una motosega che è più veloce di una sega a mano o una pistola per unghie contro un martello. Ciò non rende la sega / martello meno uno strumento.
paxdiablo

Risposte:


76

Service Locator è solo il minore di due mali per così dire. Il "minore" che si riduce a queste quattro differenze ( almeno non riesco a pensare ad altre in questo momento ):

Principio di responsabilità unica

Il contenitore di servizi non viola il principio di responsabilità singola come fa Singleton. I singleton combinano la creazione di oggetti e la logica aziendale, mentre il contenitore dei servizi è strettamente responsabile della gestione dei cicli di vita degli oggetti della tua applicazione. A questo proposito, Service Container è migliore.

Accoppiamento

I singleton sono solitamente codificati nell'applicazione a causa delle chiamate al metodo statico, il che porta a dipendenze strettamente accoppiate e difficili da simulare nel codice. La SL d'altra parte è solo una classe e può essere iniettata. Quindi, mentre tutta la tua classe dipenderà da questo, almeno è una dipendenza vagamente accoppiata. Quindi, a meno che tu non abbia implementato ServiceLocator come Singleton stesso, è leggermente migliore e anche più facile da testare.

Tuttavia, tutte le classi che utilizzano ServiceLocator ora dipenderanno da ServiceLocator, che è anche una forma di accoppiamento. Questo può essere mitigato utilizzando un'interfaccia per ServiceLocator in modo da non essere vincolato a un'implementazione concreta di ServiceLocator, ma le tue classi dipenderanno dall'esistenza di una sorta di Locator, mentre il mancato utilizzo di un ServiceLocator aumenta notevolmente il riutilizzo.

Dipendenze nascoste

Tuttavia, il problema di nascondere le dipendenze esiste molto. Quando inietti il ​​localizzatore nelle tue classi che consumano, non conoscerai alcuna dipendenza. Ma a differenza di Singleton, SL di solito istanzia tutte le dipendenze necessarie dietro le quinte. Quindi, quando prendi un servizio, non finisci come Misko Hevery nell'esempio della carta di credito , ad esempio non devi istanziare a mano tutte le dipendenze delle dipendenze.

Recuperare le dipendenze dall'interno dell'istanza viola anche la Legge di Demetra , che afferma che non dovresti scavare nei collaboratori. Un'istanza dovrebbe parlare solo con i suoi collaboratori immediati. Questo è un problema con Singleton e ServiceLocator.

Stato globale

Anche il problema dello stato globale è in qualche modo mitigato perché quando si istanzia un nuovo Service Locator tra i test vengono eliminate anche tutte le istanze create in precedenza (a meno che non si sia commesso l'errore e le si sia salvate in attributi statici nella SL). Ciò non vale per nessuno stato globale nelle classi gestite da SL, ovviamente.

Vedi anche Fowler su Service Locator vs Dependency Injection per una discussione molto più approfondita.


Una nota sul tuo aggiornamento e l'articolo collegato di Sebastian Bergmann sul test del codice che utilizza Singletons : Sebastian non suggerisce in alcun modo che la soluzione alternativa proposta renda l'utilizzo di Singleons meno problematico. È solo un modo per rendere più testabile un codice che altrimenti sarebbe impossibile testare. Ma è ancora un codice problematico. Infatti, osserva esplicitamente: "Solo perché puoi, non significa che dovresti".


1
Soprattutto la testabilità dovrebbe essere applicata qui. Non è possibile simulare chiamate a metodi statici. È tuttavia possibile simulare i servizi che sono stati iniettati tramite il costruttore o il setter.
David

44

Il pattern del localizzatore di servizi è un anti-pattern. Non risolve il problema di esporre le dipendenze (non si può dire guardando la definizione di una classe quali siano le sue dipendenze perché non vengono iniettate, ma vengono invece estratte dal localizzatore di servizi).

Quindi, la tua domanda è: perché i localizzatori di servizi sono buoni? La mia risposta è: non lo sono.

Evita, evita, evita.


6
Sembra che tu non sappia nulla di interfacce. La classe descrive semplicemente l'interfaccia necessaria nella firma del costruttore - ed è tutto ciò che deve sapere. Il localizzatore di servizi superato dovrebbe implementare l'interfaccia, tutto qui. E se IDE verificherà l'implementazione dell'interfaccia, sarà abbastanza facile controllare qualsiasi modifica.
OZ_

4
@ yes123: Le persone che dicono che hanno torto e si sbagliano perché SL è un anti-pattern. La tua domanda è "perché gli SL sono buoni?" La mia risposta è: non lo sono.
Jason

5
Non discuterò se SL sia un modello antico o meno, ma quello che dirò è che è molto meno mali rispetto a singleton e globali. Non puoi testare una classe che dipende da un singleton, ma puoi sicuramente testare una classe che dipende da un SL (anche se puoi rovinare il design SL fino al punto in cui non funziona) ... Quindi ne vale la pena notando ...
ircmaxell

3
@ Jason devi passare un oggetto che implementa l'interfaccia - ed è solo quello che devi sapere. Ti stai limitando alla sola definizione di costruttore di classi e vuoi scrivere nel costruttore tutte le classi (non le interfacce) - è un'idea stupida. Tutto ciò di cui hai bisogno è l'interfaccia. Puoi testare con successo questa classe con mock, puoi facilmente cambiare il comportamento senza cambiare codice, non ci sono dipendenze e accoppiamenti extra - questo è tutto (in generale) ciò che vogliamo avere in Dependency Injection.
OZ_

2
Certo, metto insieme Database, Logger, Disco, Modello, Cache e Utente in un unico oggetto "Input", sicuramente sarà più facile dire su quali dipendenze fa affidamento il mio oggetto rispetto a se avessi usato un contenitore.
Mahn

4

Il contenitore di servizi nasconde le dipendenze come fa il pattern Singleton. Potresti suggerire di utilizzare invece i contenitori di inserimento delle dipendenze, in quanto ha tutti i vantaggi del contenitore di servizi ma non (per quanto ne so) svantaggi che ha il contenitore di servizi.

Per quanto ne so, l'unica differenza tra i due è che nel contenitore di servizi, il contenitore di servizi è l'oggetto che viene iniettato (nascondendo così le dipendenze), quando usi DIC, il DIC inietta le dipendenze appropriate per te. La classe gestita dal DIC è completamente ignara del fatto che è gestita da un DIC, quindi hai meno accoppiamenti, dipendenze chiare e test unitari felici.

Questa è una buona domanda in SO che spiega la differenza di entrambi: qual è la differenza tra i modelli Dependency Injection e Service Locator?


"il DIC inietta le dipendenze appropriate per te" Non succede anche con Singleton?
dinamico

5
@ yes123 - Se stai usando un Singleton, non lo inietteresti, la maggior parte delle volte lo accederai semplicemente a livello globale (questo è il punto di Singleton). Suppongo che se dici che se inietti il Singleton, non nasconderà le dipendenze, ma in qualche modo sconfigge lo scopo originale del pattern Singleton - ti chiederesti, se non ho bisogno che questa classe sia accessibile a livello globale, perché devo renderlo Singleton?
Rickchristie

2

Perché puoi facilmente sostituire gli oggetti nel contenitore dei servizi con
1) ereditarietà (la classe Object Manager può essere ereditata e i metodi possono essere sovrascritti)
2) modificando la configurazione (nel caso di Symfony)

Inoltre, i singleton sono cattivi non solo a causa dell'accoppiamento elevato, ma perché sono _ single _tons. È un'architettura sbagliata per quasi tutti i tipi di oggetti.

Con il DI 'puro' (nei costruttori) pagherai un prezzo molto alto - tutti gli oggetti dovrebbero essere creati prima di essere passati al costruttore. Significherà più memoria utilizzata e meno prestazioni. Inoltre, non sempre l'oggetto può essere semplicemente creato e passato al costruttore - si possono creare catene di dipendenze ... Il mio inglese non è abbastanza buono per discuterne completamente, leggetelo nella documentazione di Symfony.


0

Per me, cerco di evitare costanti globali, singleton per un semplice motivo, ci sono casi in cui potrei aver bisogno di API in esecuzione.

Ad esempio, ho front-end e admin. All'interno dell'amministratore, voglio che siano in grado di accedere come utente. Considera il codice all'interno di admin.

$frontend = new Frontend();
$frontend->auth->login($_GET['user']);
$frontend->redirect('/');

Questo può stabilire una nuova connessione al database, un nuovo logger, ecc. Per l'inizializzazione del frontend e controllare se l'utente esiste effettivamente, valido, ecc. Inoltre userebbe cookie e servizi di localizzazione separati.

La mia idea di singleton è: non puoi aggiungere lo stesso oggetto all'interno del genitore due volte. Per esempio

$logger1=$api->add('Logger');
$logger2=$api->add('Logger');

ti lascerebbe con una singola istanza ed entrambe le variabili che puntano ad essa.

Infine, se vuoi usare lo sviluppo orientato agli oggetti, allora lavora con gli oggetti, non con le classi.


1
quindi il tuo metodo è passare la $api var intorno al tuo framework? Non ho capito esattamente cosa intendi. Inoltre, se la chiamata add('Logger')restituisce la stessa istanza, fondamentalmente hai un cotainer del servizio
dinamico

si, è esatto. Mi riferisco a loro come "Controller di sistema" e hanno lo scopo di migliorare la funzionalità dell'API. In modo simile, l'aggiunta di un controller "Auditable" due volte a un modello funzionerebbe esattamente nello stesso modo: creare solo un'istanza e un solo set di campi di controllo.
romaninsh
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.