LSP: esența, principiul substituției Barbara Liskov în dezvoltare

Autor: IT Sectr Publicat: 2026-05-12 Timp de citire: 9 min

LSP (Liskov Substitution Principle) — al treilea principiu SOLID care definește condițiile moștenirii corecte în programarea orientată pe obiecte. Principiul a fost formulat de Barbara Liskov în 1987 și formalizat astfel: dacă S este un subtip al lui T, atunci obiectele T pot fi înlocuite cu obiecte S fără a modifica proprietățile programului. După cum se menționează în cartea lui Robert Martin Clean Architecture (2017), principiul substituției cere ca subclasa să nu slăbească contractul clasei de bază.

Puncte principale

  • LSP — principiul substituției Liskov, al treilea principiu SOLID despre moștenirea corectă
  • Subclasa trebuie să păstreze contractul clasei de bază — precondiții și postcondiții
  • Încălcarea LSP se manifestă în problema pătratului și dreptunghiului și excepțiile aruncate
  • Compoziția este adesea preferabilă moștenirii pentru respectarea LSP
  • Proiectarea prin contract (Design by Contract) — metodă formală de verificare LSP

Ce este LSP (Liskov Substitution Principle)?

LSP (Liskov Substitution Principle) — principiul substituției formulat de Barbara Liskov la conferința OOPSLA în 1987. Definiția formală: fie q(x) o proprietate demonstrabilă a obiectelor x de tip T. Atunci q(y) trebuie să fie demonstrabilă pentru obiectele y de tip S, unde S este subtipul lui T. Mai simplu: obiectele subclasei trebuie să se comporte astfel încât codul care lucrează cu clasa de bază să continue să funcționeze corect și cu subclasa.

În practică LSP înseamnă că subclasa nu trebuie să încalce contractul clasei de bază. Contractul include precondiții (ce este necesar pentru apelarea metodei), postcondiții (ce este garantat după apel) și invarianți (condiții care se păstrează pe durata vieții obiectului). Subclasa poate întări precondițiile sau slăbi postcondițiile — aceasta este încălcarea LSP.

Exemplul clasic de încălcare LSP — pătratul care moștenește de la dreptunghi. Metoda setWidth la dreptunghi setează lățimea, la pătrat — și lățimea, și înălțimea. Clientul care așteaptă comportamentul dreptunghiului (modificarea unei laturi nu o afectează pe cealaltă) primește un rezultat neașteptat. Pătratul nu este un subtip corect al dreptunghiului.

Condițiile formale ale LSP

LSP stabilește trei condiții pentru moștenirea corectă: precondițiile subclasei nu pot fi mai puternice decât precondițiile clasei de bază (subclasa nu cere mai mult), postcondițiile subclasei nu pot fi mai slabe decât postcondițiile clasei de bază (subclasa garantează nu mai puțin), invarianții clasei de bază trebuie păstrați în subclasă. Aceste condiții sunt cunoscute ca regula proiectării prin contract a lui Bertrand Meyer.

Dacă cel puțin o condiție este încălcată — codul care folosește polimorfismul poate funcționa defectuos. Compilatorul nu verifică contractele semantice, ci doar pe cele sintactice. De aceea LSP este o chestiune de disciplină arhitecturală, nu de tipizare statică.

Cum funcționează principiul substituției Liskov

Mecanismul LSP se bazează pe compatibilitatea comportamentală a tipurilor. Dacă clasa S moștenește de la clasa T, codul client trebuie să poată folosi S oriunde este așteptat T, fără a-și modifica comportamentul. Aceasta include nu doar semnăturile metodelor, ci și semantica lor.

LSP nu interzice subclasei să adauge comportament nou. Este interzisă încălcarea așteptărilor codului scris pentru clasa de bază. Dacă clasa de bază garantează că metoda save nu aruncă excepții, subclasa nu trebuie să le arunce. Dacă clasa de bază returnează o valoare nenegativă, subclasa nu trebuie să returneze una negativă.

În proiecte reale LSP este cel mai des încălcat prin adăugarea de logică condițională în metodele subclasei: «dacă condiția — aruncă excepție», «dacă condiția — returnează null». Fiecare astfel de «surpriză» subminează polimorfismul și forțează codul client să verifice tipul obiectului înainte de apel — ceea ce contrazice însăși ideea proiectării orientate pe obiecte.

