LSP: podstata, princip substituce Barbary Liskové ve vývoji

Autor: IT Sectr Publikováno: 2026-05-12 Doba čtení: 9 min

LSP (Liskov Substitution Principle) — třetí princip SOLID, který určuje podmínky správné dědičnosti v objektově orientovaném programování. Princip byl formulován Barbarou Liskovou v roce 1987 a formalizován jako: pokud je S podtypem T, pak objekty T mohou být nahrazeny objekty S bez změny vlastností programu. Jak je uvedeno v knize Roberta Martina Clean Architecture (2017), princip substituce vyžaduje, aby podtřída neoslabovala kontrakt základní třídy.

Hlavní body

  • LSP — princip substituce Liskové, třetí princip SOLID o správné dědičnosti
  • Podtřída musí zachovat kontrakt základní třídy — předpoklady a následné podmínky
  • Porušení LSP se projevuje v problému čtverce a obdélníku a vyhazovaných výjimkách
  • Kompozice je často výhodnější než dědičnost pro dodržování LSP
  • Návrh podle kontraktu (Design by Contract) — formální způsob ověření LSP

Co je LSP (Liskov Substitution Principle)?

LSP (Liskov Substitution Principle) — princip substituce formulovaný Barbarou Liskovou na konferenci OOPSLA v roce 1987. Formální definice: nechť q(x) je prokazatelná vlastnost objektů x typu T. Pak q(y) musí být prokazatelné pro objekty y typu S, kde S je podtyp T. Jednoduše řečeno: objekty podtřídy by se měly chovat tak, aby kód pracující se základní třídou nadále správně fungoval i s podtřídou.

V praxi LSP znamená, že podtřída nesmí porušovat kontrakt základní třídy. Kontrakt zahrnuje předpoklady (co je nutné pro volání metody), následné podmínky (co je zaručeno po volání) a invarianty (podmínky zachovávající se po dobu života objektu). Podtřída může posilovat předpoklady nebo oslabovat následné podmínky — to je porušení LSP.

Klasický příklad porušení LSP — čtverec dědící z obdélníku. Metoda setWidth u obdélníku nastavuje šířku, u čtverce — jak šířku, tak výšku. Klient očekávající chování obdélníku (změna jedné strany neovlivňuje druhou) dostává neočekávaný výsledek. Čtverec není správným podtypem obdélníku.

Formální podmínky LSP

LSP stanovuje tři podmínky pro správnou dědičnost: předpoklady podtřídy nemohou být silnější než předpoklady základní třídy (podtřída nevyžaduje více), následné podmínky podtřídy nemohou být slabší než následné podmínky základní třídy (podtřída negarantuje méně), invarianty základní třídy musí být zachovány v podtřídě. Tyto podmínky jsou známé jako pravidlo návrhu podle kontraktu Bertranda Meyera.

Pokud je porušena alespoň jedna podmínka — kód používající polymorfismus může selhat. Kompilátor nekontroluje sémantické kontrakty, pouze syntaktické. Proto je LSP otázkou architektonické disciplíny, nikoli statického typování.

Jak funguje princip substituce Liskové

Mechanismus LSP je založen na behaviorální kompatibilitě typů. Pokud třída S dědí z třídy T, klientský kód by měl být schopen používat S všude, kde se očekává T, aniž by změnil své chování. To zahrnuje nejen signatury metod, ale i jejich sémantiku.

LSP nezakazuje podtřídě přidávat nové chování. Zakázáno je porušovat očekávání kódu napsaného pro základní třídu. Pokud základní třída garantuje, že metoda save nevyhazuje výjimky, podtřída by je neměla vyhazovat. Pokud základní třída vrací nezápornou hodnotu, podtřída by neměla vracet zápornou.

V reálných projektech je LSP nejčastěji porušováno přidáním podmíněné logiky do metod podtřídy: „pokud podmínka — vyhoď výjimku”, „pokud podmínka — vrať null”. Každé takové „překvapení” podkopává polymorfismus a nutí klientský kód kontrolovat typ objektu před voláním — což je v rozporu se samotnou myšlenkou objektově orientovaného návrhu.

