LSP - Liskov Substitution Principle

In questo terzo capitolo della mini-serie dedicata ai principi S.O.L.I.D. parliamo della L! Il Liskov Substitution Principle.
francesco
Francesco Malatesta
06/09/2015 in

Hai mai sentito parlare dei Principi S.O.L.I.D. nella programmazione PHP?

Scopriamoli insieme, in questa mini-serie dedicata all'argomento!

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:

<?php

	class BaseClass {

		public function method1()
		{
			return true;
		}

		public function method2()
		{
			return 2;
		}

	}

	class SubClass extends BaseClass {

		public function specializedMethod()
		{
			return 'whatever';
		}

	}

Supponiamo ora di avere un'altra classe utilizzatrice Utilizer.

<?php

	class Utilizer {

		public function use(BaseClass $baseClass)
		{
			$baseClass->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.

<?php

	class AlternativeSubClass extends BaseClass {

		public function specializedMethod()
		{
			return 'whatever';
		}

		public function method1()
		{
			return 'blablabla';
		}

	}

Il metodo method1 è stato sovrascritto nella classe AlternativeSubClass. Ritorna un valore diverso (e di tipo diverso) e, più in generale, si può dire abbia un comportamento differente.

Abbiamo infranto il principio: usando un'istanza di AlternativeSubClass al posto di BaseClass il comportamento dell'applicazione cambia radicalmente. Nessuna traccia del livello di coerenza che il LSP richiede.

Insomma: rimanere fedele e seguire il LSP, a volte, può essere difficile e come ho già detto prima può essere altrettanto astruso, inizialmente, fare proprio il concetto.

Ad ogni modo, tenere una certa coerenza nella definizione di classi base e derivate aiuta tanto e paga, soprattutto quando arriviamo a sviluppare applicazioni e sistemi molto più complessi.

L'Esempio

Cerchiamo di supporre, per questo esempio, una situazione vicina a quelle che sono le esigenze "reali" di uno sviluppatore. Supponiamo di avere un'applicazione che si occupa di servire delle API. Come in molte realtà di questo genere, supponiamo anche di avere un controller con alcuni metodi che, a loro volta, fanno uso di repository per interrogare il database sottostante.

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.

<?php

	class BookController {

		public function list(BookRepository $bookRepository)
		{
			return json_encode($bookRepository->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.

<?php

	// file BookRepository.php

	abstract class BookRepository {

		// definisco un metodo da implementare nelle derivate...
		public function getAllBooks();

	}

	// file DbBookRepository.php

	class DbBookRepository extends BookRepository {

		public function getAllBooks()
		{
			$result = [];

			// effettuo la query al database i cui risultati verranno inseriti
			// nell'array $result

			return $result;
		}

	}

Arrivati a questo punto è facile immaginare il funzionamento di tutto il meccanismo. Il controller verrà richiamato con qualcosa di simile a:

<?php

// preparazione...

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

// uso il $bookRepo come parametro per il metodo list() del controller
$results = $controller->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ì:

<?php

// preparazione...

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

// uso il $bookRepo come parametro per il metodo list() del controller
$results = $controller->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!.