În proiectele mobile încălcarea tipică LSP apare la crearea ViewModel de bază. Dacă BaseViewModel garantează că metoda onCleared eliberează toate resursele, iar subclasa suprascrie această metodă ca goală — orice cod care se bazează pe eliberarea resurselor prin apelul polimorfic onCleared va funcționa incorect. LSP cere ca subclasa fie să apeleze super.onCleared(), fie să execute ea însăși aceeași muncă. Compoziția prin LifecycleObserver — alternativă care exclude încălcarea LSP la gestionarea ciclului de viață.

Semne ale încălcării LSP în cod

Indicatorii principali ai încălcării LSP includ: verificarea tipului obiectului prin instanceof sau is înainte de apelarea metodei, implementări goale ale metodelor (stub-uri), aruncarea excepției NotImplementedError sau UnsupportedOperationException, returnarea null în locul valorii. Fiecare dintre aceste pattern-uri semnalează că subclasa nu este un subtip corect.

Un alt semn frecvent — moștenirea în scopul reutilizării codului, nu pentru modelarea relației «este» (is-a). Clasa Bird are metoda fly(). Clasa Penguin moștenește de la Bird și suprascrie fly() ca goală sau care aruncă excepție. Aceasta este încălcarea LSP: pinguinul nu este un subtip corect al păsării.

În dezvoltarea mobilă LSP este încălcat la crearea ViewHolder, Fragment sau ViewController de bază cu metode stub. Dacă subclasa nu folosește jumătate din metodele clasei de bază — moștenirea a fost aleasă incorect. Compoziția sau divizarea interfeței rezolvă problema mai corect.

Testul LSP

Test simplu pentru verificarea LSP: scrieți un test unitar pentru clasa de bază care verifică contractul său (valori returnate, excepții, efecte secundare). Rulați acest test pentru fiecare subclasă. Dacă testul eșuează — LSP este încălcat. Această abordare se numește «testare prin contractul clasei de bază».

În proiectele Android un astfel de test este util pentru ViewModel și Repository. Dacă BaseViewModel garantează starea Loading înainte de eroare, iar subclasa aruncă eroarea fără Loading — testul va detecta încălcarea LSP în faza CI.

Exemple LSP în dezvoltarea mobilă

Să examinăm exemplul Android cu gestionarea ClickListener. Încălcarea LSP apare când implementarea de bază garantează ceva, iar subclasa încalcă.

kotlin
// Clasă de bază cu garanție: onClick va fi apelat
open class BaseClickListener {
    open fun onClick(view: View) {
        // procesare de bază
    }
}

// Încălcare LSP: subclasa adaugă o condiție care aruncă excepție
class RestrictedClickListener : BaseClickListener() {
    override fun onClick(view: View) {
        if (!isLoggedIn) {
            throw IllegalStateException("Not logged in")
        }
        super.onClick(view)
    }
}

// Soluție corectă: contractul nu este încălcat
class ConditionalClickListener : BaseClickListener() {
    override fun onClick(view: View) {
        if (isLoggedIn) {
            super.onClick(view)
        }
    }
}

Exemplul iOS cu protocolul DataSource demonstrează încălcarea LSP prin returnarea nil în loc de date:

swift
// Protocol cu contract: returnează date sau eroare
protocol DataProvider {
    func fetchData() async throws -> [String]
}

// Încălcare LSP: returnează nil fără eroare
class SilentFailProvider: DataProvider {
    func fetchData() async throws -> [String] {
        return [] // array gol în loc de eroare
    }
}

// Respectarea corectă a LSP
class NetworkProvider: DataProvider {
    func fetchData() async throws -> [String] {
        throw NetworkError.timeout
    }
}

Regula practică: dacă subclasa nu poate îndeplini contractul clasei de bază — nu trebuie să fie subclasă. Alternativa — extragerea unei interfețe cu un contract minim și implementarea ei în fiecare tip în felul său.

LSP și moștenirea: când să alegeți compoziția

Compoziția este preferabilă moștenirii în situațiile în care relația «este» (is-a) este ambiguă sau condiționată. Exemplu clasic: Manager este Employee? Da. Dar Square este un Rectangle corect? LSP spune «nu». Dacă aveți îndoieli privind corectitudinea moștenirii — alegeți compoziția.

