„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.”

Wprowadzenie

Trzecia zasada SOLID, to zasada podstawienia Liskov, która została sformułowana przez Barbarę Liskov. Zgodnie z zasadą LSP kod powinien być tak napisany, że w miejscu klasy bazowej będziemy mogli użyć klasy pochodnej, bez wprowadzenia zakłóceń w programie.

Poniżej znajdują się przykłady, które będą pomocne w zrozumieniu trzeciej zasady SOLID.

Prostokąt/kwadrat

Mamy klasę bazową Rectangle oraz klasę Square, która dziedziczy klasę Rectangle. Z zajęć matematyki wszyscy wiemy, że każdy kwadrat to prostokąt. Prostokąt i kwadrat mają te same właściwości: wysokość i szerokość. W przypadku kwadratu szerokość i wysokość jest taka sama.

class Square extends Rectangle
{
    public function setWidth(int $width): void { 
        $this->width = $width;
        $this->height = $width;
    }
 
    public function setHeight(int $height): void {
        $this->width = $height;
        $this->height = $height;
    }
}

Załóżmy, ze mamy metodę, która oblicza pole prostokąta.

public function testCalculateArea()
{
    $shape = new Rectangle();
    $shape->setWidth(10);
    $shape->setHeight(2);
 
    $this->assertEquals($shape->calculateArea(), 20);
 
    $shape->setWidth(5);
    $this->assertEquals($shape->calculateArea(), 10);
}

Zgodnie z trzecią zasadą SOLID- podstawienia Liskova – powinniśmy móc zastąpić klasę Rectangle klasą Square. Jeśli to zrobimy, okaże się, że test nie przechodzi. Zastąpienie metody setWidth() i setHeight() złamało zasadę. Nie powinniśmy zmieniać sposobu działania metod klasy nadrzędnej.

Jak rozwiązać ten problem?

Klasa Square nie powinna dziedziczyć po klasie Rectangle. Powinniśmy zaimplementować dla nich wspólny interfejs.

<?php

class Rectangle implements CalculableArea
{
    protected int $width;
    protected int $height;

    public function __construct(int $width, int $height)
    {
        $this->width = $width;
        $this->height = $height;
    }

    public function calculateArea(): int
    {
        return $this->width * $this->height;
    }
}

class Square implements CalculableArea
{
    protected int $edge;

    public function __construct(int $edge)
    {
        $this->edge = $edge;
    }

    public function calculateArea(): int
    {
        return $this->edge ** 2;
    }
}

interface CalculableArea
{
    public function calculateArea();
}

class RectangleTest extends TestCase
{
    public function testCalculateArea()
    {
        $shape = new Rectangle(10, 2);
        $this->assertEquals($shape->calculateArea(), 20);

        $shape = new Rectangle(5, 2);
        $this->assertEquals($shape->calculateArea(), 10);
    }
}


class SquareTest extends TestCase
{
    public function testCalculateArea()
    {
        $shape = new Square(10);
        $this->assertEquals($shape->calculateArea(), 100);

        $shape = new Square(5);
        $this->assertEquals($shape->calculateArea(), 25);
    }
}
Ptaki/pingwiny

Mamy klasę abstrakcyjną Bird oraz metodę Fly(). Jest to oczywiste dlaczego metoda Fly() znajduje się w klasie Bird – ptaki potrafią latać. Mamy klasę abstrakcyjną Dove – gołębie – która dziedziczy po klasie bazowej Bird. Dodajemy kolejną klasę abstrakcyjną Penguin. Ta klasa również będzie klasą pochodną po klasie Bird. Tutaj pojawia się problem. Pingwiny nie potrafią latać, więc bez sensu jest implementowanie metody Fly() w klasie Penguin. Nastąpiło tutaj złamanie zasady podstawienia Liskov.

Przykład łamiący zasadę podstawienia Liskov:

public abstract class Bird
{
    public void Fly() { }       
}
     
public class Dove : Bird
{
    public void Fly() 
    { 
        // tresc metody w klasie gołąb
    }       
}
 
public class Penguin : Bird
{
    public void Fly() 
    { 
        // tresc metody w klasie pingwin
    }       
}
Jak rozwiązać ten problem?

Powinniśmy stworzyć dwie klasy abstrakcyjne: FlyingBird oraz WalkingBird. Klasa FlyingBird będzie zawierała metodę Fly(), a klasa WalkingBird metodę Walk(). Będą to klasy bazowe.

Klasa Dove będzie dziedziczyła po klasie FlyingBird, natomiast klasa Penguin będzie dziedziczyła po klasie WalkingBird.

Przykład z uwzględnieniem zasady podstawienia Liskov
public abstract class FlyingBird
{
    public void Fly() { }       
}
 
public abstract class WalkingBird
{
    public void Walk() { }      
}
     
public class Dove : FlyingBird
{
    public void Fly() 
    { 
        // tresc metody w klasie gołąb
    }       
}
 
public class Penguin : WalkingBird
{
    public void Walk() 
    { 
        // tresc metody w klasie pingwin
    }       
}
Animal, Cat, Dog

Kolejny przykład ilustrujący złamanie zasady Liskov. Mamy klasę bazową Animal oraz dwie klasy pochodne: Dog, Fish. Klasa Animal ma metodę Run(). Nastąpiło tutaj zjawisko nieprzemyślanego mechanizmu dziedziczenia. Klasa Fish dziedziczy po klasie Animal, ale ryba nie potrafi biegać, więc klasa pochodna – Fish – nie wykorzysta metody klasy bazowej – Run(). Jest to złamanie zasady podstawienia Liskov.

abstract class Animal
{
    public string Name { get; set; }
    public abstract void Run();
}

class Dog : Animal
{
    public override void Run()
    {
        Console.WriteLine("Dog runs");
    }
}

class Fish : Animal
{
    public override void Run()
    {
        throw new NotImplementedException("Fish can not run!"); 
    }
}

class Program
{
    static void Main()
    {
        List<Animal> animals = new List<Animal>();
        
        animals.Add(new Dog());
        animals.Add(new Fish());
        
        animals.ForEach(o => o.Run());
    }
}

Poniżej znajduje się poprawiony kod, który spełnia zasadę podstawienia Liskova. Zmieniliśmy w klasie Animal, metodę run() na metodę move(). Metoda pozwala zarówno psu, jak i rybie na odpowiednie zachowanie (bieganie w przypadku psa i pływanie w przypadku ryby).

abstract class Animal
{
    public string Name { get; set; }
    public abstract void Move();
}

class Dog : Animal
{
    public override void Move()
    {
        Console.WriteLine("Dog runs");
    }
}

class Fish : Animal
{
    public override void Move()
    {
        Console.WriteLine("Fish swims");
    }
}

class Program
{
    static void Main()
    {
        List<Animal> animals = new List<Animal>();

        animals.Add(new Dog());
        animals.Add(new Fish());

        animals.ForEach(o => o.Move());
    }
}
Podsumowanie

Zasada LSP jest kluczowa w dziedziczeniu klas, ponieważ pomaga zachować spójność i uniknąć błędów w kodzie źródłowym. Naruszenie tej zasady może prowadzić do problemów związanych z błędami w programie, trudnościami w utrzymaniu kodu i zmniejszoną elastycznością w hierarchiach dziedziczenia.

Kategorie: SOLID

0 komentarzy

Dodaj komentarz

Avatar placeholder

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