LSP: l’essenza del principio di sostituzione di Barbara Liskov nello sviluppo

Autore: IT Sectr Pubblicato: 2026-05-12 Tempo di lettura: 9 min

LSP (Liskov Substitution Principle) è il terzo principio di SOLID, che definisce le condizioni per un’ereditarietà corretta nella programmazione orientata agli oggetti. Il principio è stato formulato da Barbara Liskov nel 1987 e formalizzato come: se S è un sottotipo di T, allora gli oggetti di tipo T possono essere sostituiti da oggetti di tipo S senza modificare le proprietà del programma. Come osservato nel libro di Robert Martin Clean Architecture (2017), il principio di sostituzione richiede che una sottoclasse non indebolisca il contratto della classe base.

Punti chiave

  • LSP — principio di sostituzione di Liskov, terzo principio SOLID sull’ereditarietà corretta
  • Sottoclasse deve preservare il contratto della classe base — precondizioni e postcondizioni
  • Violazione di LSP si manifesta nel problema del quadrato e del rettangolo e in eccezioni lanciate
  • Composizione è spesso preferibile all’ereditarietà per la conformità LSP
  • Design by Contract — metodo formale per verificare LSP

Cos’è LSP (Liskov Substitution Principle)?

LSP (Liskov Substitution Principle) è il principio di sostituzione formulato da Barbara Liskov alla conferenza OOPSLA nel 1987. Definizione formale: sia q(x) una proprietà dimostrabile degli oggetti x di tipo T. Allora q(y) deve essere dimostrabile per gli oggetti y di tipo S, dove S è un sottotipo di T. In parole povere: gli oggetti di una sottoclasse devono comportarsi in modo che il codice che lavora con la classe base continui a funzionare correttamente anche con la sottoclasse.

In pratica, LSP significa che una sottoclasse non deve violare il contratto della classe base. Il contratto include precondizioni (ciò che è necessario per chiamare un metodo), postcondizioni (ciò che è garantito dopo la chiamata) e invarianti (condizioni che persistono per tutta la vita dell’oggetto). Una sottoclasse può rafforzare le precondizioni o indebolire le postcondizioni — questa è proprio una violazione di LSP.

Un esempio classico di violazione di LSP è un quadrato che eredita da un rettangolo. Il metodo setWidth di un rettangolo imposta la larghezza, mentre per un quadrato imposta sia larghezza che altezza. Un cliente che si aspetta il comportamento di un rettangolo (modificare un lato non influisce sull’altro) ottiene un risultato inaspettato. Un quadrato non è un sottotipo valido di un rettangolo.

Condizioni formali di LSP

LSP stabilisce tre condizioni per un’ereditarietà corretta: le precondizioni della sottoclasse non possono essere più forti di quelle della classe base (la sottoclasse non richiede di più), le postcondizioni della sottoclasse non possono essere più deboli di quelle della classe base (la sottoclasse non garantisce di meno), e gli invarianti della classe base devono essere preservati nella sottoclasse. Queste condizioni sono note come regola del Design by Contract secondo Bertrand Meyer.

Se almeno una condizione viene violata, il codice che utilizza il polimorfismo può fallire. Il compilatore non verifica i contratti semantici, solo quelli sintattici. Pertanto, LSP è una questione di disciplina architettonica, non di tipizzazione statica.

Come funziona il principio di sostituzione di Liskov

Il meccanismo LSP si basa sulla compatibilità comportamentale dei tipi. Se la classe S eredita dalla classe T, il codice cliente deve poter usare S ovunque sia previsto T senza modificare il suo comportamento. Questo include non solo le firme dei metodi ma anche la loro semantica.

LSP non vieta a una sottoclasse di aggiungere nuovo comportamento. È vietato violare le aspettative del codice scritto per la classe base. Se la classe base garantisce che il metodo save non lancia eccezioni, la sottoclasse non deve lanciarle. Se la classe base restituisce un valore non negativo, la sottoclasse non deve restituirne uno negativo.

Nei progetti reali, LSP viene violato più spesso quando si aggiunge logica condizionale nei metodi della sottoclasse: «se condizione — lancia un’eccezione», «se condizione — restituisci null». Ognuna di queste «sorprese» mina il polimorfismo e costringe il codice cliente a verificare il tipo dell’oggetto prima di chiamare — contraddicendo l’idea stessa del design orientato agli oggetti.

