TIL - Principio di sostituzione di Liskov


Oggi ho letto un paper sul principio di sostituzione di Liskov chiamato anche LSP1.

LSP fa parte di una serie di principi che insieme formano il S.O.L.I.D.:

  • S -> SRP (Single Responsability Principle)
  • O -> OCP (Open Closed Principle)
  • L -> LSP (Liskov substitution Principle)
  • I -> ISP (Interface Segregation Principle)
  • D -> DIP (Dependency Inversion Principle)

Classi che usano referenze a classi base devono essere capaci di usare oggetti di classi derivate senza che lo sappiano.

L’importanza di questo principio diventa ovvia proprio quando si notano le conseguenze della sua violazione.

Se un metodo viola il LSP ha bisogno di sapere quali sono tutte le classi derivate da una data classe base per poter essere “aggiornato”, violando il principio di Apertura-Chiusura presente nel SOLID.

Un esempio ricorrente è sicuramente quello della classe Rectangle:

public class Rectangle {
    private int length;
    private int breadth;

    public int getLength() {
        return length;
    }

    public int getBreadth() {
        return breadth;
    }

    public void setLength(int length) {
        this.length = length;
    }

    public void setBreadth(int breadth) {
        this.breadth = breadth;
    }

    public int getArea() {
        return this.length * this.breadth;
    }
}

Che viene estesa dalla classe Square; di primo acchitto possiam pensare: “Hey ma un quadrato è anche un rettangolo! Ma con piccole differenze…” così:

public class Square extends Rectangle {
    
    @Override
    public void setBreadth(int breadth) {
        super.setBreadth(breadth);
	super.setLength(breadth);
    }

    @Override
    public void setLength(int length) {
        super.setLength(length);
	super.setBreadth(length);
    }
}

Fatto ciò usiamo queste due classi:

public class Demo {
    public int calcArea(Rectangle r) {
        r.setBreadth(2);
	r.setLength(3);
	return r.getArea();
    }

    public static void main(String[] args) {
        Demo lsp = new Demo();

	lsp.calcArea(new Rectangle()); //6

	lsp.calcArea(new Square()); // 9
    }
}

Notiamo qualcosa:

  • La classe Square non ha bisogno dei metodi setBreadth o setLength in quanto i lati sono uguali, uno spreco di risorse non indifferente se consideriamo la creazione di centinaia di oggetti se non di più.
  • La classe Demo ha bisogno di conoscere i dettagli della classe derivata da Rectangle per evitare in modo appropriato errori. Questo viola il principio di apertura-chiusura.

Il principio di sostituzione di Liskov dice chiaramente che se sostituissimo Square con Rectangle non dovrebbe cambiare nulla in termini di “funzionalità”, in questo caso invece si nota che cambiando la classe cambiano anche le funzionalità, infatti: se passiamo Square e settiamo length o breadth entrambe vengono uguagliate dai metodi overloadati della classe Square (in quanto un quadrato ha entrambi i lati uguali), in caso di un possibile Unit Test questo andrebbe rosso. WRONG!

Come evitare la violazione del princio di sostituzione di Liskov?

  • Pensare bene al design è molto importante in quanto possiamo prevedere a priori quando un determinato tipo è COMPLETAMENTE sostituibile al tipo base che può essere usato e cambiato senza troppi fronzoli.
  • Il LSP viene chiamato “Design by Contract”, cioè definire delle condizioni (pre e post all’esecuzione del metodo che fa uso di questi oggetti) che consentono il corretto stato di ciò che volevamo.
  • Tenere la classe base la più semplice e minimale possibile rendendola facile da estendere per le classi derivate senza il bisogno di dover ricorrere all’override e quindi all’introduzione di cambiamenti funzionali.
  • Usare la composizione anziché l’ereditarietà.

  1. LSP