SOLID #3 – LSP

Przyszedł czas na przedstawienie kolejnej zasady, teraz kolej na LSP – zasada podstawiania Liskov. Skąd taka nazwa i co to ten Liskov? Nie ten a ta, a dokładnie Barbara Liskov. Pani ta sformułowała tę zasadę w roku 1988 a brzmi ona następująco:

 

 

Oznacza to w skrócie, że metoda przyjmująca jako parametr referencje do klasy bazowej, powinna działać poprawnie gdy prześlemy do niej obiekt tej klasy, ale także w przypadku gdy podstawimy obiekt klasy pochodnej dziedziczącej po tej klasie. Tyle teoria, ale jak to wygląda w praktyce?

 

Konkrety

Posiadamy trzy klasy: Vehicle, Car oraz ElectricCar. Klasa Vehicle jest klasą abstrakcyjną, czyli taką, której instancji tworzyć nie możemy, natomiast służy ona jako interfejs dla klas pochodnych. Vehicle posiada trzy właściwości publiczne Combustion(spalanie), Name oraz WheelsCount oraz pomocniczą metodę About opisującą klasę – będzie ona pomocna w testach. Klasy Car oraz Electric są powiązane z klasą Vehicle relacją Is-A (jest czymś). Samochód(Car) jest rodzajem pojazdu(Vehicle) także samochód elektryczny(ElectricCar) jest rodzajem pojazdu, więc dziedziczenie wydaje się być poprawne.

public abstract class Vehicle
{
    protected Vehicle(int combusion, string name, int wheelsCount)
    {
        Combustion = combusion;
        Name = name;
        WheelsCount = wheelsCount;
    }

    public int Combustion { get; set; }
    public string Name { get; set; }
    public int WheelsCount { get; }

    public void About()
    {
        Console.WriteLine("I'm ABSTRACT!");
    }
}

public class Car : Vehicle
{
    public Car(string type, int combusion, string name, int wheelsCount) :
        base(combusion, name, wheelsCount)
    {
        Type = type;
    }

    public string Type { get; set; }

    public void About()
    {
        Console.WriteLine($"I'm {Type} <CAR> - {Name} - with {Combustion}L combustion and {WheelsCount} wheels!");
    }
}

public class ElectricCar : Vehicle
{
    public ElectricCar(int batteriesCount, int combusion, string name, int wheelsCount) :
        base(combusion, name, wheelsCount)
    {
        BatteriesCount = batteriesCount;
    }

    public int BatteriesCount { get;set; }

    public void About()
    {
        Console.WriteLine($"I'm  <ELECTRIC_CAR> - {Name} - with {Combustion}L combustion, {WheelsCount} wheels and {BatteriesCount} batteries!");
    }
}

Klasa Car oraz ElectricCar dziedziczą po abstrakcyjnej klasie bazowej Vehicle. Obie rozszerzają ją o nowe właściwości oraz posiadają swoją metodę About, z odpowiednio zmienionym ciałem. Dobrze, teraz dodajmy metodę, która jako swój parametr przyjmuję obiekt klasy Vehicle:

public void ShowInfoAboutVehicle(Vehicle vehicle)
{
    vehicle.About();
}

Teraz utwórzmy kilka obiektów:

private static void Main()
{
    ElectricCar elCar= new ElectricCar(10, -100, "The Best Electric Car", 4);
    Car car = new Car("Sport", 20, "Opel", 4);

    elCar.About();
    car.About();
}

Jakich wyników spodziewamy się po uruchomieniu aplikacji? Otóż naszym oczom ukazują się takie informacje:

Pomimo tego, że utworzyliśmy obiekty klas Car oraz ElectricCar, wynikiem działania programu są informacje z klasy bazowej. Co się stało? Zauważmy, że w klasach Car i ElectricCar przesłaniamy metodę About klasy Vehicle. Podczas przekazywania obiektów utworzonych metodzie Main do metody ShowInfoAboutVehicle, następuje  rzutowanie klasy pochodnej na typ klasy bazowej – zostanie wywołana metoda typu statycznego, czyli w tym przypadku typu Vehicle.

