Con questo articolo torniamo alla rubrica che si occupa dei design pattern e della loro implementazione specifica in PHP. Dopo aver analizzato la Factory e l'AbstractFactory, due pattern di sicura utilità nella creazione di software ben strutturato e progettato, passiamo ora all'analisi di uno dei pattern più utilizzati della programmazione ad oggetti: il pattern Singleton.
Nell'ingegneria del software il pattern Singleton è utilizzato per limitare ad una sola le istanze di un oggetto. Questo è utile quando è necessario avere esattamente un'unica istanza di un oggetto per coordinare le azioni all'interno del proprio sistema, oppure quando si hanno degli aumenti di efficienza condividendo una sola istanza. L'utilizzo del pattern Singleton permette l'eliminazione delle variabili globali che, dovendo sottostare alle rigide regole di soping dei linguaggi di programmazione, sono ormai ritenute una pratica obsoleta che dovrebbe cadere in disuso; eliminando l'utilizzo delle variabili globali avremo la possibilità di scrivere codice più ordinato, facilmente manutenibile e meno propenso agli errori.
Struttura del pattern
L'implementazione standard del pattern singleton prevede la creazione di una classe con un metodo statico che crea una nuova istanza dell'oggetto a meno che questa non sia stata già creata in precedenza. In quel caso restituisce per riferimento (fortunatamente in PHP 5 non dobbiamo più preoccuparci esplicitamente dei riferimenti) l'istanza precedentemente creata. Per assicurarsi che l'istanza dell'oggetto non sia creata direttamente, il costruttore è definito come privato o protetto; dal punto di vista pratico non vi è alcuna differenza tra un'istanza statica di una classe ed un singleton, anche se l'utilizzo del pattern permette una stesura di codice più elegante e soprattutto ritarda la creazione dell'istanza al momento in cui verrà richiesta per la prima volta, diminuendo i consumi inutili di memoria. In linguaggi che permettono accorgimenti sintattici alternativi il pattern singleton potrebbe esser creato utilizzando sistemi alternativi (magari sovrascrivendo l'operatore new o facendo in modo che la costruzione di un oggetto restituisca sempre la prima istanza creata).
Anche se non è il caso di PHP, è utile ricordare che il pattern singleton va implementato in modo differente all'interno di applicazioni multithread: se due thread richiedono un singleton allo stesso tempo, è opportuno che solo una di queste richieste si occupi di creare il singleton mentre l'altra dovrebbe ottenere l'istanza precedentemente creata.
Prima di continuare con l'implementazione, vediamo il diagramma UML del design pattern che andremo ad implementare:
Implementazione generica
Dato che in PHP non dobbiamo preoccuparci dei problemi derivanti dalla programmazione concorrente (a meno che non si stia lavorando in ambiente CLI sfruttando qualche estensione per il supporto multithread sviluppata appositamente - ma questa è un altra storia ...) possiamo occuparci di implementare il pattern nella sua forma più semplice, quella che prevede una proprietà statica privata contenente l'istanza univoca, un costruttore privato in cui inizializzare l'oggetto ed infine un metodo statico pubblico che si occuperà di restituire l'istanza della classe oppure di crearla nel caso si tratti del primo accesso.
<?php
/**
* Semplice implementazione del design pattern singleton.
* Data la struttura sintattica di PHP non è possibile sfruttare l'ereditarietà
* per poter riutilizzare il pattern, se non utilizzando piccoli stratagemmi sintattici.
*/
class MySingleton
{
/**
* La variabile statica privata che conterrà l'istanza univoca
* della nostra classe.
*/
private static $instance = null;
/**
* Il costruttore in cui ci occuperemo di inizializzare la nostra
* classe. E' opportuno specificarlo come privato in modo che venga
* visualizzato automaticamente un errore dall'interprete se si cerca
* di istanziare la classe direttamente.
*/
private function __construct()
{
// ... codice ...
}
/**
* Il metodo statico che si occupa di restituire l'istanza univoca della classe.
* per facilitare il riutilizzo del codice in altre situazioni, si frutta la
* costante __CLASS__ che viene valutata automaticamente dall'interprete con il
* nome della classe corrente (ricordo che "new $variabile" crea un'istanza della classe
* il cui nome è specificato come stringa all'interno di $variabile)
*/
public static function getInstance()
{
if(self::$instance == null)
{
$c = __CLASS__;
self::$instance = new $c;
}
return self::$instance;
}
}
?>
Bisogna precisare che non è sempre possibile implementare il pattern Singleton sfruttando i principi di ereditarietà della programmazione ad oggetti, come accade in PHP ed in molti altri linguaggi. Spesso, per motivi relativi al linguaggio, ci si trova a dover riscrivere l'implementazione per ogni classe che vorremmo si comportasse da Singleton. Fortunatamente le righe di codice richieste per implementare il pattern sono veramente poche, e quindi il problema viene agilmente arginato.
In PHP è anche possibile creare una funziona che si occupi di restituire il Singleton di una classe generica, sfruttando la possibilità di istanziare una classe in base al nome:
<?php
/**
* Implementiamo una classe final dato che non dovrà essere estesa.
* La classe viene utilizzata semplicemente come namespace.
*/
final class singleton
{
/**
* Un array che contterrà le istanze univoche delle classi di cui avremo creato dei
* singleton.
* Le istanze saranno identificabili usando come chiave il nome in minuscolo della
* classe stessa.
*/
private static $instances = array();
/**
* Il metodo stati che si occupa di restituire l'istanza univoca della classe
* il cui nome deve essere specificato come primo parametro.
* Se la classe non esiste viene generato un errore.
*/
public static function getInstance($class_name)
{
if(!class_exists($class_name))
{
trigger_error("La classe ".$class_name." non esiste!", E_USER_ERROR);
}
/**
* PHP è case insensitive dal punto di vista sintattico, ma le chiavi
* di un hash non lo sono.
* Per evitare istanze multiple della stessa classe, uniformiamo i nomi.
*/
$class_name = strtolower($class_name);
if(!array_key_exists($class_name, self::$instances))
{
self::$instances[$class_name] = new $class_name;
}
return self::$instances[$class_name];
}
}
?>
Una volta scelto il metodo che più preferiamo possiamo utilizzarlo nel nostro codice. È opportuno ricordarsi che i due metodi non sono interscambiabili.
<?php
$instance = MySingleton::getInstance();
$instance_2 = MySingleton::getInstance();
var_dump($instance);
var_dump($instance_2);
/**
* I due valori restituiti sono puntano in realtà allo stesso oggetto,
* difatti con l'operatore === risultano identici.
*/
var_dump($instance === $instance_2);
class TestClass
{
public $a;
public function __construct()
{
$this->a = rand();
}
}
$instance = singleton::getInstance('TestClass');
$instance_2 = singleton::getInstance('TestClass');
var_dump($instance);
var_dump($instance_2);
var_dump($instance === $instance_2);
$instance->a = "prova nuovo valore";
var_dump($instance_2->a);
?>
Esempio di un caso reale
Ora vediamo come possiamo sfruttare il pattern Singleton in un caso reale. In realtà il pattern in questione è talmente comune da poter essere utilizzato in moltissimi ambiti; quello che vorrei illustrare io è l'implementazione di una semplice classe che funge da registro, e può essere utilizzata per registrare le opzioni di configurazione e condividere dati tra oggetti differenti. Un utilizzo pratico della classe potrebbe essere quello di sfruttarla come repository in cui registrare una serie di plugin da invocare in base a specifiche situazioni indicate dal nostro framework. Per comodità il registro esporrà anche un iteratore.
<?php
class RegistryException extends Exception
{
public function __construct($message)
{
parent::__construct($message);
}
}
class Registry implements IteratorAggregate
{
/**
* Implementazione del singleton come descritta in precedenza.
*/
private static $instance = null;
public static function getInstance()
{
if(self::$instance == null)
{
$c = __CLASS__;
self::$instance = new $c;
}
return self::$instance;
}
/**
* Array che conterrà le coppie chiave => valore salvate
* nel registro.
*/
private $pool;
private function __construct()
{
$this->pool = array();
}
/**
* Restituisce l'iteratore che per comodità è uno di quelli
* implementati nella SPL.
*/
public function getIterator()
{
return new ArrayIterator($this->pool);
}
/**
* Recupera il valore di una determinata chiave.
* Se la chiave non esiste restituisce l'eccezione RegistryException.
*/
public function get($key)
{
if($this->has($key))
{
return $this->pool[$key];
}
throw new RegistryException("Key not found: ".$key);
}
public function set($key, $value)
{
$this->pool[$key] = $value;
}
public function has($key)
{
return array_key_exists($key, $this->pool);
}
/**
* Funzioni di utilità per accedere alle chiavi come se fossero
* proprietà del registro.
*/
public function __get($key)
{
return $this->get($key);
}
public function __set($key, $value)
{
$this->set($key, $value);
}
}
?>
Conclusioni
Abbiamo terminato con l'analisi del pattern Singleton. Come già ampiamente discusso nell'articolo possiamo notare che il pattern è di indubbia utilità, soprattutto per il fatto che migliora notevolmente il codice rispetto ad una programmazione basata sulle variabili globali o sulle risorse distribuite.