În dezvoltarea mobilă compoziția este adesea folosită prin injectarea dependențelor: în loc să moștenească comportamentul de la clasa de bază, clasa îl primește prin constructor. ViewModel nu moștenește de la Repository, ci îl acceptă ca dependență. Aceasta exclude încălcarea LSP prin definiție — nu există moștenire, nu există încălcare a contractului.

Semne că moștenirea trebuie înlocuită cu compoziție: subclasa nu folosește o parte din metodele clasei de bază, subclasa suprascrie metode cu stub-uri goale, codul client verifică tipul obiectului prin instanceof. În aceste cazuri moștenirea a fost aleasă incorect și LSP este încălcat.

Soluția prin interfețe

Interfețele rezolvă problema LSP fără moștenire: fiecare tip implementează exact metodele de care are nevoie. În locul unei clase de bază comune Bird cu metoda fly (unde Penguin nu zboară) — interfața Flyable, pe care o implementează doar păsările zburătoare. Penguin implementează Bird fără metoda fly — LSP nu este încălcat.

În arhitectura Android această abordare se aplică prin interfețe segregate UseCase: în loc de un UseCase mare cu metodele getAll, getById, save, delete — interfețe separate GetItemsUseCase, SaveItemUseCase. Clientul depinde doar de interfața necesară și orice clasă care implementează această interfață este corectă din punctul de vedere al LSP.

Întrebări frecvente

Cu ce diferă LSP de moștenirea simplă?

Moștenirea — mecanism al limbajului, LSP — regulă de utilizare corectă a acestui mecanism. Moștenirea garantează compatibilitatea semnăturilor (sintaxă), LSP cere compatibilitatea comportamentului (semantică). Moștenirea fără LSP oferă polimorfism care se strică în runtime.

Null în subclasă încalcă întotdeauna LSP?

Dacă clasa de bază garantează returnarea non-null — da. Dacă contractul permite null (valoare opțională) — nu. LSP nu interzice null, interzice slăbirea contractului. Studiați documentația clasei de bază și verificați dacă contractul subclasei este compatibil.

Cum se aplică LSP la protocoale în Swift?

La protocoale LSP se aplică la fel ca la clase. Implementarea protocolului trebuie să respecte contractul semantic: dacă protocolul definește o metodă ca non-throwing, implementarea nu trebuie să arunce erori. Swift nu verifică acest lucru la nivel de compilare — responsabilitatea revine dezvoltatorului.

Poate LSP să fie încălcat la utilizarea sealed class?

Sealed class în Kotlin — un caz special, deoarece ierarhia este închisă și cunoscută compilatorului. LSP se aplică într-o măsură mai mică sealed class, deoarece toate subtipurile sunt enumerate explicit în expresia when. Eroarea subclasei sealed va fi locală, nu o eroare polimorfică ascunsă.

Cum să testați respectarea LSP într-un proiect?

Scrieți un test parametrizat pentru clasa de bază care se rulează pentru toate subclasele sale. Testul verifică contractele comportamentale cheie: valori returnate, excepții, stări. Dacă testul eșuează pentru una dintre subclase — LSP este încălcat. În CI un astfel de test previne regresia codului polimorfic.

Rezumat

  • LSP (Liskov Substitution Principle) — principiul substituției, al treilea în SOLID, despre compatibilitatea semantică a moștenirii
  • Subclasa trebuie să păstreze contractul clasei de bază: precondiții, postcondiții și invarianți
  • Verificarea instanceof și suprascrierile goale ale metodelor — principalele semne ale încălcării LSP
  • Compoziția și interfețele rezolvă problema LSP acolo unde moștenirea este incorectă
  • Problema pătratului și dreptunghiului — exemplu clasic de incompatibilitate a subtipurilor
  • Testul contractului pentru clasa de bază, rulat pentru toate subclasele, detectează încălcarea LSP în CI
  • Sealed class în Kotlin reduce riscurile LSP datorită ierarhiei închise cunoscute compilatorului

Vom dezvolta o aplicație mobilă la cheie

IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.

Discutați proiectul

Citiți și