Vai al contenuto

S.O.L.I.D. ovvero i 5 principi della programmazione ad oggetti

Il termine SOLID  viene utilizzato per indicare i cinque principi di progettazione orientata agli oggetti (OOD) di Robert C. Martin, conosciuto al mondo come zio Bob. I principi SOLID sono intesi come linee guida per lo sviluppo di software estendibile e manutenibile, in particolare nel contesto di pratiche di sviluppo agili e fondate sull’identificazione di code smell e sul refactoring. La parola SOLID è un acronimo che serve a ricordare tali principi (Single responsibility, Open-closed, Liskov substitution, Interface segregation, Dependency inversion), e fu coniata da Michael Feathers.

  • S – Single-responsiblity principle
  • O – Open-closed principle
  • L – Liskov substitution principle
  • I – Interface segregation principle
  • D – Dependency Inversion Principle

Single-responsibility Principle

S.R.P in breve, questo principio dice:

A class should have one and only one reason to change, meaning that a class should have only one job.

Il principio afferma che ogni classe dovrebbe avere una ed una sola responsabilità, interamente incapsulata al suo interno. Facciamo un esempio concreto: diciamo di avere alcune classi che rappresentano forme geometriche e che vogliamo avere la somma delle aree di tutte le forme istanziate. Fin qui sembra abbastanza facile. Per prima cosa bisogna implementare tutte le forme, in questo modo:

class Cerchio {
    public $raggio;
    public function __construct($raggio) {
        $this->raggio= $raggio;
    }
}

class Quadrato {
    public $lato;
    public function __construct($lato) {
        $this->lato= $lato;
    }
}

Successivamente avremo bisogno di un’altra classe che implementi il metodo per la somma e per visualizzare l’output della somma a schermo. Chiamiamo questa classe AreaCalculator:

class AreaCalculator {

    protected $forme;

    public function __construct($forme = array()) {
        $this->forme = $forme;
    }

    public function somma() {
        // operazioni logiche per fare la somma delle aree delle forme
    }

    public function output() {
        return implode('', array(
            "<h1>",
                "La somma delle aree delle forme è: ",
                $this->somma(),
            "</h1>"
        ));
    }
}

A questo punto basterà istanziare degli oggetti di tipo forma circle o square e passarle tramite array a un oggetto che si occupi di sommare e stampare il risultato:

$forme = array(
    new Cerchio(2),
    new Quadrato(5),
    new Quadrato(6)
);

$areas = new AreaCalculator($forme);

echo $areas->output();

Il problema è che l’oggetto AreaCalculator che abbiamo istanziato gestisce le operazioni di output dei dati. Cosa succederebbe se l’utente volesse visualizzare i dati in formato JSON o XML o qualcos’altro? Bisognere implementare tutta questa logica all’interno di AreaCalculator, ma verrebbe meno il principio di singola responsabilità: infatti AreaCalculator dovrebbe occuparsi solo di sommare le aree delle forme e non preoccuparsi di come l’utente vuole visualizzare i dati, siano essi JSON, XML, HTML.

Per risolvere il problema si crea una nuova classe che chiameremo SumCalculatorOutputter che implementa la logica per gestire le modalità di visualizzazione dei dati calcolati di tutte le forme. A questo punto aggiungendo qualche riga a quanto definito sopra, si avrebbe:

$forme = array(
    new Cerchio(2),
    new Quadrato(5),
    new Quadrato(6)
);

$areas = new AreaCalculator($forme);
$output = new SumCalculatorOutputter($areas);

echo $output->JSON();
echo $output->HAML();
echo $output->HTML();
echo $output->JADE();

Open-closed principle

O.C.P. in breve, questo principio dice:

Objects or entities should be open for extension, but closed for modification.

Il principio afferma che un oggetto o un’entità (software) dovrebbe essere aperta alle estensioni, ma chiusa alle modifiche. In riferimento alla classe già definita sopra, ecco un esempio del metodo somma() per chiarire le idee:

public function somma() {
    foreach($this->forme as $forma) {
        if(is_a($forma, 'Quadrato')) {
            $area[] = pow($forma->lato, 2);
        } else if(is_a($forma, 'Cerchio')) {
            $area[] = pi() * pow($forma->raggio, 2);
        }
    }

    return array_sum($area);
}

