SOLID – część 3. Zasada Podstawienia Liskov

SOLID – mnemonik, który mówi jak pisać poprawnie programy w językach obiektowych. Podążanie za tymi zasadami znacznie poprawia czytelność i możliwość utrzymywania kodu. W tej części będzie opisana trzecia zasada: Zasada Podstawienia Liskov.

W momencie kiedy szukałeś informacji na temat Liskov Substitution Principle na pewno natknąłeś się na przykład z kwadratem i prostokątem. Autor opisywał kwadra, prostokąt, dziedziczenie, przeciążenie funkcji. I voilà, pole kwadrata jest źle obliczone – to jest zasada podstawienia liskov. Po przeczytaniu takiego artykułu mówiłem sobie: „Okej. No źle to jest zaimplementowane. Ale o co chodzi ogólnie z tym LSP.

Spójrzmy jak jest zdefiniowana Zasada Podstawienia Liskov:

Funkcje które używają wskaźników lub referencji do klas bazowych, muszą być w stanie używać również obiektów klas dziedziczących po klasach bazowych, bez dokładnej znajomości tych obiektów.

Dla mnie na początku była to magia. Pozwól, że przedstawię to w troszkę innej formie.

Wyobraźmy sobie, że do projektu z czołgiem przychodzi nowa osoba – Mietek. Mietek chce zaimplementować NieladujacaSieLufa.

public class NieladujacaSieLufa : Lufa
{
	public KrotkaLufa()
	{
		DlugoscLufy = 100;
		PociskJestZaladowany = false;
	}

	public override void LadujPocisk(ISiejacyZniszczenie pociskSiejacyZniszczenie)
	{

	}
}

Metoda LadujPocisk daje nam możliwość ładowania cały czas nowych pocisków. Co jest sprzeczne z ideą klasy bazowej Lufa. W klasie Lufa jest możliwość załadowania na raz tylko jednego pocisku. To klasa bazowa mówi jak klasy dziedziczące powinny się zachowywać. Klasy które dziedziczą po klasie bazowej muszą brać przykład z klasy bazowej i robić to samo co ich klasa bazowa (ale mogą na własny sposób). Jeżeli klasa bazowa za metodą LadujePocisk ma pewną idea działania, to klasy dziedziczące też muszą spełniać tą idee działania.

var lufa = new Lufa();
ISiejacyZniszczenie pocisk = new Pocisk();
lufa.LadujPocisk(pocisk);
ISiejacyZniszczenie kolejnyPocisk = new Pocisk();
lufa.LadujPocisk(kolejnyPocisk);

Ten kawałek kodu rzuci wyjątkiem: PociskJestJuzZaladowany. Co się stanie w przypadku kiedy chcemy użyć NieladujacaSieLufa?

var lufa = new NieladujacaSieLufa();
ISiejacyZniszczenie pocisk = new Pocisk();
lufa.LadujPocisk(pocisk);
ISiejacyZniszczenie kolejnyPocisk = new Pocisk();
lufa.LadujPocisk(kolejnyPocisk);

Nic się nie stanie. Podklasa NieladujacaSieLufa powinna wyrzucić wyjątek. NieladujacaSieLufa zmieniła idea działania lufy. Może to wywołać potem problemy:

var lufa = new NieladujacaSieLufa();
ISiejacyZniszczenie pocisk = new Pocisk();
lufa.LadujPocisk(pocisk);
lufa.Strzelaj();

Zostanie wyrzucony niesłusznie wyjątek PociskNieJestZaladowany. Pocisk powinien być załadowany.

Funkcje które używają wskaźników lub referencji do klas bazowych, muszą być w stanie używać również obiektów klas dziedziczących po klasach bazowych, bez dokładnej znajomości tych obiektów.

Co oznacza pogrubiony tekst? Oznacza on tyle, że widząc jak zachowuje się Lufa czyli klasa bazowa, to mam prawo założyć, że klasy które dziedziczą po Lufie mają to samo zachowanie. Po załadowaniu do Lufy pocisku normalne jest, że powinienem umieć teraz strzelić. Skoro NieladujacaSieLufa w teorii została załadowana to powinna wystrzelić. W tym momencie łamie LSP – klasa pochodna nie zachowuje się tak klasa bazowa.

Mietek postanowił, że napisze klasę PopsutaLufa.

public class PoputaLufa : Lufa
{
	public PoputaLufa()
	{
		DlugoscLufy = 50;
		PociskJestZaladowany = false;
	}

	public virtual void LadujPocisk(ISiejacyZniszczenie pociskSiejacyZniszczenie)
        {
            if (!PociskJestZaladowany)
            {
                PociskJestZaladowany = true;
                Pocisk = pociskSiejacyZniszczenie;
            }
            else
            {
                throw new ZaladowanoWiecejNizJedenPociskow();
            }
        }
}

public class ZaladowanoWiecejNizJedenPociskow : Exception
{

}

Zasada podstawienia liskov została znowu złamana przez Mietka. Klasa bazowa nie ma w sobie wyjątku ZaladowanoWiecejNizJedenPociskow. W klasach dziedziczących nie można wyrzucać nowych wyjątków, których nie wyrzuca klasa bazowa (chyba że dziedziczymy wyjątki).
Mietek może szybko naprawić swój błąd:

public class ZaladowanoWiecejNizJedenPociskow : PociskJestJuzZaladowany
{

}

Trzecia zasada SOLID dotyczy dziedziczenia. Dba o to, aby kod był spójny i nieskomplikowany. Gorąco polecam kliknkąć w linki poniżej. Uzupełniają znacznie wiedzę na temat LSP.

SOLID – Zasada Podstawienia Liskov. Linki.

CodeProject.com – klasyczny przykład
StackOverFlow – przykład
StackExchange – świetne rozwinięcie tematu. Polecam.
YouTube – dodatkowe aspekty LSP + więcej przykładów. Polecam.

Leave a Comment

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *