Share |

sabato 30 gennaio 2010

PHP: Realizzare una Cache Singleton (OOP)

In questo articolo affronteremo la creazione di una cache rispettosa del design pattern singleton scritta in PHP. Saranno analizzate chiaramente le motivazioni che mi hanno portato a scegliere come soluzione questo tipo di componente software durante la realizzazione di un progetto web.

Requisiti

Per poter proseguire agilmente nella lettura di questo articolo è necessaria la conoscenza dei seguenti concetti:

Contesto applicativo

Dunque, dopo aver rimesso a fuoco i concetti fondamentali alla base di questo articolo (pattern singleton, cache ed OOP), passo a descrivervi brevemente ciò che ho implementato e le motivazioni che mi hanno spinto alla scelta del patter Singleton.

Partiamo dal contesto: stiamo parlando del sito che ho creato per la presentazione della mia figura professionale, di come l'ho pensato e di come l'ho progettato.

Tutto il contenuto presente sul sito è conservato in documenti XML validati attraverso l'utilizzo di una DTD specifica. Questi documenti vengono wrappati da apposite classi PHP in oggetti che vengono utilizzati da altre Factory Classes per completare il codice XHTML del template PHP, che viene inviato al browser utente, con i contenuti richiesti.

Questo in breve è il modo in cui ho architettato il sito perchè mi consente di avere completamente separati i contenuti, cioè i dati, le logiche applicative e la parte di view(pattern MVC: Model View Controller).

Le sessioni utente

Analizziamo ora cosa succede quando un utente, e poi un'altro, avviano una nuova sessione sul sito.

Arriva il primo visitatore che (per semplicità) ha richiesto la home page:

  1. viene caricato il file homepage.xml in una istanza della classe DOMDocument;
  2. vengono caricati allo stesso modo il menu principale ed il footer;
  3. tutti questi dati XML vengono wrappati in classi e sotto-classi del tipo MAPage;
  4. la classe HTMLFactory espone i dati in formato XHTML completando un template PHP;
  5. la pagina richiesta viene visualizzata dal browser.

La seconda sessione si avvia richiedendo la pagina articoli.php:

  1. viene caricato il file articoli.xml in una istanza della classe DOMDocument;
  2. vengono caricati allo stesso modo il menu principale ed il footer;
  3. viene attaraversato ricorsivamente il filesystem per creare il doc menu_laterale_articoli.xml
  4. tutti questi dati XML vengono wrappati in classi e sotto-classi del tipo MAPage;
  5. la classe HTMLFactory espone i dati in formato XHTML completando un template PHP;
  6. la pagina richiesta viene visualizzata dal browser.

Riflessioni e Considerazioni

partiamo dalle considerazioni più semplici: perchè ogni volta devo ricaricare il menù principale ed il footer?
Anche pensando di stoccarli nell'array $_SESSION[] dovrei quanto meno caricare questi documenti una volta per ogni visitatore del mio sito.

Ora, più seriamente, quante volte mi sarà capitato di modificare un documento dopo averlo uploadato? Pochissime volte: penso meno di 5. Quindi, a ben vedere, è possibile e ragionevole ipotizzare che siccome un documento non viene mai modificato, dopo essere stato caricato per la prima volta da una sessione applicativa potrebbe essere messo a disposizione di tutte le successive visite che faranno i miei utenti.

Quindi effettivamente se ogni volta che venisse richiesta una pagina i contenuti fossero già wrappati in oggetti MAPage, e quindi già a disposizione, eviterei tutte le fasi necessarie al caricamento in memoria dei dati e rimarrebbe da eseguire solo la fase di creazione del codice XHTML ed il completamento del template PHP.

In quest'ottica ridurrei al minimo tutte le attività che richiedono più risorse ed impiegano necessariamente più tempo ad eseguire il task e potrei nello stesso tempo offrire ai miei utenti delle performance migliori riducendo il carico di lavoro del web-server.

