LSP: lényeg, Barbara Liskov helyettesítési elve a fejlesztésben

Szerző: IT Sectr Megjelenés: 2026-05-12 Olvasási idő: 9 perc

LSP (Liskov Substitution Principle) — a SOLID harmadik elve, amely meghatározza a helyes öröklődés feltételeit az objektumorientált programozásban. Az elvet Barbara Liskov fogalmazta meg 1987-ben, és a következőképpen formalizálták: ha S altípusa T-nek, akkor a T objektumai helyettesíthetők S objektumaival a program tulajdonságainak megváltoztatása nélkül. Ahogy Robert Martin Clean Architecture (2017) című könyvében megjegyzi, a helyettesítési elv megköveteli, hogy az alosztály ne gyengítse az alaposztály szerződését.

Főbb pontok

  • LSP — Liskov-féle helyettesítési elv, a SOLID harmadik elve a helyes öröklődésről
  • Alosztály meg kell őrizze az alaposztály szerződését — előfeltételeket és utófeltételeket
  • LSP megsértése a négyzet-téglalap problémában és a dobott kivételekben nyilvánul meg
  • Kompozíció gyakran előnyösebb az öröklődésnél az LSP betartásához
  • Szerződéses tervezés (Design by Contract) — az LSP formális ellenőrzési módja

Mi az LSP (Liskov Substitution Principle)?

LSP (Liskov Substitution Principle) — a helyettesítési elv, amelyet Barbara Liskov fogalmazott meg az OOPSLA konferencián 1987-ben. Formális definíció: legyen q(x) a T típusú x objektumok bizonyítható tulajdonsága. Ekkor q(y)-nak bizonyíthatónak kell lennie az S típusú y objektumokra, ahol S T altípusa. Egyszerűbben: az alosztály objektumainak úgy kell viselkedniük, hogy az alaposztállyal dolgozó kód továbbra is helyesen működjön az alosztállyal.

A gyakorlatban az LSP azt jelenti, hogy az alosztály nem sértheti meg az alaposztály szerződését. A szerződés magában foglalja az előfeltételeket (mi szükséges a metódus meghívásához), az utófeltételeket (mi garantált a hívás után) és az invariánsokat (a feltételek, amelyek az objektum élettartama alatt fennmaradnak). Az alosztály erősítheti az előfeltételeket vagy gyengítheti az utófeltételeket — ez az LSP megsértése.

Az LSP megsértésének klasszikus példája — a téglalapból öröklődő négyzet. A setWidth metódus a téglalapnál a szélességet állítja be, a négyzetnél — mind a szélességet, mind a magasságot. A téglalap viselkedését váró kliens (az egyik oldal változtatása nem befolyásolja a másikat) váratlan eredményt kap. A négyzet nem helyes altípusa a téglalapnak.

Az LSP formális feltételei

Az LSP három feltételt állít fel a helyes öröklődéshez: az alosztály előfeltételei nem lehetnek erősebbek az alaposztály előfeltételeinél (az alosztály nem követel többet), az alosztály utófeltételei nem lehetnek gyengébbek az alaposztály utófeltételeinél (az alosztály nem garantál kevesebbet), az alaposztály invariánsait meg kell őrizni az alosztályban. Ezek a feltételek Bertrand Meyer szerződéses tervezési szabályaként ismertek.

Ha legalább egy feltétel sérül — a polimorfizmust használó kód hibásan működhet. A fordító nem ellenőrzi a szemantikai szerződéseket, csak a szintaktikaiakat. Ezért az LSP építészeti fegyelem kérdése, nem statikus típusosztásé.

Hogyan működik a Liskov-féle helyettesítési elv

Az LSP mechanizmusa a típusok viselkedési kompatibilitásán alapul. Ha az S osztály T osztályból öröklődik, a kliens kódnak használnia kell tudnia S-t mindenhol, ahol T várható, anélkül hogy megváltoztatná a viselkedését. Ez nemcsak a metódusok aláírásait, hanem azok szemantikáját is magában foglalja.

Az LSP nem tiltja az alosztálynak, hogy új viselkedést adjon hozzá. Tilos megsérteni az alaposztály számára írt kód elvárásait. Ha az alaposztály garantálja, hogy a save metódus nem dob kivételt, az alosztály nem dobhatja azokat. Ha az alaposztály nem negatív értéket ad vissza, az alosztály nem adhat vissza negatívot.

Valós projektekben az LSP leggyakrabban feltételes logika hozzáadásával sérül az alosztály metódusaiban: „ha feltétel — dobj kivételt”, „ha feltétel — adj vissza null-t”. Minden ilyen „meglepetés” aláássa a polimorfizmust, és arra kényszeríti a kliens kódot, hogy a hívás előtt ellenőrizze az objektum típusát — ami ellentmond az objektumorientált tervezés alapgondolatának.

