LSP – Liskov Substitution Principle

0
993

Il Principio

Siamo arrivati a metà di questo nostro piccolo viaggio: oggi esaminiamo la L di S.O.L.I.D.

Il Liskov Substitution Principle, o Principio di Sostituzione di Liskov, introduce il concetto di sostitutibilità, affermando che:

“In a computer program, if S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e., objects of type S may substitute objects of type T) without altering any of the desirable properties of that program (correctness, task performed, etc.).”

Nella nostra lingua può essere tradotto così:

“In un software, se S è un sottotipo di T, allora gli oggetti di tipo T devono poter essere sostituiti senza problemi con oggetti di tipo S senza alterare in nessun modo nessun aspetto e proprietà del software (correttezza, esecuzione dei task e così via)”.

Tale principio prende il nome da Barbara Liskov, che per prima l’ha introdotto durante una conferenza del 1987.

Prima di passare alla spiegazione, sappi che esiste anche un’enunciazione più formale del principio:

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

La Spiegazione

Il Liskov Substitution Principle, da ora LSP, ha la sua rilevanza nella programmazione ad oggetti in quanto fondato sul concetto di ereditarietà. Non è difficile da capire, ma in un primo momento può essere comunque un po’ complesso rendere propri alcuni concetti.

Spiegato in parole povere, il principio spiega che ovunque usiamo una classe base T, dovremmo essere altrettanto in grado di usare una classe S derivata da T.

Facciamo un esempio al volo:

method1();
}

}

La classe Utilizer appena vista ha un metodo use che prevede, come parametro in input, un’istanza della classe BaseClass. Se abbiamo scritto il nostro codice correttamente, e quindi rispettato il principio, dovremmo poter sostituire senza problemi un’istanza di BaseClass con una di SubClass, senza errori in fase di esecuzione.

Cosa ci vuole? Anche SubClass ha lo stesso metodo method1, visto che deriva da BaseClass!

Giusto, ma non sempre è così. Ad esempio, guarda questa alternativa di SubClass.

Nota: l’esempio è comunque in PHP astratto, ma implementare una cosa del genere in Laravel dovrebbe essere ancora più semplice, viste le sue feature.

Immagineremo di lavorare con un’API che serve informazioni su libri. Partiamo quindi dal nosto BookController.

getAllBooks());
}

// altri metodi store, show, destroy e così via…

}

Il metodo list prende in input un oggetto $bookRepository, istanza di BookRepository. Nel “dietro le quinte” abbiamo due classi: BookRepository, astratta, e DatabaseRepository, che interroga il database per ottenere i risultati. La seconda è derivata della prima.

list($bookRepo);

// output di $results…

Tutto procede bene, fin quando il progetto non evolve e si manifestano nuove necessità.

Gli analisti hanno valutato le richieste al server, il cui volume è aumentato enormemente nell’ultimo periodo, e hanno scoperto che sarebbe un’ottima idea usare un sistema di cache, tenendo in memoria per un minuto i risultati della ricerca. Considerato il numero di richieste sarebbe un’ottima idea ed un enorme risparmio di risorse.

Ora, le possibilità di implementazione potrebbero essere tantissime. Alla fine scegliamo quella più “elegante”: creare una classe CacheBookRepository che vada a “decorare” la già esistente DbBookRepository.

Eccola qui:

// file CacheBookRepository.php

class CacheBookRepository extends DbBookRepository {

private $cacheSystem;

public function __construct()
{
// inizializzo in $cacheSystem la cache;
}

public function getAllBooks()
{
$result = $this->cacheSystem->remember(‘books_list’, 1, function(){

return parent::getAllBooks();

});

return $result;
}

}

Cosa è successo?

  • la classe CacheBookRepository estende DbBookRepository. Dobbiamo usarla al suo posto, quindi dobbiamo renderla “consistente” per fare in modo di farle rispettare il LSP;
  • il costruttore si occupa di inizializzare il sistema di cache. Si tratta di un esempio quindi al momento non scendiamo nei particolari;

Il metodo getAllBooks sovrascrive quello del repository da cui deriva. Vediamo nel dettaglio cosa fa:

  • il metodo remember del cacheSystem cerca di recuperare la lista dei libri contrassegnata come books_list;
  • se presente in cache, viene ritornata ed assegnata a $result;
  • in caso contrario, il valore viene preso dal metodo parent::getAllBooks, che richiama il database repository “sottostante”.
  • la nuova lista di libri ottenuta, in caso, viene memorizzata come books_list per circa un minuto come richiesto;

Nota: ho usato il metodo remember prendendo spunto da quello già presente in Laravel. Se non ce l’hai presente o non lo ricordi, la documentazione è qui per questo.

A questo punto torniamo indietro al nostro codice di bootstrap, che si presentava così:

list($bookRepo);

// output di $results…

Per rendere attiva la nuova modifica basterà, a questo punto, sostituire

// creo l’oggetto $bookRepo
$bookRepo = new DbBookRepository;

con

// creo l’oggetto $bookRepo
$bookRepo = new CacheBookRepository;

Figo! In effetti funziona! E non ho dovuto modificare altro! Come mai?

Beh, questo è possibile perché ho scritto il codice in modo tale da rispettare tassativamente il **Liskov Substitution Principle. Non ho sovrascritto i metodi “sporcandoli” con operazioni diverse o cambiando il comportamento di base, ma cercando invece di rispettare il formato di input ed output dei dati.

Chiaramente ho usato anche dei pattern, come ad esempio il Decorator, ma se guardi bene il codice, le modifiche effettuate e il flusso che segue l’applicazione scoprirai che di fondo il LSP fa la sua (importante) parte.

Spero di essere stato chiaro nell’esposizione! Come al solito, per qualsiasi perplessità usa i commenti.

Andiamo avanti, e parliamo dell’Interface Segregation Principle!.