Ovviamente è possibile considerare di ibernare definitivamente alcuni stati applicativi e come nel mio caso è meglio che alcune situazioni siano stoccate solo per un ragionevole arco di tempo. Per esempio, se consideriamo il menù laterale della pagina degli articoli non sarebbe utile cacharlo all'infinito perchè altrimenti un nuovo articolo(cioè un nuovo documento XML) non apparirebbe mai nell'elenco; se invece pensiamo ad un singolo articolo, siccome possiamo ragionevolmente affermare che una volta pubblicato non lo modificheremo mai, è naturale prendere in considerazione l'idea di caricarlo in cache una volta per sempre.

La soluzione: una Cache Singleton

Eh si, è evidente che ciò che sto cercando è proprio questo: una cache singleton parametrizzabile in termini di tempo di vita concesso agli elementi in essa caricati.

C'è un piccolo problema però: per questioni di sicurezza non posso utilizzare l'array globale $GLOBALS[] per poter salvare e richiamare i dati della cache e neanche utilizzando delle proprietà statiche posso semplicemente tenere vivo lo stato applicativo dei miei oggetti tra una pagina ed un'altra: PHP permette di farlo solo all'interno della stessa pagina.

Considerando anche altri orizzonti, tipo la possibilità di accedere ai dati in cache anche da altri applicativi in remoto (sto progettando una estensione per FireFox che cambia notevolmente il modo di fruire il mio sito), ho optato per risolvere elegantemente il problema tramite i processi di serializzazione e de-serializzazione della cache.

MASingletonCache

Questo è il nome della classe che ho creato e che ormai lavora magnificamente da diverso tempo.

Riassumendo io avrei bisogno per il caching dei dati di un componente con queste caratteristiche:

  • Deve essere dinamico e scalabile;
  • Deve ricaricarsi ogni tot minuti/ore/giorni;
  • deve essere ATOMICO: contiene tutti i metodi di factory necessari;
  • Deve gestire dati applicativi;
  • Deve essere performante;
  • Deve essere decisamente Singletone;
  • Deve gestire accessi concorrenziali;

Tutto questo è riassunto in modo migliore nel seguente caso d'uso UML:Specifiche del componente software singleton

Per quanto riguarda l'architettura del componente, la classe implementa alcune interfacce ed estende altre classi che già utilizzo: la situazione è perfettamente descritta nel seguente diagramma di classe UML:architettura del componente software singleton.

Come si può notare, il cuore del sistema di caching è rappresentato dall'interfaccia IMAList e dalla classe MAList che la implementa.

La classe MASingletonCache fondamentalmente incapsula la lista che funge da cache e garantisce che il suo utilizzo sia aderente alle specifiche del pattern Singleton. Poi ci sono l'interfaccia IMACachable e la classe MACachable che la realizza.

La classe MASingletonCache si occupa di verificare che ogni oggetto cachato implementa l'interfaccia IMACachable in modo tale che possa essere sempre verificato da quanto tempo un dato risiede in cache.

Rinfrescati i concetti basilari e chiarito lo scenario in cui mi sono trovato, siamo pronti per affrontare il codice sorgente dei singoli componenti che fanno parte del mio sistema di caching.

Iniziamo ad analizzare il codice sorgente delle classi di Model, che rappresentano i contenuti da visualizzare e conservare.

L'Interfaccia IMAList

interface IMAList{
  public function set($key, $obj);
  public function get($key);
  public function remove($key);
  public function rebuild();
  public function size();
  public function getItems();
  public function setItems($array);
  public function contains($key);
}

Non c'è molto da dire, le particolarità di questa interfaccia sono racchiuse nell'utilizzo di oggetti abbinati ad una chiave specifica (fossimo in ambiente Java potrebbe essere una HashTable od una HashMap) e nel metodo pubblico rebuild(): effettivamente il nome scelto per la firma del metodo poteva essere migliore in quanto (lo vedremo in seguito) non si occupa di ricostruire la lista, ma è stato pensato come un punto di ingresso per lavorare in maniera differente i dati stoccati. Vedremo analizzando la classe MAList che in questo metodo viene attraversata la cache in cerca di elementi nulli o de-referenziati da rimuovere, in modo da alleggerirla il più possibile e ridurre al minimo i tempi di ricerca dei dati al suo interno.

