Object Oriented PHP: concetti base
PHP è un linguaggio di programmazione sostanzialmente procedurale che consente, in modo limitato, l'adozione di uno stile OO (Object Oriented). Questo è il primo articolo di una serie che ci porterà ad approfondire l'argomento, non si prefigge di illustrare in modo completo i concetti della programmazione ad oggetti (cosa che richiederebbe lo studio di molte caratteristiche che PHP ancora non supporta), bensì di fornire una base di partenza a chi intende iniziare ad adottare questa tecnica apparentemente poco intuitiva.
Per un' utile panoramica su tutte le tematiche che svilupperemo consiglio la lettura di La programmazione a oggetti con PHP4.
Confronto tra oggetti e procedure
Il primo passo per comprendere le differenze tra i due stili è fare subito un confronto diretto:
<?php
/*
Esempio procedurale
*/
$myFile="xyzFile.txt" ;
$fp=fopen($myfile, 'r') or die("Impossibile aprire il file");
echo(fread($fp, filesize($myFile))) ;
$fclose($fp) ;
?>
L'operazione svolta è molto semplice, leggere un file di dati e inviarne il contenuto all'output, lo stile è evidentemente procedurale e dovrebbe essere familiare a chiunque conosca PHP. Ecco invece come potremmo tradurre lo script appena visto in senso Object Oriented.
<?php
/*
Esempio OOP
*/
include_once("DataFileClass.php") ;
$x=new DataFile("xyzFile.txt") ;
$x->open('r') ;
$x->output() ;
$x->close() ;
?>
$x è un oggetto e DataFile(), open(), output(), close() sono i suoi metodi: in un certo senso rappresentano le azioni che $x può compiere, quella che viene chiamata l'interfaccia dell'oggetto e che maschera ciò che avviene al suo interno.
DataFile() è il metodo "costruttore" e ha il compito di creare e inizializzare l'oggetto.
Vedremo tra poco cosa tutto questo voglia dire realmente.
Incapsulamento
Un oggetto è un componente di codice autonomo che incapsula uno "stato" e un "comportamento".
Tra l'esempio procedurale e quello a OO è evidente un differenza fondamentale: il secondo maschera totalmente i dettagli interiori del proprio funzionamento, le variabili $myFile e $fp scompaiono sotto l'interfaccia.
Servirsi di un oggetto è come condurre un automobile: per utilizzare il veicolo non serve conoscere molto di quanto accade realmente nel motore, basta avviarlo e sapere cosa fare con volante e pedali. Fuor di metafora questo significa che non dobbiamo preoccuparci del contenuto del file incluso ("DataFileClass.php"), infatti ci è sufficiente conoscere l'interfaccia dell'oggetto, cioè le operazioni che può effettuare attraverso i metodi (pubblici).
La programmazione procedurale mantiene distinti dati e procedure, al contrario l'oggetto è un componente che racchiude in sè sia dati (le proprietà) che funzioni (i metodi): in modo sbrigativo potremmo definire gli oggetti come array in grado di contenere azioni e avere comportamenti.
L'incapsulamento richiederebbe che le proprietà di un oggetto non siano manipolabili direttamente in questo modo: $nomeIstanza->nomeProp=$valore ; dovrebbero essere accessibili soltanto attraverso i metodi, ad esempio $nomeIstanza->setNomeProp($valore) ;
Seguire questa regola (anche se in PHP per ragioni di prestazioni può essere utile trasgredire) rende gli oggetti riutilizzabili, e in generale permette di modificarne il comportamento interno mantenendo inalterata l'interfaccia: per tornare la paragone con l'automobile pensiamo a cosa accaddrebbe se per assurdo in una Ferrari sostituissimo il motore con quello di un'utilitaria... noteremmo la differenza nelle prestazioni dell'auto, ma volante e pedali resterebbero l'unica interfaccia possibile e non dovremmo imparare ad usare nuovi controlli.
Le classi
L'oggetto è uno dei 6 tipi di dati conosciuti da PHP, tuttavia si distingue nettamente dagli altri in quanto ogni oggetto deve le proprie caratteristiche fondamentali ad un'entità definita dal programmatore: la classe.
Da una classe può essere creato un numero arbitrario di oggetti che ne costituiscono le istanze, quindi tra classe e oggetto fondamentalmente intercorre la differenza che esiste tra l'idea di cane e il cane del nostro vicino (ovvero un animale ben preciso).
La classe è il modello da cui un oggetto prende forma, e l'oggetto è manifestazione concreta della classe.
I passi per creare una classe
Programmare OO NON significa servirsi di oggetti predefiniti, come abbiamo fatto poco fa (o come avviene spesso quando si utilizzano linguaggi come Javascript o Vbscript) la programmazione ad oggetti consiste nel creare classi efficienti in grado di cooperare tra loro, e la difficoltà principale per chi è abituato ad uno stile tradizionale consiste nel mutare mentalità e modo di coincepire le applicazioni.
Vediamo le pricipali fasi attraverso cui procedere:
- Ragionare astrattamente: pensare prima di tutto all'interfaccia che desideriamo fornire all'oggetto, ovvero ai metodi, al modo in cui lo utilizzeremo e alle relazioni con altri oggetti.
- Suddivisione dei compiti: chiedersi quale compiti minimi dovrà svolgere un ogetto istanza di questa classe. Gli oggetti devono essere componenti quanto più piccoli possibile e dobbiamo avvalerci dell'ereditarietà o della delegazione (concetti che affronteremo nel prossimo articolo) per estendere le loro funzionalità.
- Cominciare a costruire i metodi uno alla volta: i metodi non sono altro che funzioni, quindi la cosa non dovrebbe risultare una gran novità.
Nella pagine successive esamineremo la classe d'esempio "DataFile".
Una semplice classe per il filesystem
Ecco la classe che abbiamo usato per la lettura del file a pag 1:
<?php
// 1
class DataFile{
// 2 proprietà tutte private
var $_myFile ;
var $_openMode ;
var $_fp ;
// 3 costruttore
function DataFile($percorsoFile){
$this->_myFile=$percorsoFile ;
if(!$this->exists()){
$this->_create() ;
}
}//END Constructor
/***
METODI PRIVATI
***/
// 4
function _create(){
touch($this->_myFile) ;
}
// 5
function _isOpenReadable(){
if($this->getOpenMode()!=''){
if(!($this->getOpenMode()=='w' || $this->getOpenMode()=='a')){
return TRUE ;
}
}
else{
return FALSE ;
}
}//END _isOpenReadable
/***
FINE METODI PRIVATI
***/
// 6 controlla i permessi
function isReadable(){
return(is_readable($this->_myFile)) ;
}
// 7
function exists(){
return(file_exists($this->_myFile)) ;
}
// 8
function open($modo){
$this->_openMode=$modo ;
$this->_fp=fopen($this->_myFile,$modo) or die("Impossibile aprire il file") ;
}//END open()
//9
function getOpenMode(){
return($this->_openMode) ;
}//END getOpenMode()
// 10
function readAll(){
if(!$this->_isOpenReadable()){
die("La modalità di apertura non consente la lettura") ;
}
elseif(!$this->isReadable()){
die("Mancano i permessi per la lettura") ;
}
else{
$dati=fread($this->_fp, filesize($this->_myFile)) ;
return($dati) ;
}
}//END readAll()
// 11
function output(){
echo($this->readAll()) ;
}//END output()
// 12
function close(){
fclose($this->_fp) ;
$this->_openMode='' ;
}
}//END CLASS
?>
Quanto codice per riprodurre funzionalità così semplici, non è vero? Eppure ci è bastato scriverlo una volta soltanto, d'ora in poi le cose saranno molto più veloci. La nostra classe è volutamente spoglia e copre soltanto una parte esigua delle filesystem functions, va bene così visto che ci interessa soltanto evidenziare alcuni dei passaggi attraverso i quali si è arrivati a crearla.
È stata suddivisa in settori numerati per poterla commentare punto per punto al prossimo paragrafo.
Scomponiamo il modello
La parola chiave "this": internamente l'istanza fa riferimento a se stessa e a i suoi membri (proprietà e metodi) attraverso "this". "$this" identifica l'oggetto, mentre "$this->..." individua sempre un membro dell'oggetto.
//1 Il nome della classe
Non c'è molto da dire se non che per convenzione la prima lettera è maiuscola.
//2 Le proprietà
Questo è il punto in presentiamo in anticipo quelle che saranno le proprietà dell'oggetto, non è obbligatorio ma molto consigliabile in quanto rende il codice molto più leggibile.
Le proprietà sono globali all'interno dell'oggetto (non occorre dichiararle global nei metodi): internamente si fa riferimento ad esse attraverso $this->nomeprop, esternamente attraverso $istanza->nomeprop. Le proprietà possono contenere qualsiasi tipo di dato riconosciuto da PHP (altri oggetti compresi) e come i metodi possono essere pubbliche o private, chiariremo tra poco questi concetti.
//3 Il metodo costruttore
Serve ad istanziare l'oggetto e ad inizializzarne lo stato: in questo modello fornisce il nome del file di dati su cui intediamo operare, e lo crea nel caso in cui questo non sia già presente.
Il costruttore non può restituire alcun valore poichè già restituisce l'oggetto istanza della classe. Ricorrere ad un costruttore è considerata quasi sempre buona pratica di programmazione, anche se tecnicamente nulla vieta di prevedere un metodo init() e fare qualcosa del genere.
<?php
$x=new DataFile ;
$x->init("xyzFile.txt") ;
?>
//4-5 Membri privati
"Privato" significa non facente parte dell'interfaccia dell'oggetto, svolge esclusivamente una funzione interna e nascosta: nel nostro esempio _create() viene utilizzato esclusivamente dal metodo costruttore per creare il file qualora non esista. _isOpenReadable() è un ulteriore esempio, ha il solo scopo di aiutare il metodo open() a verificare se l'apertura del file è coerente con l'operazione che si vuole compiere. Un apertura di tipo "w" che preceda un tentativo di lettura provocherebbe un errore.
A partire da PHP v. 4.1 il nome dei membri privati deve essere preceduto dal simbolo "_", c'è una ragione ben precisa alla base di questa convenzione e ce ne occuperemo in futuro.
Ovviamente anche I metodi pubblici possono svolgere compiti interni all'oggetto: è il caso di readAll() che viene utilizzato da output() ma fa anche parte dell'interfaccia (commenti 10 e 11).
Si già detto di come il principio dell'incapsulamento richieda che non si possa accedere direttamente alle proprietà di un oggetto, tuttavia in PHP nulla impedisce di raggiungere proprietà e metodi privati tramite l'istanza in questo modo $istanza->_metodoPriv(); in linguaggi con un miglior supporto agli oggetti riceveremmo un errore. Questa è una delle moltissime lacune a cui si porrà rimedio con la release di PHP 5.
Un'altra piccola ma fastidiosa limitazione è l'impossibilità di accedere direttamente all' oggetto restituito da un metodo dell'oggetto pricipale.
Detta così la cosa risulta un po' confusa, quindi meglio fare un esempio concreto:
<?php
$x->new myClass() ;
// NO
//echo($x->metodo()->prop) ;
// SI
$y=$x->metodo() ;
echo($y->prop) ;
?>
Perchè OOP?
Adottare uno stile OO costringe gli sviluppatori a pensare una struttura prima di fare: come conseguenza la logica del programma scorre su binari abbastanza precisi, rendendo meno frequenti i ripensamenti e in ogni caso più semplici eventuali correzioni.
Il dover necessariamente prima creare una struttura può apparire una perdita di tempo, e alle volte lo è quando si tratta di svolgere compiti semplici, ma il concetto è quello di faticare un po' più all'inizio per poter lavorare più agevolmente e con maggiore rapidità in seguito.
Sicuramente un buon programmatore è in grado di scrivere codice procedurale modulare e ben organizzato, tuttavia ragionare ad oggetti facilita di molto il raggiungimento dello scopo, specialmente quando si ha a che fare con progetti di grandi dimensioni.
È bene ricordare che al momento attuale l'esecuzione di codice PHP Object Oriented comporta un calo nelle prestazioni (rilevabile solo in condizioni di traffico elevato) rispetto a quello tradizionale.
Conclusioni
Sono stati affrontati soltanto i concetti più semplici della programmazione OO, la classe che abbiamo creato è piuttosto generica e non ci è molto utile presa isolatamente: a partire dal prossimo articolo ci occuperemo di caratteristiche come ereditarietà, polimorfismo e delegazione, e ci sarà modo di capire come la genericità sia un pregio e non un limite.
Introduzione
In questo articolo approfondiremo alcuni concetti fondamentali della programmazione object oriented: ereditarietà e polimorfismo. È indispensabile aver letto Tecniche per programmare a oggetti (1) poichè ripartiremo dagli esempi già illustrati.
Ereditarietà
Si tratta di una caratteristica che consente al programmatore di organizzare il codice in modo più razionale e risparmiare molto tempo: una classe (child class) può ereditare proprietà e metodi di un'altra (parent class) specializzandone le funzioni.
Le istanze della classe figlia presenteranno proprietà e metodi delle istanze della classe madre con le differenze o aggiunte specificate appositamente.
Come sempre un paragone con la vita reale ci viene in aiuto: pensiamo di dover descrivere a una persona che ha poche nozioni di scienze naturali le caratteristiche di alcuni animali...ad esempio un cane, un gatto e una balena. Potremmo parlare di un animale alla volta, e dilungarci sulla carateristiche di ognuno, oppure spiegare prima di tutto che cosa sia un mammifero (partorisce, allatta i piccoli etc.etc.) e concentrarci soltanto successivamente sulle peculiarità specifiche di ogni animale... quale pensate che sarebbe il modo più intelligente di procedere?
L'ereditarietà va usata bene, cioè tenendo presente che lo scopo non è quello di "fare in fretta", bensì di scrivere codice più ordinato ed efficiente: non dobbiamo far ereditare una classe da un'altra soltanto perchè ci fa risparmiare tempo, l'ereditarietà deve procedere dal generale al particolare.
La classe derivata deve essere spontaneamente identificabile come una specializzazione della classe da cui deriva (rapporto "is a", ovvero "è un tipo di").
Non è una buona idea forzare le parentele e, se il nostro unico scopo è quello di risparmiare tempo e codice, possiamo servirci dell'associazione (o delegazione) e dell'aggregazione: due concetti che affronteremo più avanti.
Tra poco invece vedremo come applicare in modo concreto l'ereditarietà, partendo dalla classe elementare costruita nella prima parte dell'articolo.
Ricavare una classe figlia
<?php
/*
Includiamo la definizione della classe vista nell'articolo precedente
*/
include_once("DataFileClass.php") ;
class MimeFile extends DataFile{
/*
De nuove proprietà (private) si aggiungono a quelle della classe parent
*/
var $_mime ;
var $_disposition ;
function MimeFile($path, $mimeType){
/*
È necessario richiamare il costruttore della classe parent
*/
$this->DataFile($path) ;
$this->_mime=$mimeType ;
}
/*
Ridefinisce il metodo output rispetto alla classe madre,
la nuova definizione sovrascrive quella originaria (overriding)
*/
function output(){
/***
Gli header necessari ad identificare il MIME TYPE e a far sì che il browser visualizzi correttamente i dati
***/
header('Content-type: '.$this->mime);
header('Content-Disposition: inline; filename="'.$this->_myFile.'"');
/*
Richiamiamo il metodo originario
*/
parent::output() ;
}//END output()
}//END class
/*
Per ora l'utilizzo rimane simile a quello della classe DataFile
*/
$x=new MimeFile("test.Pdf","application/Pdf", "inline") ;
$x->open('rb') ;
$x->output() ;
$x->close() ;
?>
La classe derivata MimeFile sarebbe identica alla classe base, DataFile, però con poco codice in più abbiamo modificato il comportamento ereditato (in grado di mostrare soltanto otput in formato testo): ora, grazie agli header appropriati e ridefinendo con essi il metodo output(), possiamo mostrare anche file di tipo diverso dal solo "text/plain", facendo sì che il browser apra l'apposito programma di lettura (p.es. MsWord, Acrobat Reader ecc.).
Alcune osservazioni:
1) Il costruttore di DataFile non viene richiamato automaticamente da quello della classe derivata, infatti è stato necessario ripetere $this->DataFile($nomePercorso).
2) Solo qualora nella classe derivata venga omesso il costruttore, allora si ha la chiamata automatica a quello della classe originaria, mentre un nuovo costruttore sovrascrive quello parent.
3) L'operatore "::" serve ad eseguire il metodo output() della classe base, quest' operazione nel nostro caso è necessaria in quanto il metodo della classe derivata, utilizzando lo stesso nome, sovrascrive il metodo originario.
4) Tutti i metodi e le proprietà della classe madre non ridefiniti nella dichiarazione della classe figlia sono presenti seppure non visibili.
5) Php non consente l'ereditarietà multipla, ovvero una classe figlia può avere un genitore e un "nonno", ma non è in grado di ereditare contemporaneamente da due genitori. Non è una gran mancanza, anzi molti programmatori considerano l'ereditarietà multipla una fonte di confusione e problemi.
Aggiungiamo alcune migliorie
Vogliamo che lo scopo della classe sia solo quello di visualizzare il file nella sua interezza, e tra l'apertura e la chiusura non dobbiamo svolgere alcuna operazione sui dati contenuti: sarà bene apportare alcune modifiche per fornire alle istanze un' interfaccia migliore.
1) Incorporeremo apertura e chiusura del file nel metodo output(); i metodi open() e close() verranno sovrascritti per impedirne l'utilizzo pubblico negli oggetti MimeFile, mentre per consentire comunque l'apertura (incorporata in output()) ricorreremo ai metodi open() e close() della classe parent (operatore "::").
2) Aggiungeremo altri 2 metodi, show() e download(): il primo visualizza il file (prende il posto di output), mentre il secondo consentirà di scaricarlo.
3) output() diventerà un metodo privato (a uso interno) che fornirà da base per show() e download()
<?php
/*
Includiamo la definizione della classe base
*/
include_once("DataFileClass.php") ;
class MimeFile extends DataFile{
/*
De nuove proprietà (private) si aggiungono a quelle della classe parent
*/
var $_mime ;
var $_disposition ;
function MimeFile($path, $mimeType){
/*
È necessario richiamare il costruttore della classe parent
*/
$this->DataFile($path) ;
$this->_mime=$mimeType ;
}
/*
Ridefiniamo (overriding) il metodo open() per disabilitarlo e far sì che il file sia apribile solo in lettura,
così da evitare danneggiamenti.
*/
function open(){
die('Errore, il metodo open non fa parte dell'interfaccia dell'oggetto') ;
}
function close(){
die('Errore, il metodo open non fa parte dell'interfaccia dell'oggetto') ;
}
/*
Ridefinisce il metodo output rispetto alla classe madre,
la nuova definizione sovrascrive quella originaria (overriding)
*/
function output($disposition, $mime=''){
$mime=empty($mime)?$this->_mime:$mime ;
/*
Gli header che diranno al browser come comportarsi e rendere il contenuto del file
*/
header('Content-type: '.$mime);
header('Content-Disposition: '.$disposition.'; filename="'.$this->_myFile.'"');
parent::open('rb') ;
/*
Richiamiamo il metodo originario
*/
parent::output() ;
parent::close() ;
}//END output()
function show(){
$this->output('inline') ;
}
function download(){
/*
Per forzare il download fingiamo che si tratti di un file .zip
*/
$this->output('attachment', $mime='application/zip') ;
}
}//END class
/*
L'utilizzo ora è il seguente
*/
$x=new MimeFile("test.Pdf","application/Pdf") ;
$x->download() ;
/***
Oppure per visualizzare il file anzichè scaricare
$x->show() ;
***/
?>
Come si vede ora è possibile specificare per ogni file il MIME TYPE adeguato e fare in modo che venga visualizzato correttamente o scaricato.
Ulteriori miglioramenti
Potremmo quasi ritenerci soddisfatti, ma la logica degli oggetti ci consente di andare oltre.
Non sarebbe comodo ad esempio identificare un file .Pdf e trattarlo alla stregua di un oggetto che contiene già i metodi per la corretta visualizzazione o il download?
Le classi da cui siamo partiti DataFile e MimeFile in realtà sono delle classi puramente strumentali al raggiungimento del vero obiettivo, cioè trattare ogni file come un tipo di dati specifico a seconda del suo MIME TYPE: un tipo .pdf, un tipo .doc, un tipo .jpg e così via.
Ereditando da MimeFile ricaveremo una classe specifica per ogni tipo che potremo utilizzare in questo modo
{PSEUDOCODICE}
$nomeFile=new TipoFile($percorsoFile) ;
//ora possiamo visualizzarlo
$nomeFile->show() ;
//oppure scaricarlo
$nomeFile->download() ;
//o ancora
$nomeFile->aggiungiDati($dati, $dove) ;
{/PSEUDOCODICE}
Esempi concreti: le classi PdfFile e DocFile
<?php
/*
Includiamo la definizione della classe base MimeFile
*/
include_once("MimeFileClass.php") ;
class PdfFile extends MimeFile{
function PdfFile($path){
/*
È necessario richiamare il costruttore della classe parent
*/
$this->MimeFile($path, 'application/Pdf') ;
$this->_mime=$mimeType ;
}
}//END class
$relazione=new PdfFile('relazione.Pdf') ;
$relazione->show() ;
?>
Altro MIME TYPE
<?php
class DocFile extends MimeFile{
function DocFile($path){
/*
È necessario richiamare il costruttore della classe parent
*/
$this->MimeFile($path, 'application/msword') ;
$this->_mime=$mimeType ;
}
}//END class
/***
Esempio di utilizzo
***/
$relazione=new DocFile('relazione.doc') ;
$relazione->show() ;
?>
Ora siamo in grado di identificare tutti i mime type di cui pensiamo di avere bisogno nel modo appena visto: potrebbe sembrare un lavoro lungo e che in fondo non ne valga la pena, ma come sempre quando si ha a che fare con gli oggetti lavorare di più all'inizio significa semplificarsi la vita in futuro.
Un'ulteriore evoluzione potrebbe essere quella di estendere le nostre classi con funzioni specifiche per il tipo di dato considerato, ad esempio sarebbe interessante integrare la classe JpegFile con le funzioni della libreria GD e la classe PdfFile con una delle tante librerie per operare sui file Pdf; tuttavia ci occuperemo di questo argomento nel prossimo articolo perchè è il momento di affrontare un'altra caratteristica fondamentale degli oggetti, il polimorfismo.
Il polimorfismo
Se gli oggetti fossero persone sarebbero dei tipi molto strani, dietro due facce identiche e indistinguibili possono nascondersi caratteri totalmente differenti tra loro. Per fortuna gli oggetti non sono persone e questa caratteristica ci torna utile:
<?php
//Pagina scarica_rel.php
$relazionePdf = new PdfFile('relazione.Pdf') ;
$relazioneDoc = new DocFile('relazione.doc') ;
$relazioneRtf = new RtfFile('relazione.rtf') ;
function objShow($oggetto){
$oggetto->show() ;
}
/***
L'utente sceglie dal link il file che vuole scaricare
***/
echo('<a href="scarica_rel.php?oggettoScelto=relazioneRtf"> Scarica versione RTF</a>') ;
echo('<a href="scarica_rel.php?oggettoScelto=relazionePdf"> Scarica versione PDF</a>') ;
echo('<a href="scarica_rel.php?oggettoScelto=relazioneDoc"> Scarica versione DOC</a>') ;
/***
IL file viene scaricato
***/
if(isset($_GET['oggettoScelto'])){
objShow($_GET['oggettoScelto']) ;
}
?>
Cosa c'è di tanto particolare?
La funzione objShow() non ha idea di quale oggetto riceverà e di che cosa visualizzerà, ma grazie ai metodi comuni a tutti i tipi di oggetto riuscirà perfettamente nel risultato.
Il polimorfismo è una delle conseguenze naturali dell'ereditarietà: gli oggetti passati come argomento di objShow() sono tutti istanze di classi con un genitore comune (MediaFile) e condividono la medesima interfaccia.
Conclusioni
Spero che la lettura attenta degli esempi riportati consenta di comprendere le potenzialità dell'OOP, ho preferito un approccio pratico perchè spesso la maggiore difficoltà per chi si avvicina alla programmazione object oriented sta proprio nell'applicare concretamente i principi su cui si basa.
Devo fare una precisazione doverosa sugli header utilizzati per l'individuazione dei MIME TYPE, sono quelli che *dovrebbero* funzionare quando i browser non presentano dei bug: non trattandosi del tema principale di questo articolo ho preferito semplificare e non soffermarmi sugli stratagemmi che a volte consentono di aggirare i problemi.
In questo articolo affronteremo brevemente due metodi per estendere le capacità di una classe (associazione e aggregazione), poi effettueremo un veloce excursus su altre peculiarità del Php object oriented tenendo presente che alcuni aspetti dovrebbero cambiare con la nuova release di PHP 5.
Associazione
Parlando di Tecniche per programmare a oggetti: ereditarietà avevamo precisato che tale possibilità andava usata in modo appropriato, e non con il solo scopo di risparmiare qualche riga di codice.
Nei casi in cui le caratteristiche di una classe non risultino spontaneamente ereditabili da un altra (in base principio della specializzazione) possiamo servirci delle caratteristiche di classi o oggetti totalmente estranei, delegando loro determinate funzioni (l'associazione è chiamata anche delegazione).
È sempre utile ricordare che le relazioni intercorrenti tra i vari oggetti vanno progettate sin dall'inizio, e che il codice OO ben fatto deve lasciare poco spazio all'improvvisazione.
Applicare la delegazione agli esempi già visti
Nel articolo avevamo lasciato in disparte la classe MimeFile, che era servita soltanto come base d'appoggio per creare definizioni specifiche per ogni MIME TYPE (.Pdf, .doc, .jpg etc.etc.), quesa scelta era dovuta al fatto che, per citare solo un caso, così avremmo potuto incorporare come metodi nella classe JpgFile le funzioni della libreria GD.
D'altra parte questa scelta risulta scomoda quando non abbiamo alcun interesse per un certo MIME TYPE, infatti per fornire la possibilità di scaricare un file mp3 o dobbiamo definire una classe apposita, o siamo costretti a specificare l'apposito header nella classe MimeFile, ad esempio
<?php
include_once("MimeFileClass.php") ;
$mp3File=new MimeFile("test.mp3", "audio/mpeg") ;
$mp3File->download() ;
/*
oppure $mp3File->show() ;
*/
?>
Non sarebbe utile far sì che MimeFile sia in grado di riconoscere automaticamente il MIME TYPE senza che lo si debba fornire ogni volta? Per ottenere questo risultato possiamo chiamare in aiuto di MimeFile un'altra classe, e delegare ad essa il compito del riconoscimento.
La classe MimeTeller
MimeTeller si chiama così perchè è in grado di fornirci il MIME TYPE di un file riconoscendolo dall'estensione, per ragioni di spazio non espongo il codice ma lo rendo disponibile per il download: è soltanto di un prototipo e potete ampliare autonomamente la lista dei tipi che è in grado di riconoscere, un elenco dei principali MIME TYPE è riperibile anche tra i file di configurazione di Apache.
Ecco come estendere MimeFile con le capacità di MimeTeller
<?php
/*
Includiamo la definizione della classe da cui MimeFile eredita
*/
include_once("DataFileClass.php") ;
/*
Includiamo la definizione della classe da associare
*/
include_once("MimeTeller.php") ;
class MimeFile extends DataFile{
/*
Due nuove proprietà (private) si aggiungono a quelle della classe parent
DataFile
*/
var $_mime ;
var $_disposition ;
/* COSTRUTTORE
Non precisiamo più il MIME TYPE come secondo argomento
*/
function MimeFile($path){
/*
È sempre necessario richiamare il costruttore della classe parent
*/
$this->DataFile($path) ;
/****************
ASSOCIAZIONE
****************/
/*
Ricaviamo il MIME TYPE grazie al metodo tellMime() della
classe associata
*/
$associata=new MimeTeller($path) ;
$this->_mime=$associata->tellMime() ;
}//END COSTRUTTORE
/*
Da qui in poi gli altri metodi: tutto resta come prima
.
.
.
.
.
*/
}//END class
?>
Instanziare un oggetto da MimeFile con il MIME TYPE corretto ora è automatico:
<?php
$mp3File=new MimeFile('test.mp3') ;
$mp3File->show() ;
/***
Mentre in precedenza sarebbe stato necessario
$mp3File=new MimeFile('test.mp3', 'audio/mpeg') ;
$mp3File->show() ;
***/
?>
Questa modifica si ripercuote anche nel modo in cui creiamo le classi per i MIME TYPE che desideriamo trattare individualmente.
Riprendendo Tecniche per programmare a oggetti: PdfFile ecco come possiamo modificare la definizione della classe
<?php
/*
Includiamo la definizione della classe base MimeFile
*/
include_once("MimeFileClass.php") ;
class PdfFile extends MimeFile{
function PdfFile($path){
/*
Non serve più specificare l'header nel costruttore della classe madre
*/
$this->MimeFile($path) ;
}
/*
Qui sotto possiamo ampliare la classe e aggiungere metodi che incorporano le funzioni della PDFlib
.
.
.
.
*/
}//END class
/*
Il modo in cui dichiariamo la classe specifica ovviamente non cambia
*/
$relazione=new PdfFile('relazione.Pdf') ;
$relazione->show() ;
?>
Tutte le classi MIME specifiche che vorremo creare erediteranno da una classe (MimeFile) che si serve di un'altra classe (MimeTeller). L'associazione (o delegazione) è semplicemente il servirsi, all'interno di una classe X, di un oggetto istanza di una classe esterna Y.
Aggregazione
Il manuale PHP dichiara che il linguaggio, a partire dalla versione 4.2, supporta un ulteriore metodo di estensione delle classi, detto "aggregazione".
In realtà questa caratteristica è disponibile solo con PHP v.4.3 e in modo sperimentale, ne parliamo soltanto perchè tra le altre cose costituisce un surrogato dell'ereditarietà multipla (capacità di ereditare contemporaneamente da due classi di pari livello).
L'aggregazione consente di incorporare in un oggetto proprietà e metodi appartenenti ad un'altra classe, sia in modo completo che selettivo.
Un esempio concreto aiuta più di mille spiegazioni
<?php
class Somma{
var $num1 ;
var $num2 ;
function Somma($num1, $num2){
$this->num1=$num1 ;
$this->num2=$num2 ;
}
function makeSum(){
echo($this->num1 + $this->num2."
n") ;
}
}//END class
class Sottrazione{
var $num1 ;
var $num2 ;
function Sottrazione($num1, $num2){
$this->num1=$num1 ;
$this->num2=$num2 ;
}
function makeSub(){
echo($this->num1 - $this->num2."
n") ;
}
}//END class
class Moltiplicazione{
var $num1 ;
var $num2 ;
function Moltiplicazione($num1, $num2){
$this->num1=$num1 ;
$this->num2=$num2 ;
}
function makeMul(){
echo($this->num1 * $this->num2."
n") ;
}
}//END class
$sumObj=new Somma(5,10) ;
/*
Risultato 15
*/
$sumObj->makeSum() ;
/**
Prima aggregazione
**/
aggregate($sumObj, 'Moltiplicazione') ;
/*
Risultato 50
*/
$sumObj->makeMul() ;
/**
Ulteriore aggregazione, si accumula
**/
aggregate($sumObj, 'Sottrazione') ;
/*
Risultato -5
*/
$sumObj->makeSub() ;
?>
L'esempio è banale ma eloquente: l'oggetto $sumObj, appena istanziato possiede soltanto la capacità di sommare i due argomenti forniti al costruttore, però in seguito alle due successive aggregazioni riesce a servirsi di metodi che non appartengono alla sua classe d'origine e ad effettuare anche sottrazione e moltiplicazione.
Non è possibile aggregare il costruttore di una classe nè i suoi membri privati, ed ecco perchè quest'ultimi si distinguono per il fatto che il loro nome inizia con un "_".
Nel caso appena visto servirsi dell'aggregazione è stato davvero semplice, in quanto le tre classi sono molto simili nella struttura, ma è necessario tenere presente che non ci si può servire a caso di questa caratteristica: infatti se il metodo che aggregriamo utilizza delle proprietà che non sono presenti o definite nell'oggetto a cui viene aggregato, otterremo una serie di Notice
Le classi da aggregare devono essere progettate per divenire complementari tra loro.
Il manuale online dedica ad associazione e aggregazione un intero capitolo.
Serializzare gli oggetti
Nell'articolo Serializzare e immagazzinare i dati avevo anticipato che la serializzazione degli oggetti è un argomento un po' particolare, che merita di essere trattato una volta presa maggiore confidenza con l'argomento OOP.
Un oggetto, ormai è chiaro, è una variabile un po' particolare in quanto può avere diversi stati e comportamenti.
Serializzare un oggetto che ha effettuato la connessione al database non è la stessa cosa di serializzarlo quando la connessione è stata chiusa: la sua "situazione interiore", il suo stato, è diverso nei due casi.
Per questo motivo potremmo voler determinare esattamente quale debba essere lo stato dell'oggetto nel momento del suo "congelamento", e quale debba ridiventare quando viene recuperato e de-serializzato.
Nelle recentissime versioni del linguaggio i programmatori PHP hanno a disposizione due metodi predefiniti che si attivano automaticamente nella fase di serializzazione e deserializzazione, e sono rispettivamente __sleep() e __wakeup().
Ecco una prova puramente dimostrativa del loro funzionamento, le operazioni contenute nei due metodi vengono eseguite quando si innesca l'evento in questione.
<?php
class FreezeTest{
var $qualcosa ;
function FreezeTest($something){
$this->qualcosa=$something ;
}
function __sleep(){
echo('Mi stai serializzando, arrivederci alla prossima!') ;
}
function __wakeup(){
echo('Salve! Mi hai appena de-serializzato!') ;
}
}//END class
$x=new FreezeTest("Questo è un valore qualsiasi") ;
serialize($x) ;
/*
Vedremo stampato a video
"Mi stai serializzando, arrivederci alla prossima!"
*/
?>
Vale la pena di ricordare anche in questa occasione che affinchè un oggetto venga de-serializzato correttamente deve essere già disponibile nello script la definizione della classe a cui appartiene, e questo vale anche quando la de-serializzazione è quella praticata automaticamente nel sistema delle sessioni.
Conclusioni, bibliografia e altre risorse
L'argomento è lungi dall'essere esaurito ma spero di aver fornito uno spunto utile a coloro che intendono intraprendere la via dell'OOP in PHP, alcuni aspetti sono stati volutamente tralasciati in quanto verranno superati con l'avvento di PHP 5 (atteso per l'estate 2003 inoltrata)... niente paura comunque, di seguito trovate una serie di link a risorse utili ad approfondire proprio le cose trascurate.
- Gli oggetti sono copie e non riferimenti: http://www.aditus.nu/jpgraph/jpg_phpoo.php
- Simulare le classi astratte: http://www.phpbuilder.org/columns/griffin20030207.php3
E per finire i capitoli dedicati a classi e oggetti nel manuale di www.php.net: