Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

L - Liskov Substitution Principle

Utilizzare una classe figlia al posto di una classe padre non cambia il comportamento del programma
Utilizzare una classe figlia al posto di una classe padre non cambia il comportamento del programma
Link copiato negli appunti

Supponiamo di aver appena definito la classe Documento e di aver successivamente generato le classi Lettera, SMS e Romanzo come classi figlie della classe Documento.

Il principio che trattiamo in questa lezione, enunciato per la prima volta da Barbara Liskov nel 1987, asserisce che ovunque nel nostro codice sia richiesto un oggetto di tipo Documento debba essere possibile utilizzare un qualsiasi oggetto istanziato da una delle classi figlie di Documento, come ad esempio SMS, senza pregiudicare in alcun modo il buon funzionamento del programma.

La definizione formale del principio Liskov Substitution è espressa in questo modo:

“ Se per ogni oggetto o1 di tipo S c'è un oggetto o2 di tipo T, tale che per tutti i programmi P definiti in termini di T, il comportamento di P non vari sostituendo o1 a o2, allora S è un sottotipo di T

Come è facile violare l'LSP

Definiamo i metodi della classe Documento:

classe Documento
   string getTitolo()
   void   setTitolo(string titolo)
   string getTesto()
   void   setTesto(string testo)

Ora descriviamo un metodo di una classe Storage che, ricevuto un oggetto di tipo Documento, si preoccupa salvare su disco rigido il contenuto dello stesso:

classe Storage
   void salvaSuDisco(doc)
       testo = doc.getTitolo() + " " + doc.getTesto()
       scriviIlFile(rimuoviGliSpazi(doc.getTitolo()) + ".txt", testo)

Ok, ora concentriamoci sulla classe SMS, in una prima istanza potremmo pensare di aggiungere due nuovi metodi per la gestione del dato destinatario:

classe SMS che deriva da Documento
  // oltre ai metodi della classe Documento, che sono automaticamente inclusi in SMS
  string getDestinatario()
  void setDestinatario(string numero_di_telefono)

A questo punto ipotizziamo di creare un oggetto di tipo SMS:

messaggio = new SMS()
messaggio.setDestinatario("555 100 666")
messaggio.setTesto("Winter is coming")

e di volerlo salvare su disco:

disco_usb = new Storage()
disco_usb.salvaSuDisco(messaggio)

Riceviamo un messaggio come questo:

[!] ERRORE, non posso creare un file con nome ".txt"

Cosa è successo? Semplice, un attributo esposto dalla classe padre (Titolo) non viene utilizzato dalla classe figlia ma viene richiesto da alcune funzioni che lavorano con oggetti di tipo Documento. La classica, e sbagliata, soluzione per questo genere di problematiche è la seguente:

classe Storage
  void salvaSuDisco(doc)
    se doc è di tipo SMS
      testo = doc.getDestinatario() + " " + doc.getTesto()
      scriviIlFile(rimuoviGliSpazi(doc.getDestinatario()) + ".txt", testo)
    altrimenti
      testo = doc.getTitolo() + " " + doc.getTesto()
      scriviIlFile(rimuoviGliSpazi(doc.getTitolo()) + ".txt", testo)

Il motivo per cui è richiesta aderenza al principio di Liskov gravita attorno al tipo di intervento che abbiamo appena effettuato: iniettando un controllo esplicito sulla tipologia dell'oggetto sul quale la funzione insiste abbiamo violato l'open/close principle. La classe Storage è ora infatti aperta rispetto a Documento ed a tutte le classi figlie. Questo significa che modifiche/aggiunte alla gerarchia della classe Documento dovranno necessariamente comportare la modifica del metodo salvaSuDisco.

La soluzione

Per gestire questo tipo di situazioni è di solito sufficiente identificare dove il comportamento della classe padre diverge in modo troppo consistente rispetto a quello della classe figlia e decidere in che modo operare:

Astrarre

Generalizzare la classe padre, fino ad astrarla se necessario, in modo da eliminare quegli aspetti
che sono in conflitto con la classe figlia. Questa soluzione comporta però la necessità di reimplementare
l'aspetto rimosso all'interno delle classi figlie che ne beneficiavano realmente,
rischiando di incorrere in pericolose duplicazioni di codice.

Rinunciare all'ereditarietà

Rimuovere la relazione tra classe padre e classe figlia. In questo modo però si perdono tutti
i benefici derivanti dall'ereditarietà: tutti i metodi studiati per la classe padre che possono andar
bene anche per la classe appena separata devono essere riscritti.

Creare un nuovo livello

Creare una nuova classe padre, ad esempio Contenuto e spostare la classe figlia sotto questa. Generalizzare poi Contenuto fino a renderlo compatibile con il comportamento di SMS.

Figura 8. Creare una nuova classe padre
Creare una nuova classe padre

Implementare le differenze nella classe base

Implementare la dissonanza direttamente all'interno della classe padre ed utilizzare l'overriding nella classe figlia per definirne il diverso comportamento.

Ad esempio, la classe Documento potrebbe essere ridefinita come segue:

classe Documento
  string stampa()
    ritorna titolo + " " + testo
  string titoloFile()
    ritorna rimuoviGliSpazi(titolo) + ".documento.txt"

mentre la classe SMS potrebbe contenere la propria versione di stampa():

classe SMS che deriva da Documento
  string stampa()
    ritorna destinatario + " " + testo
  string titoloFile()
    ritorna rimuoviGliSpazi(destinatario) + ".sms.txt"

La funzione salvaSuDisco a questo punto potrebbe limitarsi ad invocare i metodi appena definiti, delegando ai singoli oggetti le corrette implementazioni:

classe Storage
  void salvaSuDisco(doc)
    scriviIlFile(doc.titoloFile(), doc.stampa())

Conclusioni

Il principio di Liskov è importantissimo anche in funzione della sua stretta correlazione con il principio Open/Close che abbiamo visto nella lezione precedente. Un software che non rispetta questo principio rischia di sviluppare un alto grado di viscosità e rigidità in breve tempo.

Ti consigliamo anche