Se avessimo voluto calcolare l’area di altri tipi di forme (rettangolo, trapezio, rombo, etc…) avremmo dovuto aggiungere più costrutti if / else. Ciò va evidentemente contro la principio di OCP.

Una soluzione al problema è quella di rimuovere la logica di calcolo dell’area dal metodo somma() e implementare un metodo area() direttamente in ogni classe forma. Per la classe Quadrato, quindi avremo:

class Quadrato { 
    public $lato; 

    public function __construct($lato) { 
        $this->lato = $lato; 
    } 

    public function area() { 
        return pow($this->lato, 2) ; 
    } 
}

La stessa cosa avviene per la classe Cerchio e per tutte le classi forme che verranno implementate successivamente. A questo punto per calcolare la somma delle aree, basterà che il metodo somma() contenga le poche righe di codice qui sotto. In questo modo, senza modificare righe di codice, possiamo calcolare l’area di nuove forme:

public function somma() {
    foreach($this->forme as $forma) {
        $area[] = $forma->area();
    }

    return array_sum($area);
}

Infine, per essere sicuri che la classe contenga un metodo area(), possiamo creare un’interfaccia che implementi il metodo all’interno di ogni singola classe. In questo modo:

interface InterfacciaForme {
    public function area();
}

class Circle implements InterfacciaForme {
    public $raggio;

    public function __construct($raggio) {
        $this->raggio= $raggio;
    }

    public function area() {
        return pi() * pow($this->raggio, 2);
    }
}

Quindi il metodo somma() si trasforma definitivamente, aggiungendo un if che controlla se l’oggetto forma è di tipo InterfacciaForme, quindi che abbia al suo interno implementato il metodo area(), altrimenti genera un eccezione.

public function somma() {
    foreach($this->forme as $forma) {
        if(is_a($forma, 'InterfacciaForme')) {
            $area[] = $forma->area();
            continue;
        }

        throw new AreaCalculatorInvalidShapeException;
    }

    return array_sum($area);
}

Liskov substitution principle

L.S.P. in breve, afferma che:

Let q(x) be a property provable about objects of x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T.

Se q(x) è una proprietà che si può dimostrare essere valida per oggetti x di tipo T, allora q(y) deve essere valida per oggetti di tipo S dove S è un sottotipo di T. Più semplicemente B è un sottotipo di A se e solo se, per ogni programma che usi oggetti di classe A, posso utilizzare al loro posto oggetti di classe B e lasciare immutato il comportamento “logico” del programma.

Se vogliamo, ancora più semplicemente: gli oggetti dovrebbero poter essere sostituiti con dei loro sottotipi, senza alterare il comportamento del programma che li utilizza.

Facciamo un esempio con la classe AreaCalculator. Diciamo di voler implementare una classe chiamata VolumeCalculator che estende la classe AreaCalculator.

class VolumeCalculator extends AreaCalulator {
    public function __construct($forme = array()) {
        parent::__construct($forme);
    }

    public function somma() {
        // logic per calcolare i volumi e ritornare un array di output
        return array($summedData);
    }
}

Nel SumCalculatorOutputter avremo:

class SumCalculatorOutputter {
    protected $calculator;

    public function __constructor(AreaCalculator $calculator) {
        $this->calculator = $calculator;
    }

    public function JSON() {
        $data = array(
            'somma' => $this->calculator->somma();
        );

        return json_encode($data);
    }

    public function HTML() {
        return implode('', array(
            '<h1>',
                'Somma delle aree delle forme date: ',
                $this->calculator->somma(),
            '</h1>'
        ));
    }
}

Tuttavia, quando si istanziano gli oggetti di tipo Quadrato e Cerchio e si cerca di invocare il metodo HTML (il quale richiama il metodo somma()), verrà prodotto un E_NOTICE, che ci informa della conversione di array di stringhe. Per risolvere il problema, bisogna trasformare il metodo somma() nella classe VolumeCalculator, in modo da ritornare non un array di valori, ma un singolo valore float, double o int.

Interface segregation principle

I.S.P. in breve, dice che:

A client should never be forced to implement an interface that it doesn’t use or clients shouldn’t be forced to depend on methods they do not use.

Una classe client non dovrebbe dipendere da metodi che non usa, e che pertanto è preferibile che le interfacce siano molte, specifiche e piccole (composte da pochi metodi) piuttosto che poche, generali e grandi.

