Liskov Substitution Principle

0

Staram się zrozumieć jedną z zasad SOLID, konkretnie zasadę podstawienia Liskov. W sieci znajduję bardzo dużo artykułów, niedających jednak jasnego wyjaśnienia. Odnoszę wrażenie, że ich autorzy sami niedokładnie rozumieją temat.

Coś więcej udało mi się znaleźć w dwóch miejscach.

Tu: http://www.codeproject.com/Articles/595160/Understand-Liskov-Substitution-Principle-LSP

Omówiony tutaj przykład z grubsza trzyma się kupy, chociaż wydaje mi się lekko nieżyciowy. Komentarze pod artykułem też mówią, że jest on nieprawidłowy: http://www.codeproject.com/Articles/595160/Understand-Liskov-Substitution-Principle-LSP?msg=4570854#xx4570854xx lub w szczególności: www.codeproject.com/Messages/4569903/This-is-wrong.aspx. Niestety to, co się dzieje dalej, jest już z pogranicza zrozumienia.

Inną propozycję wyjaśnienia znalazłem w książce "Rusz głową! Analiza i projektowanie obiektowe". Autorzy w tym miejscu proponują zastosowanie kompozycji zamiast dziedziczenia. Dla mnie ten przykład też jest dość niejednoznaczny.

Proszę o pomoc w zrozumieniu osoby, które znają tę zasadę i stosują ją w życiu codziennym. Idealne byłyby przykłady błędnego kodu ze wskazaniem, jakie zagrożenia to niesie oraz rozwiązania z użyciem zasady Liskov. Chociaż będę wdzięczny za każdą poradę :)

[Mirek]

4

Przykład z kwadratem i prostokątem jest głupi, bo to akurat przykład klas które w "zwykłym życiu" są powiązane a w programie raczej nie będą ;)

Zasada podstawiania jest dość prosta: ZAWSZE możesz podstawić obiekt pochodny w miejsce obiektu bazowego.
Innymi słowami: obiekt pochodny musi z logicznego punktu widzenia być szczególnym przypadkiem obiektu bazowego.

Kiedy łamiemy tą zasadę? Na przykład kiedy ludzie bawią się w dziedziczenie tylko po to żeby mieć dostęp do prywatnych składników pewnej klasy. Na przykład w klasie X masz pewną metodę która by ci sie przydała w klasie Y. Niektórych kusi żeby dziedziczyć w klasie Y po klasie X, mimo ze te klasy nie mają ze sobą nic wspólnego. Z punktu widzenia OOP w takiej sytuacji należałoby raczej wyciągnąć tą metodę do osobnej klasy a potem delegować wywołanie metody zarówno w X jak i Y. Można też w niektórych językach użyć traitów/mixinów. Niemniej na pewno nie należy dziedziczyć.

0
kchteam napisał(a)

lub w szczególności: www.codeproject.com/Messages/4569903/This-is-wrong.aspx

A Quadrilateral still has independent Width and Height properties. So, if they are independent, setting one property should never set the other property.

Non sequitur. Pierwszy raz słyszę, żeby poszczególne property absolutnie musiały być całkowicie niezależne.
Przykładowo, w kontrolce GUI Panel mamy property Width, Height i Dock. Ta ostatnia pozwala np. na automatyczne wypełnienie kontrolką całego obszaru okna. Siłą rzeczy zadokowanie kontrolki powoduje zmianę jej wymiarów.

W kontekście dziedziczenia, spokojnie możemy sobie wyobrazić podstawowy Panel bez funkcjonalności dokowania, i bardziej wypasiony DockingPanel z dodaną właściwością Dock.
Inne klasy nie muszą wiedzieć czy operują na Panel czy na DockingPanel.
A może być też SquarePanel, wymuszający swoją kwadratowość.

2

Chodzi o możliwość podmiany klasy bazowej przez dziedziczoną.

Jesli piszesz funkcje:

func test(rectangle rect) {
    rect.width = 10;
    rect.height = 20;
    assert(rect.area() == 200);
    bar(rect);
}

To raczej nie spodziewasz się nigdy że asercja rzuci wyjątkiem pod tytułem "pole jest równe 400 zamiast 200".

A taki efekt dałoby napisanie kwadratu gdzie zmiana jednego pola zmienia oba.

A może być też SquarePanel, wymuszający swoją kwadratowość.

Może być, ale szczerze mówiąc nie widzę zastosowania ;). I jeśli dziedziczyłby z Panelu to łamałby LSP i powodował WTFy.

0
kchteam napisał(a):

Staram się zrozumieć jedną z zasad SOLID, konkretnie zasadę podstawienia Liskov. W sieci znajduję bardzo dużo artykułów, niedających jednak jasnego wyjaśnienia. Odnoszę wrażenie, że ich autorzy sami niedokładnie rozumieją temat.

To masz tutaj wyjaśnienie:
http://www.pzielinski.com/?p=423

3

Powody łamania Liskov mogą być różne. Żeby jednak nie dyskutować o patologiach, to może uzupełnię o objawy złamania zasady.

