SOLID - Liskov Substitution Principle
November 15, 2021 • ☕️ 5 min read • 🏷 computer, software, solid
Translated by author into: English
The Liskov Substitution Principle (LSP) is an inheritance principle developed by Barbara Liskov, which makes it possible to replace a subclass in a class’s inheritance tree with a higher-level class. According to this principle, in order for subclasses to be used in place of higher-level classes, subclasses must have the same properties and behaviors.
Objects of subclasses must exhibit the same behavior when they are replaced by objects of the superclass.
In PHP, an example to apply the Liskov Substitution principle is as follows:
<?php
// Parent class
class Vehicle {
protected $model;
public function setModel($model) {
$this->model = $model;
}
}
// Child class
class Car extends Vehicle {
public function getModel() {
return "The model of this car is: " . $this->model;
}
}
// Child class
class Bicycle extends Vehicle {
public function getModel() {
return "The model of this bicycle is: " . $this->model;
}
}
// A child class that can be used in place of the parent class
class ElectricCar extends Car {
public function getModel() {
return "This car is electric. " . parent::getModel();
}
}
// Example
$electricCar = new ElectricCar();
$electricCar->setModel("Tesla Model 3");
echo $electricCar->getModel(); // "This car is electric. The model of this car is: Tesla Model 3"In this example, the Vehicle class is a parent class, and the Car and Bicycle classes are its children. The ElectricCar class is a subclass of the Car class. The ElectricCar class can replace the Vehicle class and therefore we can apply any code of the Vehicle class to the ElectricCar class. This forms the basis of the Liskov Substitution principle.
The PHP code below is an example of a class that violates the Liskov Substitution principle.
<?php
// Parent class
class Vehicle {
protected $model;
public function setModel($model) {
$this->model = $model;
}
}
// Child class
class Car extends Vehicle {
public function getModel() {
return "The model of this car is: " . $this->model;
}
}
// Child class
class Bicycle extends Vehicle {
public function getModel() {
return "The model of this bicycle is: " . $this->model;
}
}
// A child class that cannot be used as a replacement for the parent class
class ElectricCar extends Car {
private $batteryType;
public function __construct($batteryType) {
$this->batteryType = $batteryType;
}
public function getModel() {
return "This electric car uses a " . $this->batteryType . " battery. " . parent::getModel();
}
}
// Example
$electricCar = new ElectricCar("lithium-ion");
$electricCar->setModel("Tesla Model 3");
echo $electricCar->getModel(); // "This electric car uses a lithium-ion battery. The model of this car is: Tesla Model 3"
// Error: ElectricCar class cannot be used instead of Vehicle class
$vehicle = new Vehicle();
$vehicle = $electricCar; // Hata: "Cannot assign an ElectricCar instance to a Vehicle variable"In this example, the ElectricCar class is a subclass of the Car class and can replace the Vehicle class. However, the ElectricCar class has a batteryType property, which is not available in the Vehicle class. Therefore, the ElectricCar class cannot be used in place of the Vehicle class. This indicates a violation of the Liskov Substitution principle.
An example to apply the Liskov Substitution principle in GoLang is as follows:
package main
import "fmt"
// Parent interface
type Vehicle interface {
SetModel(model string)
}
// Child struct
type Car struct {
model string
}
func (c *Car) SetModel(model string) {
c.model = model
}
func (c *Car) GetModel() string {
return fmt.Sprintf("The model of this car is: %s", c.model)
}
// Child struct
type Bicycle struct {
model string
}
func (b *Bicycle) SetModel(model string) {
b.model = model
}
func (b *Bicycle) GetModel() string {
return fmt.Sprintf("The model of this bicycle is: %s", b.model)
}
// A child struct that can be used instead of the parent interface
type ElectricCar struct {
Car
}
func (e *ElectricCar) GetModel() string {
return fmt.Sprintf("This car is electric. %s", e.Car.GetModel())
}
func main() {
// Example
electricCar := ElectricCar{}
electricCar.SetModel("Tesla Model 3")
fmt.Println(electricCar.GetModel()) // "This car is electric. The model of this car is: Tesla Model 3"
// ElectricCar struct can be used instead of Parent interface
var vehicle Vehicle = &electricCar
fmt.Println(vehicle.GetModel()) // "This car is electric. The model of this car is: Tesla Model 3"
}In this example, the Vehicle interface is a parent interface, and the Car and Bicycle structs are child structs that implement this interface. The ElectricCar struct is a substruct of the Car struct. ElectricCar struct can replace Vehicle interface and therefore we can apply any code of Vehicle interface to ElectricCar struct.
Similar to the PHP example, the GoLang code below is an example that violates the Liskov Substitution principle.
package main
import "fmt"
// Parent interface
type Vehicle interface {
SetModel(model string)
GetModel() string
}
// Child struct
type Car struct {
model string
}
func (c *Car) SetModel(model string) {
c.model = model
}
func (c *Car) GetModel() string {
return fmt.Sprintf("The model of this car is: %s", c.model)
}
// Child struct
type Bicycle struct {
model string
}
func (b *Bicycle) SetModel(model string) {
b.model = model
}
func (b *Bicycle) GetModel() string {
return fmt.Sprintf("The model of this bicycle is: %s", b.model)
}
// A child struct that cannot be used as a replacement for the parent interface
type ElectricCar struct {
Car
}
func (e *ElectricCar) GetModel() string {
return fmt.Sprintf("This car is electric. %s", e.Car.GetModel())
}
// A method that cannot replace ElectricCar
func (e *ElectricCar) Charge() {
fmt.Println("Charging the electric car...")
}
func main() {
// Example
electricCar := ElectricCar{}
electricCar.SetModel("Tesla Model 3")
fmt.Println(electricCar.GetModel()) // "This car is electric. The model of this car is: Tesla Model 3"
// Error: ElectricCar struct cannot be used to replace Vehicle interface
var vehicle Vehicle = &electricCar // Hata: "ElectricCar does not implement Vehicle (missing Charge method)"
}In this example, the ElectricCar struct is a substruct of the Car struct and can replace the Vehicle interface. However, ElectricCar struct has a Charge() method and this method is not available in Vehicle interface. Therefore, ElectricCar struct cannot be used instead of Vehicle interface.
Resources
- https://en.wikipedia.org/wiki/Liskov_substitution_principle
- https://reflectoring.io/lsp-explained/
- https://blog.knoldus.com/what-is-liskov-substitution-principle-lsp-with-real-world-examples/