Mobil projektekben tipikus LSP-sértés fordul elő az alap ViewModel létrehozásakor. Ha a BaseViewModel garantálja, hogy az onCleared metódus felszabadítja az összes erőforrást, és az alosztály felülírja ezt a metódust üresen — bármely kód, amely az onCleared polimorfikus hívásán keresztüli erőforrás-felszabadításra támaszkodik, helytelenül fog működni. Az LSP megköveteli, hogy az alosztály vagy hívja meg a super.onCleared()-et, vagy maga végezze el ugyanazt a munkát. Kompozíció LifecycleObserver-en keresztül — olyan alternatíva, amely kizárja az LSP megsértését az életciklus kezelésében.

Az LSP megsértésének jelei a kódban

A fő indikátorok az LSP megsértésének a következők: az objektum típusának ellenőrzése instanceof vagy is segítségével a metódushívás előtt, metódusok üres implementációi (stubok), NotImplementedError vagy UnsupportedOperationException kivétel dobása, null visszaadása érték helyett. Ezen minták mindegyike azt jelzi, hogy az alosztály nem helyes altípus.

Egy másik gyakori jel — az öröklődés kód újrafelhasználása céljából, nem pedig az „egy” (is-a) kapcsolat modellezésére. A Bird osztálynak van egy fly() metódusa. A Penguin osztály öröklődik Bird-ből, és felülírja a fly()-t üresként vagy kivételt dobóként. Ez az LSP megsértése: a pingvin nem helyes altípusa a madárnak.

A mobilfejlesztésben az LSP sérül az alap ViewHolder, Fragment vagy ViewController stub metódusokkal történő létrehozásakor. Ha az alosztály nem használja az alaposztály metódusainak felét — az öröklődés helytelenül lett kiválasztva. Kompozíció vagy interfész szétválasztása oldja meg a problémát helyesebben.

LSP teszt

Egyszerű teszt az LSP ellenőrzésére: írjon egységtesztet az alaposztályhoz, amely ellenőrzi annak szerződését (visszaadott értékek, kivételek, mellékhatások). Futtassa ezt a tesztet minden egyes alosztályra. Ha a teszt sikertelen — az LSP sérült. Ezt a megközelítést „tesztelés az alaposztály szerződésén keresztül”-nek nevezik.

Android projektekben egy ilyen teszt hasznos a ViewModel és Repository számára. Ha a BaseViewModel garantálja a Loading állapotot a hiba előtt, és az alosztály hibát dob Loading nélkül — a teszt rögzíti az LSP megsértését a CI szakaszban.

LSP példák a mobilfejlesztésben

Vizsgáljuk meg az Android példát a ClickListener kezelésével. Az LSP megsértése akkor következik be, amikor az alap implementáció garantál valamit, és az alosztály megsérti azt.

kotlin
// Alaposztály garanciával: onClick meghívásra kerül
open class BaseClickListener {
    open fun onClick(view: View) {
        // alap feldolgozás
    }
}

// LSP megsértése: az alosztály olyan feltételt ad hozzá, amely kivételt dob
class RestrictedClickListener : BaseClickListener() {
    override fun onClick(view: View) {
        if (!isLoggedIn) {
            throw IllegalStateException("Not logged in")
        }
        super.onClick(view)
    }
}

// Helyes megoldás: a szerződés nem sérült
class ConditionalClickListener : BaseClickListener() {
    override fun onClick(view: View) {
        if (isLoggedIn) {
            super.onClick(view)
        }
    }
}

Az iOS példa a DataSource protokollal az LSP megsértését mutatja be nil visszaadásával adat helyett:

swift
// Szerződéses protokoll: adatot vagy hibát ad vissza
protocol DataProvider {
    func fetchData() async throws -> [String]
}

// LSP megsértése: nil-t ad vissza hiba nélkül
class SilentFailProvider: DataProvider {
    func fetchData() async throws -> [String] {
        return [] // üres tömb hiba helyett
    }
}

// Az LSP helyes betartása
class NetworkProvider: DataProvider {
    func fetchData() async throws -> [String] {
        throw NetworkError.timeout
    }
}

Gyakorlati szabály: ha az alosztály nem tudja teljesíteni az alaposztály szerződését — ne legyen alosztály. Alternatíva — hozzon létre egy interfészt minimális szerződéssel, és implementálja azt minden típusban a saját módján.

LSP és öröklődés: mikor válasszuk a kompozíciót

Kompozíció előnyösebb az öröklődésnél olyan helyzetekben, ahol az „egy” (is-a) kapcsolat nem egyértelmű vagy feltételes. Klasszikus példa: A Manager Employee? Igen. De vajon a Square helyes Rectangle? Az LSP azt mondja: „nem”. Ha kétségei vannak az öröklődés helyességével kapcsolatban — válassza a kompozíciót.