Jeśli w ciele metody która przyjmuje typ nadrzędny (bardziej ogólny), sprawdzasz czy obiekt będący argumentem jest typem podrzędnym, łamiesz LSP (ang. Liskov substitution priciple) (to w nawiązaniu do słusznej odpowiedzi @Shalom)
Jeśli w klasie dziecka, dziedziczącej z rodzica, stosujesz metodę obecną w rodzicu która jest bardziej restrykcyjna co do typów przyjmowanych niż w klasie rodzica (przyjmuje typ bardziej szczegółowy w dół drzewa dziedziczenia) i/lub metoda ta zwraca typ mniej szczegółowy (w górę drzewa dziedziczenia), to łamiesz także regułę Liskov.

Drugi objaw nawiązuje bezpośrednio do DbC (ang. Design by Contract). Co ważne chodzi o złamanie kontraktu interfejsu klasy a nie dodatkową polimorficzną metodę.

4

O ile dobrze rozumiem tę zasadę, można ją najprościej zaprezentować w następujący sposób:

Jeżeli nadpisujemy metodę klasy, po której dziedziczymy, to nie możemy zmienić jej zachowania.
Możemy jedynie rozszerzyć funkcjonalność tej metody.

Przedstawia to poniższy kod w Javie:

public class CoffeeMachine {
   protected void makeCoffee() {
      System.out.println("preparing coffee...");
   }
}

public class SweetCoffeeMachine extends CoffeeMachine {
   @Override
   protected void makeCoffee() {
      super.makeCoffee(); // jeżeli usuniemy tę linię, zasada Liskov zostanie złamana
      addSugar();
   }

   private void addSugar() {
      System.out.println("adding sugar...");
   }
}
0

Praktyczne znaczenie Liskov jest takie, że podpięcie klasy która dziedziczy po rodzicu nie będzie powodowało konieczności zmian w już istniejącym kodzie, inaczej może być wiele problemów. I tak np. w miejsce standardowego buttona mogę sobie podpiąć jakiś inny ładniej wyglądający ale i tak ma spełniać swoją funkcję. Będzie nadal buttonem i o to mi chodzi. Oczywiście zawsze można podpiąć ten stary jak się nie podoba i ma działać. Przydało by się to zobrazować na przykładzie, żeby pokazać praktyczne znaczenie.

3

Jeżeli nadpisujemy metodę klasy, po której dziedziczymy, to nie możemy zmienić jej zachowania.
Możemy jedynie rozszerzyć funkcjonalność tej metody.

Nie to mówi Liskov.
Zacytuję z Wikipedii:

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.

Tyle, i tylko tyle. Nie ma tu zakazu kwadratów które zmieniają wysokość wraz z szerokością, nie ma tu zakazu zmiany zachowania. Jest zasada że "Funkcje [...] muszą być w stanie używać obiektów bez dokładnej znajomości [klas] tych obiektów."

Zasada dotyczy więc przede wszystkim tych funkcji, a w drugiej kolejności klas - które powinny być zaprojektowane tak, by dało się ich używać zgodnie z zasadą.

Co zasada znaczy w praktyce jest dużo zależne od danego programu i jego hierarchii klas.

Są inne zasady programowania o szerszym znaczeniu niż zasada Liskov, ale póki mówimy o Liskov to sprowadza się do zakazu zadawania pytania "jakiej klasy jest obiekt" (patrz wątki na forum "jak sprawdzić jakiego typu jest zmienna").

Klasa dziedzicząca może zmieniać swoje zachowanie, pod warunkiem że nie ma to wpływu na kod który klasy używa - a nawet jeśli ma wpływ, to można poprawić kod używający obiektu by przywrócić zachowanie zasady podstawienia.

1

Tu ciekawy artykuł w temacie znalazłem: http://alblue.bandlem.com/2004/07/java-liskov-substution-principle-does.html
Niby tylko o Javie, ale tak naprawdę dotyczy też innych języków. LSP nie zawsze jest czymś, co powinno się zachowywać za wszelką cenę.

3
Krolik napisał(a):

.. LSP nie zawsze jest czymś, co powinno się zachowywać za wszelką cenę.

Ba..., oczywiście. Całe S.O.L.I.D. czy GRASP związane jest z przygotowaniem do zmiany kodu wokół "osi zmian". Po pierwsze czasem wiesz że wg. jakiegoś aspektu kod nie będzie zmieniany, a po drugie niestety nigdy nie będziesz w stanie w bardziej złożonym kodzie przygotować się na wszystkie osie zmian. Bo to i nie jest opłacalne i nie jest potrzebne :-)
Jak w większości przypadków reguł. S.O.L.I.D. należy znać, rozumieć i stosować ale zawsze w konkretnym przypadku w granicy zdrowego rozsądku.

0

Super!

Bardzo fajnie naświetliliście mi temat. W podziękowaniu za Wasz wkład - łapki w górę ;)

[Mirek]

1 użytkowników online, w tym zalogowanych: 0, gości: 1