C#

SOLID - Zasada podstawiania Liskov

S –   Zasada pojedynczej odpowiedzialności (Single Responsibility Principle – SRP)
OZasada otwarte – zamknięte – (Open-Closed Principle – OCP)
L – Zasada podstawiania Liskov  – (Liskov Substitution Principle – LSP)
I  – Zasada segregacji interfejsu – (Interface Segregation Principle – ISP)
D – Zasada odwracania zależności – (Dependency Inversion  Principle DIP)

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:

🐵
Funkcje, które korzystają z referencji bądź wskaźników do klas bazowych, muszą być w stanie poprawnie używać obiektów klas dziedziczących po tych klasach bazowych, bez ich dokładnej znajomości.

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?

Zasada podstawiania Liskov

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ę 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.

Tagi