A mobilfejlesztésben a kompozíciót gyakran függőséginjektáláson keresztül használják: ahelyett, hogy az alaposztálytól örökölné a viselkedést, az osztály a konstruktoron keresztül kapja meg. A ViewModel nem öröklődik a Repository-ból, hanem függőségként fogadja el. Ez definíció szerint kizárja az LSP megsértését — nincs öröklődés, nincs szerződésszegés.

Jelek arra, hogy az öröklődést kompozícióra kell cserélni: az alosztály nem használja az alaposztály metódusainak egy részét, az alosztály felülírja a metódusokat üres stubokkal, a kliens kód instanceof segítségével ellenőrzi az objektum típusát. Ezekben az esetekben az öröklődés helytelenül lett kiválasztva, és az LSP sérül.

Megoldás interfészekkel

Interfészek megoldják az LSP problémáját öröklődés nélkül: minden típus pontosan azokat a metódusokat implementálja, amelyekre szüksége van. A közös Bird alaposztály helyett a fly() metódussal (ahol a Penguin nem repül) — a Flyable interfész, amelyet csak a repülő madarak implementálnak. A Penguin a Bird-öt implementálja a fly() metódus nélkül — az LSP nem sérül.

Az Android architektúrában ezt a megközelítést szegregált interfészek UseCase segítségével alkalmazzák: egy nagy UseCase helyett a getAll, getById, save, delete metódusokkal — külön GetItemsUseCase, SaveItemUseCase interfészek. A kliens csak a szükséges interfésztől függ, és minden osztály, amely implementálja ezt az interfészt, helyes az LSP szempontjából.

Gyakran ismételt kérdések

Miben különbözik az LSP a hétköznapi öröklődéstől?

Öröklődés — nyelvi mechanizmus, LSP — a mechanizmus helyes használatának szabálya. Az öröklődés garantálja az aláírások kompatibilitását (szintaxis), az LSP a viselkedés kompatibilitását (szemantika) követeli meg. Az LSP nélküli öröklődés olyan polimorfizmust ad, amely futásidőben elromlik.

Az alosztályban lévő null mindig megsérti az LSP-t?

Ha az alaposztály non-null visszatérést garantál — igen. Ha a szerződés megengedi a null-t (opcionális érték) — nem. Az LSP nem tiltja a null-t, a szerződés gyengítését tiltja. Tanulmányozza az alaposztály dokumentációját, és ellenőrizze, hogy az alosztály szerződése kompatibilis-e.

Hogyan alkalmazható az LSP a Swift protokollokra?

A protokollokra az LSP ugyanúgy alkalmazandó, mint az osztályokra. A protokoll implementációjának be kell tartania a szemantikai szerződést: ha a protokoll egy metódust non-throwing-ként definiál, az implementáció nem dobhat hibát. A Swift ezt nem ellenőrzi fordítási szinten — a felelősség a fejlesztőé.

Megsérthető-e az LSP sealed class használatakor?

A Kotlin Sealed class — speciális eset, mivel a hierarchia zárt és ismert a fordító számára. Az LSP kisebb mértékben alkalmazható a sealed class-ra, mivel az összes altípus explicit módon fel van sorolva a when kifejezésben. A sealed-alosztály hibája lokális lesz, nem rejtett polimorfikus hiba.

Hogyan teszteljük az LSP betartását egy projektben?

Írjon egy paraméterezett tesztet az alaposztályhoz, amely az összes alosztályára lefut. A teszt ellenőrzi a kulcsfontosságú viselkedési szerződéseket: visszaadott értékeket, kivételeket, állapotokat. Ha a teszt az egyik alosztálynál sikertelen — az LSP sérült. CI-ban egy ilyen teszt megakadályozza a polimorf kód regresszióját.

Összefoglalás

  • LSP (Liskov Substitution Principle) — helyettesítési elv, a SOLID harmadik eleme, az öröklődés szemantikai kompatibilitásáról
  • Alosztály meg kell őrizze az alaposztály szerződését: előfeltételeket, utófeltételeket és invariánsokat
  • instanceof ellenőrzés és metódusok üres felülírása — az LSP megsértésének fő jelei
  • Kompozíció és interfészek megoldják az LSP problémáját ott, ahol az öröklődés helytelen
  • Négyzet-téglalap probléma — az altípusok összeférhetetlenségének klasszikus példája
  • Szerződés teszt az alaposztályhoz, az összes alosztályra futtatva, észleli az LSP megsértését CI-ban
  • A Kotlin Sealed class csökkenti az LSP kockázatait a zárt, fordító által ismert hierarchia révén

Kulcsrakész mobilalkalmazást fejlesztünk

Az IT Sectr 2017 óta készít iOS és Android alkalmazásokat induló vállalkozásoknak és vállalkozásoknak. Tanácsot adunk, és a legjobb megoldást javasoljuk.

Projekt megbeszélése

Olvassa el is