venerdì 13 dicembre 2013

La OOP in PHP spiegata in un esempio - Parte 11 - Osservazioni su Interfacce e Classi Astratte o quasi


Nel caso trattato nella parte 10, aggiungendo dei metodi all'interfaccia iFigura abbiamo imposto che una figura debba poter essere rappresentata in un'area di disegno (metodo disegna(\iAreaDiDisegno $oAreaDiDisegno)) e debba poter essere trasformata tramite i metodi trasla, ruota e scala. In tale circostanza abbiamo, delegando tutto alla interfaccia, ossia abbiamo vincolato una figura (classe che implementa iFigura) a dover saper gestire delle trasformazioni. In più le stesse dichiarazioni di metodi di trasformazioni sono presenti uguali nell'interfaccia iCoordinate nono, nonostante la programmazione a oggetti ci dica di raggruppare comportamenti simili tramite ereditarietà.

L'alternativa è nel definire due interfacce, una iFigura ed una iTrasformabile, in cui iFigura ha il solo metodo disegna() e iTrasformabile dichiara i metodi trasla(), scala() e ruota(). Infine implementare tali interfacce nelle classi concrete. Ma anche in questo caso avremmo una disgiunzione tra i metodi di una figura e i metodi di trasformazione. In altre parole se un metodo dovesse richiedere tra i propri argomenti una oggetto che implementi l'interfaccia iTrasformabile, non è garantito che l'oggetto implementi anche il metodo disegna() che appartiene ad un'altra interfaccia.

In una simile circostanza è possibile definire delle classi astratte aFigura e aFiguraTrasformabile, che  estende la prima, in modo che eventuali classi concrete che estendono aFiguraTrasformabile possiedano tutti i metodi di entrambe le interfacce, mentre le classi concrete estese da aFigura sono prive dei metodi di trasformazione restando comunque delle figure rappresentabili tramite il metodo disegna().

Ciò permette di poter associare ai parametri dei metodi il tipo più appropriato e poter passare dove richiesto un oggetto di tipo aFigura, tanto una classe concreta che estenda aFigura quanto una classe concreta che estenda aFiguraTrasformabile utilizzata come una aFigura. Allo stesso modo potremo agire su cCoordinate facendogli implementare la sola interfaccia iCoordinate ed estendere la classe con una cCoordinateTrasformabili che implementa l'interfaccia iTrasformabile.

In generale possiamo quindi dire che al fine di perseguire una forma di ereditarietà multipla, pur beneficiando della dichiarazione del tipo per gli argomenti del metodo possiamo:
  1. Definire le varie interfacce
  2. Creare delle classi astratte che richiamano, implementano o meno una o più interfacce, e specializzano la classe astratta per ereditarietà con altre classi astratte ampliando la platea dei metodi
  3. Definire delle classi concrete con il livello di affinamento desiderato come estensione delle classi astratte
  4. Utilizzare le classi astratte (che accorpano più interfacce) o le specifiche interfacce come tipo per gli argomenti dei metodi
Si fa riferimento a classi astratte intese quali gusci vuoti che dichiarano cosa aspettarsi dalla classe concreta che le implementa, ma nulla vieta di utilizzare classi astratte con metodi già implementati o classi concrete. Ciò può essere più agevole in presenza di classi banali come cCoordinate visto poc'anzi. In generale è comunque bene distinguere da ciò che vuole essere una'interfaccia (dichiarazione di un tipo di dato), da ciò che è l'implementazione concreta.

Ritornando al nostro esempio, dovremo quindi rivedere la classe cPoligono perchè sia estensione di cFiguraTrasformabile (considerato l'uso che ne facciamo nei nostri esempi). Occorre ripensare anche il metodo setVertice(), che accetta come argomento un semplice iCoordinate perché al suo interno vada poi a memorizzare un cCoordinateTasformabili. Dal punto di vista degli utilizzatori non è cambiato nulla ed un poligono continua ad essere una collezione di oggetti iCoordinate. Per cPoligono è cambiato tanto perché, adesso è a tutti gli effetti una aFiguraTrasformabile ed al suo interno fa uso di oggetti cCoordinateTrasformabili che gli permettono di compiere le operazioni di trasformazione necessarie.

Vediamo ora il codice. Iniziamo con le interfacce ossia definendo iTrasformabile e rimuovendo i metodi di trasformazione da iCoordinate e iFigura:

<?php
interface iTrasformabile {
    public function trasla($dx, $dy);  
    public function scala(\iCoordinate $oPuntoDiFuga, $scalaX, $scalaY);  
    public function ruota(\iCoordinate $oCentroRotazione, $angolo);
}
?>


<?php
interface iFigura{
    public function disegna(\iAreaDiDisegno $oAreaDiDisegno);  
}
?>


Rivediamo la definizione di cCoordinate dividendola con una classe cCoordinateTrasformabili che la estenda:


<?php
require_once './iCoordinate.php';

class cCoordinate implements iCoordinate{

    private $x;
    private $y;

    public function __construct($x, $y) {
        $this->setXYArray([$x, $y]);
    }

    //implementazione iCoordinate
    public function setX($x) {
        $this->x = $x;
    }

    public function setY($y) {
        $this->y = $y;
    }

    public function getX() {
        return $this->x;
    }

    public function getY() {
        return $this->y;
    }

    public function setXY($x, $y) {
        $this->setX($x);
        $this->setY($y);
    }

    public function setXYArray($XY) {
        if (is_array($XY) and count($XY) == 2)
            $this->setXY($XY[0], $XY[1]);
        else
            throw new Exception("La funzione setXYArray si aspetta di ricevere come argomento un array contenente le due coordinate");
    }
 
    public function getXYArray(){
        return [$this->getX(),  $this->getY()];
    }
}

?>


<?php
require_once './cCoordinate.php';
require_once './iTrasformabile.php';
class cCoordinateTrasforamabile extends cCoordinate implements iTrasformabile{
    //Impelmentazione iTrasformabile
    public function trasla($dx, $dy) {
        $dx = (int) $dx;
        $dy = (int) $dy;
        $this->setX($this->getX()+$dx);
        $this->setY($this->getY()+$dy);
    }
 
    public function scala(\iCoordinate $oPuntoDiFuga, $scalaX, $scalaY) {      
        $x = ($this->getX()-$oPuntoDiFuga->getX())*$scalaX;
        $y = ($this->getY()-$oPuntoDiFuga->getY())*$scalaY;
        $this->setX($x+$oPuntoDiFuga->getX());
        $this->setY($y+$oPuntoDiFuga->getY());      
    }
 
    public function ruota(\iCoordinate $oCentroRotazione, $angolo) {
        $this->trasla(-$oCentroRotazione->getX(), -$oCentroRotazione->getY());
        $x = $this->getX() * cos(deg2rad($angolo)) - $this->getY() * sin(deg2rad($angolo));
        $y = $this->getY() * cos(deg2rad($angolo)) + $this->getX() * sin(deg2rad($angolo));
        $this->setX($x);
        $this->setY($y);
        $this->trasla($oCentroRotazione->getX(), $oCentroRotazione->getY());
    }
 
    public static function cCoordinate2cCoordinateTrasformabile(\iCoordinate $oCoordinate){      
        return new cCoordinateTrasforamabile($oCoordinate->getX(), $oCoordinate->getY());
    }
}

?>

In cCoordinateTrasfomabile è astato aggiunto un metodo statico che mi permette di produrre un oggetto cCoordinateTrasformabili da un oggetto iCoordinate.

Definiamo le classi astratte aFigura e aFiguraTrasformabile le quali non sono altro che un modo di riscrivere le interfacce iFigura e iTrasformabile legandole in un nuovo tipo/interfaccia che accorpi i metodi di entrambe:

<?php
require_once './iFigura.php';
abstract class aFigura implements iFigura{
    abstract public function disegna(\iAreaDiDisegno $oAreaDiDisegno);
}
?>


<?php
require_once './aFigura.php';
require_once './iTrasformabile.php';
abstract class aFiguraTrasformabile extends aFigura implements iTrasformabile{
    abstract public function ruota(\iCoordinate $oCentroRotazione, $angolo);
    abstract public function scala(\iCoordinate $oPuntoDiFuga, $scalaX, $scalaY);
    abstract public function trasla($dx, $dy);
}

?>

Ora modifichiamo cPoligono perchè sia estensione del nuovo tipo aFiguraTrasformabile e non semplice implementazione di singole interfacce iFigura e iTrasformabile. In tutto questo sorge un solo problema. I vertici del poligono sono degli oggetti \iCoordinate impostati tramite l'unico metodo di accesso setVertice(\iCoodrinate $oVertice). Ma per le trasformazioni abbiamo bisogno che siano degli oggetti cCoordinateTrasformabili. A tal fine basta modificare il suddetto metodo ottenendo:

<?php

require_once './aFiguraTrasformabile.php';
require_once './cCoordinateTrasforamabile.php';
require_once './iColore.php';
require_once './iColorabile.php';
require_once './iPoligono.php';
require_once './tColore.php';
require_once './tColorabile.php';

class cPoligono extends aFiguraTrasformabile implements Iterator, iColore, iColorabile, iPoligono {

    use tColore,
        tColorabile;

    private $vertici = [];

    public function __construct(\iColore $oColore) {
        $this->setColore($oColore);
    }
     
    public function delVertici() {
        $this->vertici = [];
    }

    public function setVertice(\iCoordinate $oCoordinate) {      
        $this->vertici[] = cCoordinateTrasforamabile::cCoordinate2cCoordinateTrasformabile($oCoordinate);
    }

    public function getVertice($numero) {
        if ($numero < $this->numVertici() and $numero >= 0)
            return $this->vertici[$numero];
        else
            throw new Exception("Si cerca di accedere ad un vertice non disponibile");
    }    
 
    public function numVertici() {
        return count($this->vertici);
    }     

    //Interfaccia iFigura
    public function disegna(\iAreaDiDisegno $oAreaDiDisegno) {
        $ultimo = $primo = null;
        foreach ($this as $punto)
            if (!is_null($ultimo)) {
                $oAreaDiDisegno->tracciaSegmento($ultimo, $punto, $this->getColore());
                $ultimo = $punto;
            }
            else
                $primo = $ultimo = $punto;
        $oAreaDiDisegno->tracciaSegmento($primo, $ultimo, $this->getColore());
    }
 
    public function trasla($dx,$dy){
        $dx = (int) $dx;
        $dy = (int) $dy;    
        foreach ($this as $vertice)
            $vertice->trasla($dx, $dy);              
    }
 
    public function scala(\iCoordinate $oPuntoDiFuga, $scalaX, $scalaY) {
        foreach ($this as $vertice)
            $vertice->scala ($oPuntoDiFuga, $scalaX, $scalaY);
    }
 
    public function ruota(\iCoordinate $oCentroRotazione, $angolo) {
        foreach ($this as $vertice)
            $vertice->ruota($oCentroRotazione, $angolo);
    }
 
    //Interfaccia Iterator
    public function current() {
        return current($this->vertici);
    }

    public function key() {
        return key($this->vertici);
    }

    public function next() {
        return next($this->vertici);
    }

    public function rewind() {
        return reset($this->vertici);
    }

    public function valid() {
        return current($this->vertici) !== false;
    }
}

?>

in altri termini ciò che cambia a parte la extends e la rimozione della interfaccia iFigura perchè già dichiarata in aFigura, è il metodo setVertice che si preoccupa di convertire la iCoordinate in un oggetto cCoordinateTrasformabile prima di memorizzarlo fra i vertici. Abbiamo così ottenuto l'ampliamento delle capacità della classe pur preservando la compatibilità con il codice già scritto.

Il risultato finale del nostro script di esempio è assolutamente immutato, ma il modo in cui lo fa è mutato nella forma.