Riprendendo l’esempio precedente, se volessimo aggiungere un nuovo metodo per calcolare il volume, possiamo definire l’intestazione nell’interfaccia InterfacciaForme. Se volessimo seguire il principio di segregazione delle interfacce allora conviene definire due interfacce distinte (InterfacciaForme e SolidInterfacciaForme), in questo modo:

interface InterfacciaForme {
    public function area();
}

interface SolidInterfacciaForme {
    public function volume();
}

// la classe cubo implementa le due interfacce
class Cubo implements InterfacciaForme, SolidInterfacciaForme {
    public function area() {
        // calcola l'area
    }

    public function volume() {
        // calcola il volume
    }
}

Questo approccio funziona ed è valido ma se vogliamo evitare qualsiasi tipo di trappole possiamo creare una nuova interfaccia chiamata GestioneInterfacciaForme e implementarla nelle classi di tipo forme piatte e solide. In questo modo basterà un singolo metodo calcola() per gestire i calcoli di qualsiasi tipo di forma, sia esso volume o area.

interface GestioneInterfacciaForme {
    public function calcola();
}

class Quadrato implements InterfacciaForme, GestioneInterfacciaForme {
    public function area() { /* operazioni di calcolo */ }

    public function calcola() {
        return $this->area();
    }
}

class Cubo implements InterfacciaForme, SolidInterfacciaForme, GestioneInterfacciaForme {
    public function area() { /* operazioni di calcolo */ }
    public function volume() { /* operazioni di calcolo */ }

    public function calcola() {
        return $this->area() + $this->volume();
    }
}

Ora, in AreaCalculator, si può facilmente sostituire la chiamata al metodo area() con il metodo calcola() e verificare se l’oggetto è un’istanza di GestioneInterfacciaForme e non InterfacciaForme.

Dependency Inversion Principle

D.I.P. in breve, afferma che:

Entities must depend on abstractions not on concretions. It states that the high level module must not depend on the low level module, but they should depend on abstractions.

Ovvero, i moduli di alto livello non devono dipendere da quelli di basso livello. Entrambi devono dipendere da astrazioni; Le astrazioni non devono dipendere dai dettagli; sono i dettagli che dipendono dalle astrazioni. Detta così, potrebbe sembrare un tantino confusionario. Il principio di inversione delle dipendenze aiuta a disaccoppiare il codice in modo tale che le classi dipendano da astrazioni piuttosto che da implementazioni concrete. Ma vediamo di capire meglio con un esempio:

class PasswordReminder {
    private $dbConnection;

    public function __construct(MySQLConnection $dbConnection) {
        $this->dbConnection = $dbConnection;
    }
}

La classe PasswordReminder è di alto livello, mentre la classe MySQLConnection è di basso livello. Questo costrutto viola il principio DIP poichè la classe PasswordReminder è costretta a dipendere dalla classe MySQLConnection. In seguito se si dovesse cambiare il motore di database, è necessario modificare il PasswordReminder e ciò comporta anche una violazione al principio OCP. Il PasswordReminder non dovrebbe preoccuparsi di quale database viene utilizzato nell’applicazione: per risolvere questo problema, possiamo creare l’interfaccia:

interface DBConnectionInterface {
    public function connect();
}

L’interfaccia dispone di un metodo di connessione chiamato connect() e la MySqlConnection implementa questa interfaccia. In questo modo, anzichè utilizzare MySqlConnection all’interno del costruttore di PasswordReminder, possiamo utilizzare l’interfaccia e cosi facendo non ci importa più il tipo di database che viene utilizzato. PasswordReminder può facilmente connettersi al database senza problemi e il principio OCP non è violato.

class MySQLConnection implements DBConnectionInterface {
    public function connect() {
        return "Database connection";
    }
}

class PasswordReminder {
    private $dbConnection;

    public function __construct(DBConnectionInterface $dbConnection) {
        $this->dbConnection = $dbConnection;
    }
}

 

I principi SOLID sono strumenti da prendere in considerazione sia quando si scrive codice o quando si effettua il refactoring di sistemi legacy.

La sensazione dei programmatori meno esperti che si imbattono in codice e framework progettati con i principi SOLID sia quello di avere a che fare con sistemi “over-engineered”. In realtà l’utilizzo di questi principi, meglio se utilizzati in combinazione tra di essi, aiuta gli sviluppatori ad estendere, modificare e testare il codice in modo più efficace.