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)