lunedì 23 dicembre 2013

La OOP in PHP spiegata in un esempio - Parte 15 - Side Effect da Clonazione e Funzione Speciale __clone() (deep clone)


Approfondiamo un momento la clonazione. Con tale operazione ci si aspetta di ottenere due oggetti identici nello stato ma disgiunti l'uno dall'altro. In alcuni casi potrebbe non essere proprio così.

Proviamo ad eseguire il seguente script:

<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>Forme Geometriche</title>
    </head>
    <body>
        <?php          
            require_once './cColore.php';
            require_once './cAreaDiDisegnoHTML.php';
            require_once './cCoordinateTrasforamabile.php';
            require_once './cFabbricaPoligoni.php';
            require_once './cRettangolo.php';
            require_once './cPoligonoRettangolo.php';                                                        
         
            $triangolo = new cPoligono(new cColore(255, 0, 0));
            $triangolo->setVertice($punto = new cCoordinateTrasforamabile(10,10));
            $punto->trasla(10, 0);
            $triangolo->setVertice($punto);
            $punto->trasla(0, 10);
            $triangolo->setVertice($punto);
         
            $triangolo2= clone $triangolo;          
            $triangolo2->trasla(3, 3);
            $triangolo2->setRGB(0, 255, 0);
                   
            $ad = new cAreaDiDisegnoHTML(50, 50);      
            $triangolo->disegna($ad);
            $triangolo2->disegna($ad);
       
            echo $ad->output();
        ?>
    </body>
</html>

Lo script crea un poligono di forma triagolare, quindi lo clona e chiede al clone di spostarsi di 3 punti in diagonale e di assumere un altro colore. Il risultato è quello mostrato nella seguente figura:
Output dello script precedente
Perchè c'è solo il triangolo verde? Il problema è l'operazione di clonazione. Dal post precedente sappiamo che la clonazione consiste nel generare un nuovo oggetto, della stessa classe di quello che si intende clonare, e nel copiare le corrispondenti variabili oggetto dell'originale nel clone. Il problema è nel "copiare". La classe cPoligono memorizza colore $oColore (trait tColorabile) e vertici ($vertici in cPoligono) al suo interno. Questi elementi però sono oggetti e sappiamo da quanto appreso in precedenza che copiare un oggetto, ossia assegnare un oggetto ad una variabile, equivale a trasferire il riferimento della locazione di memoria da cui reperire l'oggetto. Questo è quello che è avvenuto. Ciò significa che nonostante la clonazione i due oggetti continuano a condividere, tanto l'oggetto colore quanto i vari oggetti che costituiscono i vertici dei poligoni. Ne consegue che le operazioni di traslazione influenzano entrambi gli oggetti (che risultano sovrapposti), così come la modifica del colore. 

Un'immagine sulla situazione nell'aria di memoria può chiarire il concetto:
L'area di memoria dello script
Come si può osservare dall'immagine, i due oggetti $triangolo e $triangolo2 afferiscono a due aree di memoria distinte chiamate LocazioneA e LocazioneB. Anche le variabili oggetto $oColore e $vertici sono state copiate dall'originale al clone con il risultato però che $oColore e gli elementi di $vertici afferiscono ai medesimi oggetti di classe cColore e cCoordinateTrasformabili individuati da LocazioneC, LocaizoneD, LocazioneE e LocazioneF.

PHP offre un funzione speciale, chiamata __clone(). Tale funzione, se definita, è invocata sul'oggetto clonato immediatamente dopo le operazioni di clonazione prodotte dal costrutto clone. In altri termini la funzione clone esegue questi passaggi:
  1. Creare un nuovo oggetto della stessa classe dell'oggetto da clonare
  2. Copiare le variabili oggetto dall'oggetto da clonare al clone
  3. Invocare il metodo __clone() dell'oggetto risultante dalla clonazione
  4. Restituire l'oggetto risultante
All'interno della funzione __clone() possiamo quindi invocare la clonazione di quelle variabili oggetto interne alla classe che altrimenti sarebbero condivise fra l'oggetto da clonare e il suo clone.

Iniziamo con il modificare il trait tColorabile aggiungendogli la funzione __clone() come segue:

<?php
trait tColorabile{
    private $oColore;
 
    public function setColore(\iColore $oColore) {
        $this->oColore = clone $oColore;
    }

    public function getColore() {
        return $this->oColore;
    }  
 
    public function __clone(){      
        $this->setColore(clone $this->getColore());
    }

}
?>

Nel rispetto del principio di incapsulamento la funzione __clone() non accede agli elementi interni se non tramite i metodi d'accesso. Nella fattispecie non fa altro che impostare un nuovo oggetto clone di quello attualmente utilizzato.

Quindi modifichiamo l'oggetto cPoligono perché implementi la sua __clone(). Dato che stiamo definendo un metodo con lo stesso nome utilizzato nel trait (vedi l'approfondimento sui trait), dovremo rendere accessibile il metodo del trait con nome diverso all'interno della classe:

<?
...
class cPoligono extends aFiguraTrasformabile implements Iterator, iColore, iColorabile, iPoligono {

    use tColore,
        tColorabile{
            __clone as tColorabile__clone;
        }

...
 
    public function __clone() {      
        $this->tColorabile__clone();
        $temp=[];
        for($i=0, $n=$this->numVertici(); $i<$n; $i++)      
            $temp[]=clone $this->getVertice ($i);
        $this->delVertici();
        foreach ($temp as $vertice)
            $this->setVertice ($vertice);
    }

...

?>

Come si può vedere il metodo lascia al trait gestire i propri processi post clonazione, quindi si preoccupa delle proprie variabili membro generando un elenco di cloni dei vertici, cancellando i vertici già impostanti e inserendo i vertici clonati al proprio interno. Se una ipotetica classe genitrice avesse implementato il metodo __clone() si sarebbe dovuto invocare anche parent::__clone() per permettere alla genitrice di effettuare le proprie operazioni post clonazione.

Il risultato dello script di test è finalmente quello atteso.
Output dello script di test


La clonazione effettuata per mezzo della sola istruzione clone è definita shallow clone ossia una clonazione non profonda. D'altro canto, nella maggior parte dei casi, ciò di cui si necessita è quello che è stato qui implementato tramite il metodo __clone() definito deep clone o clonazione profonda.

Dato che la clonazione è una operazione di base pressoché imprescindibile per ogni classe, al fine di ottenere coerenza e uniformità nel codice è necessario che le classi che ammettono la clonazione siano tutte figlie di una classe standard astratta aClasse che presenti come unico metodo __clone() senza alcuna istruzione all'interno. In tal modo tutte le figlie ne saranno automaticamente dotate e ogni classe che implementa la funzione __clone() ai fini di una deep clone può serenamente invocare parent::__clone(). Esistono poi i pattern, ma qui non ne parleremo. L'unica cosa da tener presente è che se vogliamo che una classe non possa essere clonata, possiamo implementare il metodo __clone come segue:
public function __clone(){
     throw new Exception("Classe non clonabile!");
}

Ossia sollevando un'eccezione in caso di clonazione e lasciando al client la responsabilità della gestione di tale eccezione.