Zasada podstawienia Liskov

0

Na wikipedii piszą:

jeśli będziemy tworzyć egzemplarz klasy potomnej, to niezależnie od tego, co znajdzie się we wskaźniku na zmienną, wywoływanie metody, którą pierwotnie zdefiniowano w klasie bazowej, powinno dać te same rezultaty.

Na helionie też coś podobnego piszą:
https://helion.pl/blog/mnemonik-solid-l-jak-liskov-substitution-principle-55

  • Mamy klasę A z metodą MyMethod, która zwraca wartość Z.
  • Tworzymy klasę B, która dziedziczy z A.
  • Niezależnie od sposobu utworzenia klasy A oraz B:
  • A a = new A()
  • A b1 = new B()
  • B b2 = new B()
    Musi zajść równość: a.MyMethod() == b1.MyMethod() == b2.MyMethod()
    W innym przypadku dochodzi do pogwałcenia zasady Liskov Substition Principle.

Ja stworzyłem takie klasy

class Employee
{
    public virtual float GetAverageSalary()
    {
        return 3000;
    }
}

class Programmer : Employee
{
    public override float GetAverageSalary()
    {
        return 8000;
    }

    public List<string> GetKnownProgrammingLanguages()
    {
        return new List<string> { "C#", "Java", "Python" };
    }
}

Employee employee = new Employee();
var averageEmployeeSalary = employee.GetAverageSalary(); // 3000

Programmer programmer = new Programmer();
var averageProgrammerSalary = programmer.GetAverageSalary(); // 8000

To to jest niby złamanie zasady Liskov????? Bo przecież metoda GetAverageSalary() zwróci inny wynik dla klasy pracownika i programisty. To chyba przesada, że ja tu łamię zasadę Liskov??

5

Co? Nie no jakaś bzdura, to po co w ogóle dziedziczyć i podstawiać klasę pochodną skoro ma się zachowywać dokładnie tak samo jak klasa nadrzędna. Rozumiem że różnić się może jedynie skutkami ubocznymi dodając na przykład logowanie i tyle? Autor poleciał i wygląda że sam nie rozumie tematu - tylko angielski cytat i przykład z kwadratem i prostokątem ma sens.
Z tym że autor twierdzi że naruszeniem jest to że wynik jest różny, a naruszenie wynika z tego że "Postconditions cannot be weakened in the subtype" - w tym przypadku area != width * height.
Ogólnie chodzi o to żeby można było stworzyć klasę pochodną i podstawić ją w dowolnym miejscu a kod nadal powinien działać poprawnie i nie być zależnym od konkretnej implementacji.
Zaglądaj lepiej do anglojęzycznych źródeł i wikipedii

0

Poszukałem trochę i jest w sumie tak jak @obscurity pisze. Tu na stackoverflow jest podobne pytanie:
https://stackoverflow.com/questions/28159268/is-the-liskov-principle-violated-if-overridden-methods-return-a-different-value

I też piszą, że to nienarusza Liskov. Więc ten gość, którego artykuł jest na helion pisze nieprawdę.

Druga kwestia, że na polskiej wikipedii jest zdanie, które także wprowadza w błąd:
https://pl.wikipedia.org/wiki/Zasada_podstawienia_Liskov

jeśli będziemy tworzyć egzemplarz klasy potomnej, to niezależnie od tego, co znajdzie się we wskaźniku na zmienną, wywoływanie metody, którą pierwotnie zdefiniowano w klasie bazowej, powinno dać te same rezultaty

bo na angielskiej wikipedii nie widzę podobnego sformułowania:
https://en.wikipedia.org/wiki/Liskov_substitution_principle

1
Aleksander Wercis napisał(a):

ten gość: Jerzy Piechowiak, którego artykuł jest na helion pisze nieprawdę.

Druga kwestia, że na polskiej wikipedii jest zdanie, które także wprowadza w błąd:
https://pl.wikipedia.org/wiki/Zasada_podstawienia_Liskov

Nic dziwnego, bo według historii ktoś dodał ten fragment na wikipedię na podstawie wypocin na helionie

screenshot-20240417081942.png
Brawo że ktoś po 5 latach się zorientował, ciekawe ile osób się z tego zdążyło nauczyć dobrych praktyk...

6

Jestem między kawą a zapiekanką więc postaram się być zwięzły:

  • Metoda w klasie bazowe definiuje pewien kontrakt, jawny (np. udokumentowany w komentarzu lub dokumentacji) lub niejawny
  • Metoda w klasie pochodnej musi spełniać ten (nie)jawny kontrakt.