Co zrobić aby została wywołana metoda odpowiedniego typu?

 

Poprawki

Moglibyśmy sprawdzać rzeczywisty typ przekazywanego obiektu:

public void ShowInfoAboutVehice(Vehicle vehicle)
{
    if (vehicle.GetType() == typeof(Car))
        ((Car)vehicle).About();
    else if (vehicle.GetType() == typeof(ElectricCar))
        ((ElectricCar)vehicle).About();
}

Dzięki temu wyniki byłyby poprawne:

 

Stanowczo odradzam takie rozwiązanie problemy, ze względu na to, że przy dodaniu kolejnej klasy pochodnej będziemy zmuszeni do dodania kolejnego warunku if do tej metody do obsłużenia nowego typu – przy okazji łamiemy zasadę OCP.

 

W takim razie pokażę odpowiednie rozwiązanie tego problemu. Do tej pory korzystaliśmy z polimorfizmu statycznego. Aby nasza metoda zwracała informacje odpowiednie dla rodzaju przekazanego obiektu, będziemy zmuszeni dokonać poprawek w naszych klasach. W klasie Vehicle do definicji metody About dodamy słowo kluczowe virtual, natomiast w dwóch pozostałych klasach słowo override. Dzięki temu w metodzie ShowInfoAboutVehice zostanie wywołana metoda About rzeczywistego typu obiektu do niej przekazanego.

public abstract class Vehicle
{
    protected Vehicle(int combusion, string name, int wheelsCount)
    {
        Combustion = combusion;
        Name = name;
        WheelsCount = wheelsCount;
    }

    public int Combustion { get; set; }
    public string Name { get; set;}
    public int WheelsCount { get; set;}

    public virtual void About()
    {
        Console.WriteLine("I'm ABSTRACT!");
    }
}

public class Car : Vehicle
{
    public Car(string type, int combusion, string name, int wheelsCount) :
        base(combusion, name, wheelsCount)
    {
        Type = type;
    }

    public string Type { get; set; }

    public override void About()
    {
        Console.WriteLine($"I'm {Type} <CAR> - {Name} - with {Combustion}L combustion and {WheelsCount} wheels!");
    }
}

public class ElectricCar : Vehicle
{
    public ElectricCar(int batteriesCount, int combusion, string name, int wheelsCount) :
        base(combusion, name, wheelsCount)
    {
        BatteriesCount = batteriesCount;
    }

    public int BatteriesCount { get; set; }

    public override void About()
    {
        Console.WriteLine($"I'm  <ELECTRIC_CAR> - {Name} - with {Combustion}L combustion, {WheelsCount} wheels and {BatteriesCount} batteries!");
    }
}

 

Dzięki temu zabiegowi nasza metoda ValidShowInfoAboutVehicle wygląda bardzo prosto:

public void ShowInfoAboutVehicle(Vehicle vehicle)
{
    vehicle.About();
}

 

Jeszcze jedno

Teraz metoda ta jest uniezależniona od nowych typów pochodnych klasy Vehicle. Wszystko wydaje się być poprawne. Wywoływana jest metoda About odpowiedniego typu. Teraz spójrzmy na naszą właściwość Combustion w klasie Vehicle. Ma ona sens dla pojazdów spalinowych, natomiast dla pojazdów elektrycznych jest ona kompletnie bezużyteczna. Mimo to występuje ona w definicji klasy Vehicle, przez co siłą rzeczy jest dostępna we wszystkich klasach pochodnych. Jest to nieścisłość przed, którą chroni nas właśnie LSP. Należy tworzyć takie drzewa dziedziczenia, aby kolejne klasy pochodne korzystały poprawnie ze wszystkich metod, pól i metod swojej klasy bazowej, pamiętając o problemach, które czyhają przy korzystaniu z polimorfizmu – należy świadomie korzystać z tego mechanizmu. Dzięki czemu zachowanie klas będzie w pełni przewidywalne.

 

Mam coś dla Ciebie

Zapisz się do mojego newslettera, a ja prześlę Ci zbiór kilkunastu praktycznych wskazówek dla programisty aplikacji mobilnych.

Menu