La classe MAList

class MAList implements IMAList {
  private $items = array();
 
  /**
   * Riceve un valore identificativo univoco e 
   * l'oggetto da conservare.
   * @param String $key Identificativo Univoco;
   * @param mixed  $obj Oggetto da conservare;
   */
  public function set($key, $obj){
    if($this->isValidVar($key) 
        && $this->isValidVar($obj))
      $this->items[$key] = $obj;
  }

  /**
   * Riceve un valore identificativo e restituisce 
   * l'oggetto immagazzinato identificato da $key.
   * @param  String $key  Identificativo Univoco;
   * @return mixed        Oggetto immagazzinato;
   */
  public function get($key){
    if($this->isValidVar($key) && 
        array_key_exists($key, $this->items))
      return $this->items[$key];
  
      return null;
  }
  /**
   * Rimuove l'elemento identificato 
   * con $key dalla lista.
   * @param String $key Valore univoco 
   *                    dell'elemento da rimuovere.
   */
  public function remove($key){
    if($this->isValidVar($key) && 
        array_key_exists($key, $this->items)) 
      unset($this->items[$key]); 
  }
  /** Rimuove tutti i dati nulli dallo stack. */
  public function rebuild(){
    foreach($this->items as $k=>$v){
      if(!$this->isValidVar($v))
        unset($this->items[$k]);
    }
  }
  /**
   * Restituisce la dimensione dello stack.
   * @return  Integer  Il numero di oggetti 
   *                   presenti in lista;
   */
  public function size(){
    $c = count($this->items);
    return (!$c) ? -1 : $c;
  }
  /**
   * Verifica l'esistenza di un determinato dato.  
   * @param  String  $key  La chiave abbinata al dato di 
   *                       cui verificarne l'esistenza;
   * @return  Boolean      true se il dato esiste, 
   *                       altrimenti false;
   */
  public function contains($key){
    return array_key_exists($key, $this->items);
  }
  /**
   * Restituisce l'intero stack. 
   * Prima di resituirlo esegue una chiamata
   * al metodo rebuild(). 
   * @return  Array  Tutta la lista;
   */
  public function getItems(){
    $this->rebuild();
    return $this->items;
  }
  /**
   * Sostituisce l'intero stack.  
   * @param  Array  Il nuovo stack con cui 
   *                sostituire il vecchio;
   */
  public function setItems($arr){ 
    $this->items = $arr;
  } 
  /**
   * Verifica se il parametro ricevuto 
   * e' utilizzabile.
   * @return Boolean  true se e' utilizzabile, 
   *                  altrimenti false;
   */
  private function isValidVar($var){
    return (isset($var) && !empty($var))
       ? true : false;
  }
}

La classe incapsula un array nella proprietà privata items ed espone i metodi per il salvataggio/modifica/lettura/ricerca/elimininazione dei dati presenti in tale proprietà.

Qui puoi vedere cosa realmente accade quando viene chiamato il metodo MAList::rebuild(): il metodo viene richiamato solo quando si accede al membro pubblico MAList::getItems() per eseguito un controllo su ogni singolo item presente prima di restituire tutta la lista di oggetti.

IMAList e MAList rappresentano il garante e il gestore dei nostri dati.

Passiamo ora ad analizzare ciò che per me vuol dire essere un oggetto cachabile.

Con il termine cachabile intendo un determinata tipologia di oggetti sui quali la mia cache può eseguire determinati controlli ed eventualmente agire sugli stessi di conseguenza.

Se ridiamo un'occhiata alle features descitte nel caso d'uso UML, sopra esposto, si può facilmente capire quali sono i benefici che l'interfaccia IMACachable dona al progetto.

Avere una cache che è in grado di distruggere e ricaricare i propri items in base ad un arco di tempo prestabilito è sicuramente una ottima caratteristica che rende il sistema molto elastico e facilmente riadattabile in altri progetti.

L'interfaccia IMACachable

L'interfaccia è molto semplice quanto utile. Essa si compone di un solo metodo pubblico che deve essere implementato dalla classe che la realizzerà.