V mobilních projektech se typické porušení LSP vyskytuje při vytváření základního ViewModel. Pokud BaseViewModel garantuje, že metoda onCleared uvolňuje všechny zdroje, a podtřída tuto metodu přepíše jako prázdnou — jakýkoli kód spoléhající na uvolňování zdrojů prostřednictvím polymorfního volání onCleared bude pracovat nesprávně. LSP vyžaduje, aby podtřída buď volala super.onCleared(), nebo sama provedla stejnou práci. Kompozice prostřednictvím LifecycleObserver — alternativa vylučující porušení LSP při správě životního cyklu.

Příznaky porušení LSP v kódu

Hlavní indikátory porušení LSP zahrnují: kontrolu typu objektu pomocí instanceof nebo is před voláním metody, prázdné implementace metod (stuby), vyhazování výjimky NotImplementedError nebo UnsupportedOperationException, vracení null místo hodnoty. Každý z těchto vzorů signalizuje, že podtřída není správným podtypem.

Dalším častým příznakem je dědičnost za účelem opětovného použití kódu, nikoli modelování vztahu „je” (is-a). Třída Bird má metodu fly(). Třída Penguin dědí z Bird a přepisuje fly() jako prázdnou nebo vyhazující výjimku. To je porušení LSP: tučňák není správným podtypem ptáka.

V mobilním vývoji je LSP porušováno při vytváření základního ViewHolder, Fragment nebo ViewController se stub metodami. Pokud podtřída nepoužívá polovinu metod základní třídy — dědičnost byla zvolena nesprávně. Kompozice nebo rozdělení rozhraní řeší problém správněji.

Test LSP

Jednoduchý test pro ověření LSP: napište unit test pro základní třídu kontrolující její kontrakt (návratové hodnoty, výjimky, vedlejší účinky). Spusťte tento test pro každou podtřídu. Pokud test selže — LSP je porušeno. Tento přístup se nazývá „testování prostřednictvím kontraktu základní třídy”.

V projektech Android je takový test užitečný pro ViewModel a Repository. Pokud BaseViewModel garantuje stav Loading před chybou a podtřída vyhazuje chybu bez Loadingu — test zachytí porušení LSP ve fázi CI.

Příklady LSP v mobilním vývoji

Podívejme se na příklad Android se zpracováním ClickListener. Porušení LSP nastává, když základní implementace něco garantuje a podtřída to porušuje.

kotlin
// Základní třída s garancí: onClick bude zavolán
open class BaseClickListener {
    open fun onClick(view: View) {
        // základní zpracování
    }
}

// Porušení LSP: podtřída přidává podmínku vyhazující výjimku
class RestrictedClickListener : BaseClickListener() {
    override fun onClick(view: View) {
        if (!isLoggedIn) {
            throw IllegalStateException("Not logged in")
        }
        super.onClick(view)
    }
}

// Správné řešení: kontrakt nebyl porušen
class ConditionalClickListener : BaseClickListener() {
    override fun onClick(view: View) {
        if (isLoggedIn) {
            super.onClick(view)
        }
    }
}

Příklad iOS s protokolem DataSource demonstruje porušení LSP vracením nil místo dat:

swift
// Protokol s kontraktem: vrací data nebo chybu
protocol DataProvider {
    func fetchData() async throws -> [String]
}

// Porušení LSP: vrací nil bez chyby
class SilentFailProvider: DataProvider {
    func fetchData() async throws -> [String] {
        return [] // prázdné pole místo chyby
    }
}

// Správné dodržování LSP
class NetworkProvider: DataProvider {
    func fetchData() async throws -> [String] {
        throw NetworkError.timeout
    }
}

Praktické pravidlo: pokud podtřída nemůže splnit kontrakt základní třídy — neměla by být podtřídou. Alternativa — extrahovat rozhraní s minimálním kontraktem a implementovat ho v každém typu po svém.

LSP a dědičnost: kdy zvolit kompozici

Kompozice je výhodnější než dědičnost v situacích, kde je vztah „je” (is-a) nejednoznačný nebo podmíněný. Klasický příklad: Manager je Employee? Ano. Ale je Square správný Rectangle? LSP říká „ne”. Pokud pochybujete o správnosti dědičnosti — zvolte kompozici.

