Introduction
Liskov Substitution Principle is the third principle in S.O.L.I.D
principles. It was introduced by Barbara Liskov in 1987. It is a principle in object-oriented programming that states that if a program is using a base class, then the reference to the base class can be replaced with a derived class without affecting the functionality of the program.
Problem
Let's say we have a Rectangle
class that has a width
and height
properties and a getArea
method that returns the area of the rectangle.
class Rectangle {
constructor(public width: number, public height: number) {}
getArea(): number {
return this.width * this.height;
}
}
Now we want to create a Square
class that extends the Rectangle
class. The Square
class has a size
property and overrides the getArea
method to return the area of the square.
class Square extends Rectangle {
constructor(public size: number) {
super(size, size);
}
getArea(): number {
return this.size * this.size;
}
}
Now let's create a function that takes a Rectangle
object and prints its area.
function printArea(rectangle: Rectangle) {
console.log(rectangle.getArea());
}
Now let's create a Rectangle
object and pass it to the printArea
function.
const rectangle = new Rectangle(2, 3);
printArea(rectangle); // 6
Now let's create a Square
object and pass it to the printArea
function.
const square = new Square(2);
printArea(square); // 4
As you can see, the printArea
function works fine with both Rectangle
and Square
objects. But what if we want to create a Square
object and pass it to the printArea
function as a Rectangle
object?
Although this works fine, but we had to override the getArea
method in the Square
class to make it work. This is a violation of the Liskov Substitution Principle. The Square
class is not a proper substitute for the Rectangle
class.
Solution
To make it properly addressed, we can do it using base class.
Using A Shape as Base Class
Let's make a base class Shape
this is basically an abstract class with abstract getArea
method.
abstract class Shape {
abstract getArea(): number;
}
Now let's make the Rectangle
and Square
classes extend the Shape
class.
class Rectangle extends Shape {
constructor(public width: number, public height: number) {
super();
}
getArea(): number {
return this.width * this.height;
}
}
class Square extends Shape {
constructor(public size: number) {
super();
}
getArea(): number {
return this.size * this.size;
}
}
Now let's create a function that takes a Shape
object and prints its area.
function printArea(shape: Shape) {
console.log(shape.getArea());
}
Another way to solve this problem
We can also solve this problem by using an interface.
interface Shape {
getArea(): number;
}
Now let's make the Rectangle
and Square
classes implement the Shape
interface.
class Rectangle implements Shape {
constructor(public width: number, public height: number) {}
getArea(): number {
return this.width * this.height;
}
}
class Square implements Shape {
constructor(public size: number) {}
getArea(): number {
return this.size * this.size;
}
}
Now let's create a function that takes a Shape
object and prints its area.
function printArea(shape: Shape) {
console.log(shape.getArea());
}
More Example
Imagine that we have a Vehicle
class that has a fuelUp
and maxFuelCapacity
methods
class Vehicle {
public fuelUp(): string {
return 'Fueled';
}
public maxFuelCapacity(): number {
return 100;
}
}
Now Let's create Car
class which inherits the Vehicle
class, pretty much the same methods in our case.
class Car extends Vehicle {
// nothing to do
}
Let's use it with a function fuelUpVehicle
that takes a Vehicle
object and calls the fuelUp
method.
function fuelUpVehicle(vehicle: Vehicle): void {
vehicle.fuelUp();
}
Now let's create a Car
object and pass it to the fuelUpVehicle
function.
const car = new Car();
fuelUpVehicle(car);
So far so good, now let's create another vehicle, a ElectricCar
this time, which also inherits the Vehicle
class, but it doesn't need the fuelUp
method, because it doesn't use fuel.
class ElectricCar extends Vehicle {
public fuelUp(): string {
throw new Error('Electric car does not need fuel');
}
public maxFuelCapacity(): number {
return 0;
}
}
If we tried to use the same function fuelUpVehicle
with the ElectricCar
object, it will throw an error.
const electricCar = new ElectricCar();
fuelUpVehicle(electricCar); // Error: Electric car does not need fuel
This violates the Liskov Substitution Principle
because ElectricCar
is not a proper Substitution
for the Vehicle
car.
Let's fix this behavior by changing the ElectricCar
class
class ElectricCar extends Vehicle {
public fuelUp(): string {
return 'Electric car is charged';
}
public maxFuelCapacity(): number {
return 100; // this is the battery capacity in percentage not in liters
}
}
Now if we tried to use the same function fuelUpVehicle
with the ElectricCar
object, it will work fine.
const electricCar = new ElectricCar();
fuelUpVehicle(electricCar); // Electric car is charged
Conclusion
To address Liskov Substitution Principle (LSP)
we can say that the base class should be able to be substituted with any of its derived classes without affecting the functionality of the program.
In other words, Subtypes must be substitutable for their base types. In simpler terms, if a piece of code works with objects of the base class, it should work equally well (without surprises) with objects of any derived class.
Following the list
You can see the updated list of design principles from the following link
https://mentoor.io/en/posts/634524154/open-closed-principle-in-typescript
Join us in our Discord Community
https://discord.gg/XDZcTuU8c8
Top comments (0)