Nei progetti mobili, una tipica violazione di LSP si verifica quando si creano ViewModel di base. Se BaseViewModel garantisce che il metodo onCleared rilasci tutte le risorse, e una sottoclasse sovrascrive questo metodo come vuoto — qualsiasi codice che fa affidamento sulla pulizia delle risorse tramite una chiamata polimorfica a onCleared funzionerà in modo errato. LSP richiede che la sottoclasse chiami super.onCleared() o esegua lo stesso lavoro da sola. La composizione tramite LifecycleObserver è un’alternativa che elimina la violazione di LSP nella gestione del ciclo di vita.

Segni di violazione di LSP nel codice

Gli indicatori principali di violazione di LSP includono: verificare il tipo dell’oggetto tramite instanceof o is prima di chiamare un metodo, implementazioni vuote di metodi (stub), lanciare NotImplementedError o UnsupportedOperationException, restituire null invece di un valore. Ciascuno di questi modelli segnala che la sottoclasse non è un sottotipo valido.

Un altro segno comune è l’ereditarietà finalizzata al riutilizzo del codice piuttosto che a modellare una relazione «è-un» (is-a). La classe Bird ha un metodo fly(). La classe Penguin eredita da Bird e sovrascrive fly() come vuoto o che lancia un’eccezione. Questa è una violazione di LSP: un pinguino non è un sottotipo valido di uccello.

Nello sviluppo mobile, LSP viene violato quando si creano classi base ViewHolder, Fragment o ViewController con metodi stub. Se una sottoclasse non usa metà dei metodi della classe base — l’ereditarietà è stata scelta in modo errato. La composizione o la segregazione delle interfacce risolve il problema più correttamente.

Test LSP

Un test semplice per verificare LSP: scrivete un test unitario per la classe base che verifichi il suo contratto (valori restituiti, eccezioni, effetti collaterali). Eseguite questo test per ogni sottoclasse. Se il test fallisce — LSP è violato. Questo approccio si chiama «test attraverso il contratto della classe base».

Nei progetti Android, tale test è utile per ViewModel e Repository. Se BaseViewModel garantisce uno stato Loading prima di un errore, e una sottoclasse lancia un errore senza Loading — il test rileverà la violazione di LSP nella fase CI.

Esempi di LSP nello sviluppo mobile

Consideriamo un esempio Android con la gestione di ClickListener. Una violazione di LSP si verifica quando l’implementazione di base garantisce qualcosa e la sottoclasse lo viola.

kotlin
// Classe base con garanzia: onClick verrà chiamato
open class BaseClickListener {
    open fun onClick(view: View) {
        // gestione di base
    }
}

// Violazione di LSP: la sottoclasse aggiunge una condizione che lancia un’eccezione
class RestrictedClickListener : BaseClickListener() {
    override fun onClick(view: View) {
        if (!isLoggedIn) {
            throw IllegalStateException("Not logged in")
        }
        super.onClick(view)
    }
}

// Soluzione corretta: contratto non violato
class ConditionalClickListener : BaseClickListener() {
    override fun onClick(view: View) {
        if (isLoggedIn) {
            super.onClick(view)
        }
    }
}

Un esempio iOS con il protocollo DataSource dimostra una violazione di LSP restituendo nil invece di dati:

swift
// Protocollo con contratto: restituisce dati o errore
protocol DataProvider {
    func fetchData() async throws -> [String]
}

// Violazione di LSP: restituisce nil senza errore
class SilentFailProvider: DataProvider {
    func fetchData() async throws -> [String] {
        return [] // array vuoto invece di errore
    }
}

// Corretta conformità LSP
class NetworkProvider: DataProvider {
    func fetchData() async throws -> [String] {
        throw NetworkError.timeout
    }
}

Regola pratica: se una sottoclasse non può adempiere al contratto della classe base, non dovrebbe essere una sottoclasse. Un’alternativa è estrarre un’interfaccia con un contratto minimo e implementarla in ogni tipo a modo suo.

LSP ed ereditarietà: quando scegliere la composizione

La composizione è preferibile all’ereditarietà in situazioni in cui la relazione «è-un» (is-a) è ambigua o condizionale. Un esempio classico: un Manager è un Employee? Sì. Ma un Square è un Rectangle valido? LSP dice «no». Se dubitate della correttezza dell’ereditarietà — scegliete la composizione.