interface IMACachable{
  /**Restituisce il valore timestamp di quando 
   * è stata realizzata istanza di classe. */
  public function getCreationUTS();
}

La classe MACachable

Questa classe fondamentalmente è il classico JavaBean ma oltre ad implementare l'interfaccia IMACachable racchiude due proprietà molto importanti:

  • key, la chiave con cui viene stoccato nella cache;
  • data, il vero oggetto sa cachare;

Ritengo strategico, in un ottica nella quale ogni oggetto possa auto-verificarsi a favore della cache, che l'oggetto conservi in se stesso la stessa chiave che la classe MASingletonCache utilizza per riferircisi.

class MACachable implements IMACachable{
  private $creationUTS;
  private $key;
  private $data;
 
  public function __construct($key=null,$data=null){
    $this->creationUTS = time();
    $this->key = $key;
    $this->data = $data;
  } 
 
  public function getCreationUTS(){
    return $this->creationUTS;}
  public function getKey(){return $this->key;}
  public function setKey($key){$this->key = $key;}
  public function getData(){return $this->data;}
  public function setData($data){$this->data = $data;} 
}

Ora che conosciamo come sono strutturati i capisaldi del sistema, possiamo addentrarci nel codice sorgente della classe MASingletonCache per capire come le sue logiche interne agiscono sui dati cachati e come è possibile garantire sempre l'utilizzo di una istanza unica di classe.

La classe MASingletonCache

class MASingletonCache 
      extends MAList 
      implements IMACachable {
/**
 * Arco di tempo massimo in cui trattenere 
 * i dati in cache.
 * Espresso in secondi (max. 5 min)
 */
const DEFAULT_ITEMS_TIME_OF_LIFE = 300;

/**
 * Se utilizzato, gli oggetti cachati 
 * non scadranno mai.
 */
const INFINITY_ITEMS_TIME_OF_LIFE = -999;

/**
 * Se true, inibisce il funzionamento della cache.
 * Utilizzato in fase di test e debug.
 */
private static $suspended = false;

/**
 * Unica istanza della classe. 
 * Garantisce l'utilizzo del 
 * design pattern Singleton.
 */
private static $instance = null;

/**
 * Unix Timestamp del momento di 
 * creazione della cache.
 */
private $creationUTS;

/**
 * Indirizzo del file su cui eseguire 
 * le operazioni di serializzazione della cache.
 */
private $storePath;

/**
 * Secondi di vita concessi agli oggetti cachati
 */
private $itemsTimeOfLife;

private function __construct($path,$itemsTOL){
  $this->creationUTS = time();
  $this->storePath = $path;
  $this->itemsTimeOfLife = $itemsTOL;
} 

/**
 * Unico punto di accesso per la creazione 
 * di una istanza di classe.
 * L'istanza di classe creata sarà unica 
 * in tutta l'applicazione (Singleton).
 * 
 * @param $path  String  Path per la creazione del file 
 *                       nel quale salvare lo stato 
 *                       dell'istannza di classe;
 * @param $itemsTOL Integer  Secondi di vita concessi ad 
 *                           un oggetto cachato prima di 
 *                           essere rimosso dalla cashe. 
 *                           300secondi è il valore di default   
 * @return  MASingletonCache Istanza di classe Singleton.
 */
public static function getInstance($path,$itemsTOL=300){
  if(!isset($path) || empty($path))
    throw new UnAcceptedValueException
      ('Impossibile recuperare la cache Singleton',6485986);
 
  if(!self::$suspended)
    self::$instance = unserialize(file_get_contents($path));

  echo "<!-- Tentativo recupero cache da IO: " . 
    isset(self::$instance) . "; Cache Sospesa: " . 
    ((self::$suspended) ? '1' : '0' ) . " -->\n";
 
  if(!self::$instance){
    self::$instance = new MASingletonCache($path,$itemsTOL);
    echo "<!-- nuova instanza di MASingletonCache -->\n";
  }
  return self::$instance;
}

/**
 * Prima di eseguire la serializzazione della cache 
 * esegue una compattazione degli oggetti presenti in cache. 
 * @see self::compact()
 */
public function store(){
  echo '<!-- Inizio Fase di Storage -->'."\n";
  $this->compact();
  $s = serialize(self::$instance);
  $fp = fopen($this->storePath, "w");
  fwrite($fp, $s);
  fclose($fp);
  echo '<!-- Fine Fase di Storage -->'."\n";
}

/**
 * Verifica la scadenza di ogni oggetto in cache.
 * Se un oggetto è scaduto o se non è una istanza 
 * della classe MACachable viene rimosso. 
 * @see MACachable
 */
private function compact(){
  $arr = parent::getItems();
  foreach($arr as $k=>$v){
    echo "\t" . '<!-- Oggetto in Analisi: "' 
      . $k . '" tipo: "' . get_class($v) . '" -->';
  
    if(!isset($v) || get_class($v) != 'MACachable'){
      parent::remove($k); 
      echo '<!-- Oggetto RIMOSSO perchè di tipo'
        . ' diverso da \'MACachable\'. -->'."\n";
      continue;
    }

    $dataCached = $v->getData();  
    if(!isset($dataCached) || empty($dataCached))
    unset($arr[$k]);
  
    $seconds = time() - $v->getCreationUTS(); 
    echo '<!-- Oggetto conservato in cache da ' 
      . $seconds . ' sec.(Limite=' . $this->itemsTimeOfLife 
      . 'sec.) -->';

    if($this->itemsTimeOfLife 
        != self::INFINITY_ITEMS_TIME_OF_LIFE){ 
      if( $seconds >= $this->itemsTimeOfLife){
        parent::remove($k); 
        echo '<!-- Oggetto RIMOSSO. -->';
      }
    }
    echo "\n";
  }
}

/** ---------- SATISFY IMACachable --------------- **/
/**
 * Restituisce il valore unixtimestamp 
 * del momento della creazione della cache.
 */
public function getCreationUTS(){
  return  $this->creationUTS;
}
}