W tym wypadku GetSalary zwraca pensję pracownika, i to jest kontrakt w klasie bazowej. To jest ma być zwrócone coś co mozną uznać za pensję (a więc nie zero i nie liczba ujemna) oraz nie wolno rzucić wyjątkiem.

Metoda w klasie pochodnej która zwraca 8000 a nie 3000 przestrzega tego kontraktu.

Liskow można złamać np. tak:

  • Metoda w klasie pochodnej rzuca wyjątek który nie dziedziczy po żadnym wyjątku rzucanym przez metodę w klasie bazowej
  • Metoda w klasie pochodnej rzuca wyjątek gdy metoda w klasie bazowe zwraca zwykły wynik
  • Metoda w klasie pochodnej zmiena stan gdy metoda w klasie bazowej gwarantuje że stan się nie zmieni.

Myśląc o tym w kategorii kontraktów, pre i post conditions łatwo dojść o co chodzi z tą Liskow.

PS. Dla własnego dobra przestałbym już czytać o programowaniu po polsku, czasy gdy ludzie pisali coś konkretnego po polsku o kodowaniu już dawno mineły. Albo Eng albo chatGPT...

0

Wujek Bob podawał przykład z kwadratem i prostokątem, ale nie jako przykład zasady podstawienia, tylko jako przykład dziedziczenia. Wniosek był taki, że nie należy dziedziczyć kwadratu po prostokącie (kwadrat szczególny przykład prostokąta) ani prostokąta po kwadracie (jako rozszeżenie funkcjonalności), ponieważ kod jest reprezentacją tych figur a nie jest figurami; w kodzie (reprezentacji) zależności między tymi figurami nie występują.
Przynajmninej jakoś tak to pamiętam.

0
0xmarcin napisał(a):

Jestem między kawą a zapiekanką więc postaram się być zwięzły:

  • Metoda w klasie bazowe definiuje pewien kontrakt, jawny (np. udokumentowany w komentarzu lub dokumentacji) lub niejawny
  • Metoda w klasie pochodnej musi spełniać ten (nie)jawny kontrakt.

W tym wypadku GetSalary zwraca pensję pracownika, i to jest kontrakt w klasie bazowej. To jest ma być zwrócone coś co mozną uznać za pensję (a więc nie zero i nie liczba ujemna) oraz nie wolno rzucić wyjątkiem.

Metoda w klasie pochodnej która zwraca 8000 a nie 3000 przestrzega tego kontraktu.

Liskow można złamać np. tak:

  • Metoda w klasie pochodnej rzuca wyjątek który nie dziedziczy po żadnym wyjątku rzucanym przez metodę w klasie bazowej
  • Metoda w klasie pochodnej rzuca wyjątek gdy metoda w klasie bazowe zwraca zwykły wynik
  • Metoda w klasie pochodnej zmiena stan gdy metoda w klasie bazowej gwarantuje że stan się nie zmieni.

Myśląc o tym w kategorii kontraktów, pre i post conditions łatwo dojść o co chodzi z tą Liskow.

PS. Dla własnego dobra przestałbym już czytać o programowaniu po polsku, czasy gdy ludzie pisali coś konkretnego po polsku o kodowaniu już dawno mineły. Albo Eng albo chatGPT...

Również łamie gdy metoda w klasie bazowej ma np. checka if (value < 100) return ok, a w pochodnej jest if (value < 110) return ok, więc metoda w klasie dziedziczącej zmienia tzw. invariants

1

@Aleksander Wercis @obscurity : zacznijmy od podstaw - zasada podstawienia Liskovej jest tutaj złamana, ale problemem tutaj nie jest łamanie tej zasady tylko pokracznie zaprojektowana klasa (konkretnie GetAverageSalary()).

W tym przypadku:

  • Employee zarabia trzy tysiące
  • Programmer zarabia osiem tysięcy
  • ergo Programmer nie jest Employee, chociaż z drzewka wynika, że jest

Albo trzeba wydzielić abstrakcję do oddzielnej klasy, i dorzucić dwie klasy dziedziczące, tj.

abstract class Employee
{
  public abstract int GetAverageSalary();
}

class Programmer : Employee
{
  public override int GetAverageSalary()
  {
    return 8000;
  }
}

class AverageEmployee : Employee
{
  public override int GetAverageSalary()
  {
    return 3000;
  }
}

Wtedy nie można liczyć, że GetAverageSalary() zwróci 3000 dla każdego pracownika, ponieważ sam kontrakt Employee.GetAverageSalary() jest nieokreślony - wiadomo jedynie, że zwraca int.

Alternatywnie można średnią wypłatę wstrzykiwać przez konstruktor.

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