- Úvod, definice
- Porušení principu LSP - nutná znalost podtříd
- Porušení principu LSP - nezaměnitelnost podtříd
- Podmínky typu předpoklady, následné podmínky a invarianty
- Kovariance a kontravariance
- Další zdroje a ukázky
Úvod, definice
Liskovové princip zaměnitelnosti (Liskov Substitution Principle - LSP) je třetím základním principem objektové návrhové metodologie SOLID. Je definován takto:
Funkce (metody), které používají ukazatele nebo reference na bázovou třídu, musí být schopny použít objekty jejich podtříd bez jejich znalostí.
Tento princip nám klade zásadní podmínky na vlastnosti bázových tříd a jejich podtříd (potomků). Definuje, za jakých podmínek je možné vytvářet podtřídy bázových tříd a využívat výhody plynoucí z dědičnosti. Při porušení principu LSP se dostáváme do problémů, jak bude ukázáno v dalším textu.
Porušení principu LSP - nutná znalost podtříd
Navrhujeme reprezentaci základních geometrických tvarů. Bázovou třídou je třída Tvar, jejími potomky jsou třídy Ctverec a Kruznice. Metoda VykreslitTvar() zajišťuje vykreslení tvarů různého typu:
class Tvar { }
class Ctverec : Tvar { }
class Kruznice : Tvar { }
...
void VykreslitTvar(Tvar tvar)
{
if (tvar is Ctverec)
{
VykreslitCtverec(tvar as Ctverec);
}
else if (tvar is Kruznice)
{
VykreslitKruznici(tvar as Kruznice);
}
}
Takto definová metoda VykreslitTvar() porušuje LSP, neboť není splněna druhá část definice. Metodě nestačí znalost pouze bázové třídy, ale musí znát i její potomky. Musí vědět, že v systému existují třídy Ctverec a Kruznice a musí je umět přetypovat. Metoda zároveň porušuje princip otevřenosti a uzavřenosti OCP. Pokud vznikne další potomek třídy Tvar například Trojuhelnik, je nutné metodu otevřít a doplnit znalost nové podtřídy.
Porušení principu LSP - nezaměnitelnost podtříd
Navrhujeme reprezentaci základních geometrických tvarů. Vznikne bázová třída Tvar a její podtřída pro obdélníky Obdelnik. U obdélníku evidujeme rozměry pro výšku a šířku, které zpřístupníme přes veřejné settery:
class Tvar { }
class Obdelnik : Tvar
{
private double sirka;
private double vyska;
public double Sirka
{
get { return sirka; }
set { sirka = value; }
}
public double Vyska
{
get { return vyska; }
set { vyska = value; }
}
}
Požadujeme také třídu Ctverec. Vyjdeme z předpokladu, že čtverec je speciální případ obdélníka a třídu Ctverec nadefinujeme jako podtřídu třídy Obdelnik. Musíme zajistit, aby vždy platila rovnost rozměrů šířky a výšky čtverce. Nabízí se využít settery pro Vysku a Sirku a přidat nastavení i druhé vlastnosti. Dostáváme definici třídy Ctverec:
class Ctverec : Obdelnik
{
public double Sirka
{
set
{
base.Sirka = value;
base.Vyska = value;
}
}
public double Vyska
{
set
{
base.Vyska = value;
base.Sirka = value;
}
}
}
Na první pohled vypadají naše třídy v pořádku a dostavuje se pocit uspokojení. Zkušenější návrhář však začíná pochybovat a hledá na řešení problémová místa.
Toto přiřazení ještě dopadne podle očekávání:
Ctverec ctverec = new Ctverec();
ctverec.Vyska = 1; // Vyska == 1, Sirka == 1
... ovšem, pokud logiku nastavení vlastnosti Vyska umístíme do třídy, která předpokládá na vstupu obecnější třídu Obdelnik, dostáváme "neočekávané" chování:
void F(Obdelnik obdelnik)
{
obdelnik.Vyska = 1;
}
Obdelnik obdelnik = new Obdelnik();
F(obdelnik); // Vyska == 1, Sirka == 0, ok
Ctverec ctverec = new Ctverec();
F(ctverec); // Vyska == 1, Sirka == 0 (předpokládali jsme, že bude 1)
K čemu tady došlo? Proč je šířka čtverce 0, přestože v setteru Ctverec.Vyska je i nastavení šířky na stejnou hodnotu? Metoda F() předpokládá na vstupu objekt typu Obdelnik a volání setteru vlastnosti Vyska se tedy omezí na úroveň této třídy. Setter Ctverec.Vyska se vůbec nevyvolá.
V C# je v tomto případě možné použít modifikátor override
, kterým označíme metodu nebo vlastnost podtřídy jako přepsanou.
Přepisovat můžeme pouze metody a vlastnosti, které jsou v bázové třídě označené jako virtuální (virtual
),
abstraktní (abstract
) nebo přepsané (override
). Přepsané metody nebo vlastnosti budou prováděny až na úroveň
příslušné podtřídy i přesto, že se pracuje s bázovou třídou. V našem případě přepíšeme definice tříd Obdelnik a Ctverec takto:
class Obdelnik : Tvar
{
private double sirka;
private double vyska;
public virtual double Sirka
{
get { return sirka; }
set { sirka = value; }
}
public virtual double Vyska
{
get { return vyska; }
set { vyska = value; }
}
}
class Ctverec : Obdelnik
{
public override double Sirka
{
set
{
base.Sirka = value;
base.Vyska = value;
}
}
public override double Vyska
{
set
{
base.Vyska = value;
base.Sirka = value;
}
}
}
... a metoda F() se bude chovat očekávaným způsobem i pro čtverce:
Ctverec ctverec = new Ctverec();
F(ctverec); // Vyska == 1, Sirka == 1, ok
Pokud si v tento okamžik pomyslíte, že veškeré problémy s návrhem jsou vyřešeny, zkuste si napsat třeba tuto metodu:
void G(Obdelnik obdelnik)
{
obdelnik.Sirka = 4;
obdelnik.Vyska = 5;
if (obdelnik.Sirka * obdelnik.Vyska != 20)
{
// Chyba
}
}
Pokud je na vstupu objekt typu Ctverec, nastane chyba. Přiřazení obdelnik.Vyska = 5 totiž způsobí (pro autora metody G()) nechtěné nastavení šířky čtverce na 5 a podmínka nebude splněna (5 * 5 == 25). Autor metody G() omezil správně funkcionalitu pouze na objekty typu obdélník. To že nadefinujeme podtřídu Ctverec jako potomka bázové třídy Obdelnik a využijeme tuto metodu, nemohl předpokládat. Při chybném návrhu třídy Ctverec jsme porušili Liskovové princip zaměnitelnosti.
Tento příklad by Vás měl varovat. Vždy pečlivě zvažte, zda-li vztah mezi dvěma třídami opravdu splňuje podmínky kladené na dědičnost. Problémy se mohou projevit později, kdy je pracné chybný návrh přepracovat.
Podmínky typu předpoklady, následné podmínky a invarianty
Omezující podmínky jsou základními stavebními kameny v návrhové metodologii nazvané Návrh řízený podmínkami (dohodami) (Design by Contract - DbC). Většina vyšších programovacích jazyků obsahuje syntaktické konstrukty pro zajištění DbC. Např. v .Net Fx 4.0 jsou k dispozici třídy ve jmenném prostoru System.Diagnostics.Contracts.
Podmínky mohou být trojího typu:
- Předpoklady (Preconditions) - podmínky, které musí platit před provedením kódu metody nebo vlastnosti.
- Následné podmínky (Postconditions) - podmínky, které musí platit po provedení kódu metody nebo vlastnosti.
- Invarianty (Invariants) - podmínky, které musí platit po celou dobu existence objektu.
Princip LSP definuje požadavky na chování, které musí podtřídy splňovat:
- Předpoklady nesmí být zesilovány v podtřídách.
- Následné podmínky nesmí být oslabovány v podtřídách.
- Invarianty nadřízené třídy musí být zachovány v podtřídách.
Kovariance a kontravariance
Pro pochopení vzájemné zaměnitelnosti objektových typů, které jsou v hierarchii dědičnosti, je nutná znalost pojmů kovariance a kontravariance.
Kovariance (covariance) vyjadřuje možnost do návratové proměnné typu bázové třídy vložit objekt libovolné podtřídy. Třída DemoKovarinace má metody Funkce1() a Funkce2(), které pro delegáta funkce s návratovým typem Obdelnik splňují podmínku kovariance. Funkce2() vrací objekt typu Ctverec, který je podtřídou požadované třídy Obdelnik.
class DemoKovariance
{
public delegate Obdelnik VratitObdelnik();
public static Obdelnik Funkce1()
{
return new Obdelnik();
}
public static Ctverec Funkce2()
{
return new Ctverec();
}
static void Main()
{
VratitObdelnik funkce1 = Funkce1;
Obdelnik obdelnik1 = funkce1();
VratitObdelnik funkce2 = Funkce2;
Obdelnik obdelnik2 = funkce2();
}
}
Kontravariance (contravariance) vyjadřuje možnost objektovou hodnotu předávanou jako parametr vložit do objektového typu bázové třídy (nadtřídy). Jak ukazuje příklad, delegát funkce PredatCtverec() má v signatuře parametr typu Ctverec. Funkce Funkce2() má na vstupu parametr typu Obdelnik, který je bázovou třídou třídy Ctverec. Kontravariance je tedy splněna, program bude fungovat korektně.
class DemoKontravariance
{
public delegate void PredatCtverec(Ctverec ctverec);
public static void Funkce1(Ctverec ctverec)
{
}
public static void Funkce2(Obdelnik obdelnik)
{
}
static void Main()
{
PredatCtverec funkce1 = Funkce1;
funkce1(new Ctverec());
PredatCtverec funkce2 = Funkce2;
funkce2(new Ctverec());
}
}
Princip LSP definuje požadavky na metody podtříd:
- Kontravariance argumentů metod v podtřídách.
- Kovariance návratových typů metod v podtřídách.
- Podtřídy nemohou vyvolat jiné výjimky než výjimky použité v bázové třídě nebo podtřídy těchto výjimek.
Další zdroje a ukázky
Celý článek bohužel ukazuje na nepochonení nejen Liskovové, ale i Martina - obdélník NENÍ A NEMŮŽE být bázovou třídou pro čtverec, o tom je celý slavný rectangle - square příklad i motivace!!!
OdpovědětVymazatA kontravariance a kovariance u Liskovove se specificky tykaji dedicnosti, uvedene priklady jsou zcela irevevantni. Vetsina jazyku neumoznuje kontravarianci parametru metod v potomcich (viz Wiki, i tam to je a to Wiki je zdroj vetsinou k nicemu).
Vymazat