Dettagliatamente

Dando uno sguardo alla dichiarazione della classe possiamo notare che:

  • la classe eredita tutte le proprietà ed i metodi di MAList;
  • è anche un oggetto cachabile;
  • MASingletonCache::getInstance(String, int) è l'unico entry point disponibile;

Avere un costruttore privato è la base del paradigma Singleton infatti è solo garantendo il passaggio obbligato per il metodo statico e pubblico 'getInstance(String, int)' che possiamo assicurare che esisterà una e solo una istanza della classe, ed in particolare sarà rappresentata dalla proprietà privata e statica 'instance'.

Il meccanismo alla base del pattern Singleton è molto semplice: ad ogni chiamata di MASingletonCache::getInstance() viene verificato che $instance sia valorizzata: in caso affermativo viene restituita immediatamente al chiamante altrimenti viene settata con una nuova istanza della classe creata al volo e poi restituita.

In questo caso però c'è molto di più: il costruttore riceve 2 parametri che, in ordine, rappresentano l'indirizzo del file su cui eseguire la serializzazione dell'oggetto $instance e il numero di secondi di vita concessi ad ogni oggetto conservato. Al termine del 'count-down' l'oggetto viene rimosso dalla cache.

La fase di Serializzazione

Questa attività comprende le fasi di consolidamento, di lettura dei dati e creazione degli oggetti che quei dati rappresentano.
Nel mio componente il consolidamento dei dati avviene quando si accede al membro pubblico MASingletonCache::store() ed invece i dati vengono caricati in memoria quando si chiama il metodo MASingletonCache::getInstance(String, int).

Di fatto serializzare un oggetto significa riportare il suo stato interno in un formato tale che lo stesso oggetto possa essere ricreato rileggendo lo stesso formato di dati.

In questo caso non disponendo di un Server DB serializzo l'istanza di classe su un file di testo che arbitrariamente ho scelto e di cui ho passato l'indirizzo alla classe, come primo parametro del metodo MASingletonCache::getInstance(String, int).