Nello sviluppo mobile, la composizione viene spesso utilizzata tramite iniezione di dipendenze: invece di ereditare il comportamento da una classe base, una classe lo riceve tramite un costruttore. Un ViewModel non eredita da Repository ma lo accetta come dipendenza. Ciò elimina la violazione di LSP per definizione — niente ereditarietà, niente violazione del contratto.

Segni che l’ereditarietà dovrebbe essere sostituita dalla composizione: la sottoclasse non usa alcuni metodi della classe base, la sottoclasse sovrascrive metodi con stub vuoti, il codice cliente verifica il tipo dell’oggetto tramite instanceof. In questi casi, l’ereditarietà è stata scelta in modo errato e LSP è violato.

Soluzione tramite interfacce

Le interfacce risolvono il problema LSP senza ereditarietà: ogni tipo implementa esattamente i metodi di cui ha bisogno. Invece di una classe base comune Bird con un metodo fly() (dove Penguin non vola) — un’interfaccia Flyable che solo gli uccelli volanti implementano. Penguin implementa Bird senza il metodo fly() — LSP non è violato.

Nell’architettura Android, questo approccio viene applicato tramite interfacce UseCase segregate: invece di un grande UseCase con metodi getAll, getById, save, delete — interfacce separate GetItemsUseCase, SaveItemUseCase. Un cliente dipende solo dall’interfaccia necessaria, e qualsiasi classe che implementa quell’interfaccia è corretta dal punto di vista LSP.

Domande frequenti

In cosa LSP differisce dalla semplice ereditarietà?

L’ereditarietà è un meccanismo del linguaggio; LSP è una regola per l’uso corretto di tale meccanismo. L’ereditarietà garantisce la compatibilità delle firme (sintassi); LSP richiede la compatibilità comportamentale (semantica). L’ereditarietà senza LSP dà polimorfismo che si rompe a runtime.

Null in una sottoclasse viola sempre LSP?

Se la classe base garantisce un ritorno non nullo — sì. Se il contratto ammette null (valore opzionale) — no. LSP non vieta null; vieta l’indebolimento del contratto. Studiate la documentazione della classe base e verificate se il contratto della sottoclasse è compatibile.

Come si applica LSP ai protocolli in Swift?

LSP si applica ai protocolli allo stesso modo che alle classi. Un’implementazione di protocollo deve rispettare il contratto semantico: se un protocollo definisce un metodo come non-throwing, l’implementazione non deve lanciare errori. Swift non verifica questo a livello di compilatore — la responsabilità è dello sviluppatore.

LSP può essere violato usando sealed class?

Sealed class in Kotlin è un caso speciale perché la gerarchia è chiusa e nota al compilatore. LSP si applica alle sealed class in misura minore perché tutti i sottotipi sono elencati esplicitamente nell’espressione when. Un errore di una sottoclasse sealed sarà locale, non un errore polimorfico nascosto.

Come testare la conformità LSP in un progetto?

Scrivete un test parametrizzato per la classe base che viene eseguito per tutte le sue sottoclassi. Il test verifica i contratti comportamentali chiave: valori restituiti, eccezioni, stati. Se il test fallisce su una delle sottoclassi — LSP è violato. In CI, tale test previene la regressione del codice polimorfico.

Riepilogo

  • LSP (Liskov Substitution Principle) — principio di sostituzione, terzo in SOLID, sulla compatibilità semantica dell’ereditarietà
  • Sottoclasse deve preservare il contratto della classe base: precondizioni, postcondizioni e invarianti
  • Controlli instanceof e sovrascritture vuote di metodi sono i principali segni di violazione di LSP
  • Composizione e interfacce risolvono il problema LSP dove l’ereditarietà è errata
  • Il problema del quadrato e del rettangolo è un esempio classico di incompatibilità di sottotipi
  • Un test del contratto per la classe base, eseguito per tutte le sottoclassi, rileva la violazione di LSP in CI
  • Sealed class in Kotlin riduce i rischi LSP grazie a una gerarchia chiusa nota al compilatore

Svilupperemo un'applicazione mobile chiavi in mano

IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.

Discuti il progetto

Leggi anche