V mobilním vývoji se kompozice často používá prostřednictvím vkládání závislostí: místo dědění chování od základní třídy, třída ho získává přes konstruktor. ViewModel nedědí z Repository, ale přijímá ho jako závislost. To vylučuje porušení LSP z definice — není dědičnost, není porušení kontraktu.

Příznaky, že dědičnost by měla být nahrazena kompozicí: podtřída nepoužívá část metod základní třídy, podtřída přepisuje metody prázdnými stuby, klientský kód kontroluje typ objektu pomocí instanceof. V těchto případech byla dědičnost zvolena nesprávně a LSP je porušeno.

Řešení pomocí rozhraní

Rozhraní řeší problém LSP bez dědičnosti: každý typ implementuje přesně ty metody, které potřebuje. Místo společné základní třídy Bird s metodou fly (kde Penguin nelétá) — rozhraní Flyable, které implementují pouze létající ptáci. Penguin implementuje Bird bez metody fly — LSP není porušeno.

V architektuře Android se tento přístup aplikuje prostřednictvím segregovaných rozhraní UseCase: místo jednoho velkého UseCase s metodami getAll, getById, save, delete — samostatná rozhraní GetItemsUseCase, SaveItemUseCase. Klient závisí pouze na potřebném rozhraní a každá třída implementující toto rozhraní je z hlediska LSP správná.

Často kladené otázky

Čím se LSP liší od běžné dědičnosti?

Dědičnost — jazykový mechanismus, LSP — pravidlo správného použití tohoto mechanismu. Dědičnost garantuje kompatibilitu signatur (syntax), LSP vyžaduje kompatibilitu chování (sémantika). Dědičnost bez LSP dává polymorfismus, který se v runtime rozbíjí.

Porušuje null v podtřídě vždy LSP?

Pokud základní třída garantuje non-null návrat — ano. Pokud kontrakt povoluje null (volitelná hodnota) — ne. LSP nezakazuje null, zakazuje oslabení kontraktu. Prostudujte dokumentaci základní třídy a zkontrolujte, zda je kontrakt podtřídy kompatibilní.

Jak se LSP aplikuje na protokoly ve Swiftu?

Na protokoly se LSP aplikuje stejně jako na třídy. Implementace protokolu musí dodržovat sémantický kontrakt: pokud protokol definuje metodu jako non-throwing, implementace by neměla vyhazovat chyby. Swift to nekontroluje na úrovni kompilace — odpovědnost je na vývojáři.

Může být LSP porušeno při použití sealed class?

Sealed class v Kotlin — zvláštní případ, protože hierarchie je uzavřená a známá kompilátoru. LSP se na sealed class vztahuje v menší míře, protože všechny podtypy jsou explicitně uvedeny ve výrazu when. Chyba sealed-podtřídy bude lokální, nikoli skrytá polymorfní chyba.

Jak testovat dodržování LSP v projektu?

Napište parametrizovaný test pro základní třídu, který se spouští pro všechny její podtřídy. Test kontroluje klíčové behaviorální kontrakty: návratové hodnoty, výjimky, stavy. Pokud test selže na jedné z podtříd — LSP je porušeno. V CI takový test zabraňuje regresi polymorfního kódu.

Shrnutí

  • LSP (Liskov Substitution Principle) — princip substituce, třetí v SOLID, o sémantické kompatibilitě dědičnosti
  • Podtřída musí zachovat kontrakt základní třídy: předpoklady, následné podmínky a invarianty
  • Kontrola instanceof a prázdná přepsání metod — hlavní příznaky porušení LSP
  • Kompozice a rozhraní řeší problém LSP tam, kde je dědičnost nesprávná
  • Problém čtverce a obdélníku — klasický příklad nekompatibility podtypů
  • Test kontraktu pro základní třídu, spuštěný pro všechny podtřídy, odhaluje porušení LSP v CI
  • Sealed class v Kotlin snižuje rizika LSP díky uzavřené hierarchii známé kompilátoru

Vyvineme mobilní aplikaci na klíč

IT Sectr vytváří aplikace pro iOS a Android pro startupy a podniky od roku 2017. Poradíme vám a navrhneme nejlepší řešení.

Prodiskutovat projekt

Přečtěte si také