Vale la pena soffermarsi a riflettere su come l'introduzione della serializzazione ha cambiato questa classe.
Effettivamente la classe è scritta rispettando tutti i vincoli che il design pattern Singleton richiede:

  • avere un unico costruttore privato;
  • fornire un metodo getter statico che ritorna sempre la stessa istanza della classe;
  • memorizzare il riferimento all'unica istanza in un attributo privato anch'esso statico;

Però, non è vero in assoluto che l'istanza restituita rappresenti sempre lo stesso insieme di dati.

Nuovi orizzonti

E' vero che l'istanza generata è unica ed è sempre la stessa ma, semplicemente parametrizzando il file su cui serializzare, ho dato alla classe la caratteristica di poter rappresentare dati diversi con la stessa istanza di classe. Questo potrà sembrare banale e sbagliato ma innanzi tutto in realtà è frutto dell'impossibilità (gentile concessione del servizio di hosting che utilizzo) di poter utilizzare la variabile globale $GLOBALS[] per salvare dati a livello globale e l'incapacità di PHP (almeno fino ad oggi) di conservare stati applicativi tra una pagina ed un'altra.

Soprattutto, però, mi offre la possibilità di organizzare le mie cache, per esempio, in base al tempo di sopravvivenza dei dati in esse conservati.

Così facendo deciderò di serializzare sul file './permanent.cache' tutti quei dati che non devono mai scadere:

$cache = MASingletonCache('./permanent.cache',
           MASingletonCache::INFINITY_ITEMS_TIME_OF_LIFE);

Mentre utilizzerò il file './temporary.cache' per i dati che ogni 10 minuti verranno rimossi dalla cache:

$tempCache = MASingletonCache('./temporary.cache', 600);

Il membro pubblico MASingletonCache::store() non esegue solo il consolidamento dei dati, ma in esso avviene l'eliminazione degli oggetti nulli o deferenziati e la verifica che nessuna istanza di MACachable si scaduta. Nel caso contrario, ossia quando un oggetto cachato risulta essere scaduto, questo stesso viene rimosso dalla cache. Il risultato è che la fase di scrittura, e successivamente quella di ri-lettura, terrà conto solo dei dati considerati ancora validi risultando essere più snella e veloce possibile.

Esempio di utilizzo di MASingletonCache

include_once 'IMAList.php';
include_once 'MAList.php';
include_once 'IMACachable.php';
include_once 'MACachable.php';
include_once 'MASingletonCache.php';

/** Carico la cache permanente */
$permanentCache = MASingletonCache('./permanent.cache',
    MASingletonCache::INFINITY_ITEMS_TIME_OF_LIFE);

/** Pagina di presentazione che deve essere sempre disponibile */
$splashpage = (isset($permanentCache->get('page.splashpage'))) 
  ? $permanentCache->get('page.splashpage') 
  : new MACachable('page.splashpage', 
        new SplashPage('Ciccio_Pasticcio'));
 
echo $splashPage->html();
$permanentCache->store();

/** Carico la cache 'volatile' */
$tempCache = MASingletonCache('./temporary.cache', 600);

/** Ovviamente un menu dinamico non potrà  
 * essere un dato cachato in modo permanente */
$menuDownload = ( isset($tempCache->get('menu.download')) ) 
  ? $tempCache->get('menu.download') 
  : new MACachable('menu.download', new Menu('Downloads'));
 
echo $menuDownload->html();
$tempCache->store();

Conclusioni

Il sistema di caching presentato a mio avviso centra tutte le specifiche descritte nel caso d'uso UML riportato precedentemente, in più risulta essere sicuramente di facile riutiizzo in altri progetti.

La fase di serializzazione avviene tramite le funzioni standard offerte dalle librerie PHP ed in modo trasparente.

In questo articolo la cache viene serializzata in 'chiaro' ma è possibile trasformare i dati da serializzare utilizzando algoritmi di cifratura ed anche di compressione.

Spero che l'articolo ed i concetti espressi ti siano piaciuti e che tu abbia potuto apprezzare la qualità del codice PHP qui offerto.

Alla prossima,
MA.

0 commenti:

Posta un commento

Non ti è chiaro qualcosa?
No problem, posta il tuo dubbio ;)

..... e ricordati di firmarlo!