Liskov Substitution Principle – LSP

Liskov Substitution Principle – LSP

Teraz porozmawiamy o zasadzie podstawiania Liskov. Zasada podstawiania Liskov lub LSP, to trzecia z zasad SOLID która mówi, że:

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.

W miejsce typu bazowego możesz podstawić dowolny typ klasy pochodnej i nie powinieneś utracić poprawnego działania. Czyli korzystanie z funkcji klasy bazowej musi być możliwe również w przypadku podstawienia instancji klas pochodnych. Mówiąc w skrócie klasa dziedzicząca po klasie bazowej nie powinna zmieniać jej funkcjonalności tylko rozszerzać możliwości bez wpływu na jej aktualne działanie. Autorem tej zasady jest Barbara Liskov.

Zasada ta może wydawać się troszkę skomplikowana, jednakże w prostych słowach mówi ona nam o tym, że każda funkcja w naszym programie powinna działać w sposób przewidywalny, bez względu na to czy jako parametr przekażemy jej klasę bazową czy klasę dziedziczącą.

Generalnie mówiąc na przykład klasa Fiat nie koniecznie powinna dziedziczyć po klasie Ferrari a raczej powinny dziedziczyć po klasie Samochód, która jest bardziej ogólna dla tego typu obiektów. Dlaczego?

Wyobraźmy sobie taką sytuację, że jazda na przykład Fiatem jest generowane na podstawie parametrów silnika w taki sam sposób jak na przykład dla Ferrari, jednak w każdym z tych samochodów silnik jest inny.

Zobaczmy to na antyprzykładzie

abstract class Vehicle
{
   public string Name { get; set; }
   public abstract void Drive();
}

class Fiat : Vehicle
{
   public override void Drive()
   {
      Console.WriteLine("Fiat jedzie");
   }
}

class Ship : Vehicle
{
   public override void Drive()
   {
      throw new NotImplementedException();
   }
}

Nasza klasa bazowa Vehicle (pojazd) posiada metodę Drive (jeździć). Klasa Fiat jak również klasa Ship (statek) dziedziczą po naszej klasie bazowej pojazd czyli dziedziczą metodę Drive (jeździć).

Jednak statek nie może jeździć, statek pływa. Dlatego w tym przypadku jest to złamanie zasady Liskov i nie możemy wykonać tego dziedziczenia w poprawny sposób.

Nasz statek nie będzie jeździł po ulicach, będzie raczej pływał po morzach. Nasze dziedziczenie musimy zaplanować w taki sposób aby klasa pochodna mogła wykorzystać wszystkie metody klasy bazowej, które implementuje.

Zobaczmy lepszy przykład:

abstract class Vehicle
{
   public string Name { get; set; }
   public abstract void Drive();
}
class Fiat : Vehicle
{
   public override void Drive()
   {
      Console.WriteLine("Fiat jedzie");
   }
}

class Ferrari : Vehicle
{
   public override void Drive()
   {
      Console.WriteLine("Ferrari jedzie");
   }
}
Zwróćcie uwagę na powyższy przykład, gdzie udało nam się zachować zasadę LSP. W przypadku obiektów klasy pochodnej możemy je użyć w miejscu klasy bazowej. 
class Program
{
   static void Main(string[] args)
   {
      Vehicle _vehicle;

      _vehicle = new Fiat();
      _vehicle.Drive();

      _vehicle = new Ferrari();
      _vehicle.Drive();

      Console.ReadLine();
   }
}

W celu zobrazowania działania zasady podstawiania Liskov, posłużyłem się prostym przykładem. Posiadamy dwie klasy, w których znajdujemy wiele wspólnych składowych. Wspólnymi składowymi mogą być: właściwości, metody itd.

Co w takiej sytuacji należało zrobić?

Zgodnie z zasadą podstawiania Liskov, wypadałoby wyodrębnić wspólne elementy obu klas do jednej klasy – najlepiej abstrakcyjnej.

Cały czas projektując aplikację tworzymy hierarchie klas. Następnie rozszerzamy niektóre klasy tworząc klasy pochodne. Musimy upewnić się, że nowe klasy pochodne po prostu rozszerzają się bez zastępowania funkcjonalności starych klas. W przeciwnym razie nowe klasy mogą wywołać niepożądane efekty, gdy zostaną użyte w istniejących modułach programu.

Zasada podstawiania Likova stwierdza, że jeśli moduł programu używa klasy bazowej, to odwołanie do klasy bazowej można zastąpić klasą pochodną bez wpływu na funkcjonalność modułu programu. Typy pochodne muszą być całkowicie substytucyjne dla swoich typów podstawowych.

Najpopularniejszym i książkowym przykładem na złamanie tej zasady
jest dziedziczenie klasą kwadrat po klasie prostokąt.

Przykład

Poniżej znajduje się klasyczny przykład, w którym naruszona jest zasada podstawiania Likov. W przykładzie zastosowano 2 klasy: Prostokąt i Kwadrat. Załóżmy, że gdzieś w aplikacji jest używany obiekt Prostokąt. Następnie rozszerzamy aplikację i dodajemy klasę Kwadrat.

Klasa kwadrat jest zwracana przez wzorzec fabryki oparty na pewnych warunkach i nie wiemy dokładnie, jaki typ obiektu zostanie zwrócony. Ale wiemy, że to prostokąt. Otrzymujemy obiekt prostokąt, ustawiamy szerokość na 4 i wysokość na 8 i uzyskujemy obszar. W przypadku prostokąta o szerokości 4 i wysokości 8 obszar powinien wynosić 32. Zamiast tego wynikiem będzie 64

Naruszenie zasady zastępowania Likov
class Rectangle
{
   public virtual int Width { get; set; }
   public virtual int Height { get; set; } 
   public int getArea() => Width * Height;
}
class Square : Rectangle
{
   public override int Width
   {
      set { base.Width = base.Height = value; }
   }

   public override int Height  
   {
      set { base.Width = base.Height = value; }
   }
}  
class Program
{
   private static Rectangle GetNewRectangle()
   {
      // może to być obiekt zwrócony przez jakąś fabrykę ...
      return new Square();
   }

   public static void Main(String[] args)
   {
      Rectangle rec = GetNewRectangle();
      rec.Width =  4;
      rec.Height = 8;
      // użytkownik wie, że r to prostokąt.
      // Zakłada, że jest w stanie ustawić szerokość i wysokość 
      // tak jak dla klasy bazowej

      Console.WriteLine(rec.getArea());
      // A tu niespodzianka,
      // teraz jest zaskoczony, że obszar wynosi 64 zamiast 32.
   }
}

Wniosek:

Ta zasada jest tylko rozszerzeniem zasady otwarte – zamknięte
i oznacza, że musimy upewnić się,
że nowe klasy pochodne rozszerzają klasy bazowe
bez zmiany ich zachowania.

2 comments

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *