Definition
The Liskov Substitution Principle (LSP) is the third of the five SOLID principles of object-oriented design. It states that:
"Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program."
In other words, if class B is a subtype of class A, then we should be able to replace objects of type A with objects of type B without disrupting the behavior of our program.
// Base class
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
setWidth(width) {
this.width = width;
}
setHeight(height) {
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
// Derived class that violates LSP
class Square extends Rectangle {
constructor(size) {
super(size, size);
}
// This violates LSP because it changes the behavior
setWidth(width) {
this.width = width;
this.height = width; // Square must maintain equal sides
}
// This also violates LSP
setHeight(height) {
this.height = height;
this.width = height; // Square must maintain equal sides
}
}
This implementation violates the Liskov Substitution Principle for several reasons:
- The
Square
class changes the behavior of thesetWidth
andsetHeight
methods. - Code that works with a
Rectangle
expects to be able to change width and height independently. - If we substitute a
Square
for aRectangle
, the program may behave incorrectly. - Consider this function that would work with a
Rectangle
but fail with aSquare
:
function increaseRectangleWidth(rectangle) {
const initialHeight = rectangle.height;
rectangle.setWidth(rectangle.width + 10);
// With a Square, this assertion would fail
assert(rectangle.height === initialHeight);
}
// Base shape interface/class
class Shape {
getArea() {
throw new Error("Method 'getArea()' must be implemented.");
}
}
// Rectangle implementation
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
setWidth(width) {
this.width = width;
}
setHeight(height) {
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
// Square as a separate implementation, not derived from Rectangle
class Square extends Shape {
constructor(size) {
super();
this.size = size;
}
setSize(size) {
this.size = size;
}
getArea() {
return this.size * this.size;
}
}
This implementation follows the Liskov Substitution Principle:
- Both
Rectangle
andSquare
extend theShape
base class. - Each class has its own appropriate interface that doesn't violate expectations.
Square
doesn't try to overrideRectangle
methods in a way that changes behavior.- Code that expects a
Shape
will work correctly with either aRectangle
or aSquare
. - The hierarchy accurately reflects the relationship between the shapes.
This approach recognizes that a square is not a special type of rectangle in terms of behavior in an object-oriented system, even though mathematically it is.
Interactive Demo: Rectangle vs Square
This demo shows how the Liskov Substitution Principle works with rectangles and squares. Try changing the dimensions and see how the different implementations behave.