Objects of a superclass should be able to be replaced with objects of its subclasses without causing any errors in the program. In other words, subclasses should be able to extend superclasses' functionality without changing the superclass's behavior. This principle is named after computer scientist Barbara Liskov, who first formulated it in a 1987 conference paper.
Explain it to a 10-year-old
Imagine you have a smartphone to make calls, send texts, and access the internet. You also have a tablet to watch movies, play games, and read books.
If you follow the Liskov Substitution Principle, you should be able to use the tablet wherever you use the smartphone, and it should work the same way. This means that you should be able to make calls, send texts, and access the internet using the tablet, just like you would using the smartphone. You can even use additional features of the tablet, like the larger screen and the higher-resolution camera, that you can't use on the smartphone. This makes your smartphone more flexible and easier to use.
Violation
In this example, the Square class extends the Rectangle class and overrides the set methods for the height and width properties. However, these setters' behavior is inconsistent with the Rectangle class's behavior, which violates the Liskov Substitution Principle. Code that expects a Rectangle object to behave in a certain way will not work correctly when given a Square object instead.
class Rectangle {
height: number;
width: number;
constructor(height: number, width: number) {
this.height = height;
this.width = width;
}
getArea() {
return this.height * this.width;
}
}
class Square extends Rectangle {
constructor(sideLength: number) {
super(sideLength, sideLength);
}
// This setter violates the Liskov Substitution Principle because
// it changes the behavior of the parent class in a way that is not
// expected by code that uses the Rectangle class.
set height(value: number) {
this.width = value;
}
set width(value: number) {
this.height = value;
}
}
const rectangle = new Rectangle(5, 10);
console.log(rectangle.getArea()); // 50
const square = new Square(5);
console.log(square.getArea()); // 25
// This code violates the Liskov Substitution Principle because
// the setters in the Square class break the contract of the
// Rectangle class.
square.height = 10;
console.log(square.getArea()); // 100, expected 25
Fix (Implement to an interface)
With this approach, the Rectangle and Square classes implement the Shape interface, specifying the expected behavior of any class that implements it. This ensures that the Rectangle and Square classes can be used interchangeably, as they both have the same behavior. As a result, the code adheres to the Liskov Substitution Principle, as objects of the Rectangle and Square classes can be used interchangeably without causing any issues in the program.
interface Shape {
height: number;
width: number;
getArea(): number;
}
class Rectangle implements Shape {
height: number;
width: number;
constructor(height: number, width: number) {
this.height = height;
this.width = width;
}
getArea() {
return this.height * this.width;
}
}
class Square implements Shape {
height: number;
width: number;
constructor(sideLength: number) {
this.height = sideLength;
this.width = sideLength;
}
getArea() {
return this.height * this.width;
}
}
const rectangle: Shape = new Rectangle(5, 10);
console.log(rectangle.getArea()); // 50
const square: Shape = new Square(5);
console.log(square.getArea()); // 25
How to validate?
There are several ways to validate whether or not your code violates the Liskov Substitution Principle. Here are some tips to help you identify potential violations in your code:
Look for situations where a derived class overrides or extends the behavior of a base class in a way that is not consistent with the contract of the base class. This is often a sign that the Liskov Substitution Principle is being violated.
Pay attention to the use of abstract base classes and interfaces. These are key tools for achieving polymorphism in object-oriented programming, and they can help you ensure that your code follows the Liskov Substitution Principle.
Use unit tests to validate the behavior of your classes and their derived classes. This can help you catch potential violations of the Liskov Substitution Principle and other defects in your code.
Consider using a linting tool, such as TypeScript's no-unsafe-inheritance rule, to automatically identify potential violations of the Liskov Substitution Principle in your code.