Liskov Substitution Principle
Part 3 of the SOLID principles; L is for the Liskov Substitution Principle (LSP). The simplest LSP definition states:
"Derived classes must be substitutable for their base classes." Robert.C.Martin
Fun fact: “Liskov” in LSP refers the Barbara Liskov; an American computer scientist who is the original developer of the LSP.
What is it?
This principle basically states that objects of a superclass must be replaceable with objects of its subclass without causing any breaking changes. So if you have a method that accepts an object of type x, you should be able to pass the child object of type x, and this should work as expected. Basically, a child class should be able to do anything a parent class can do.
Why use it?
One of the main reasons for subclassing is so that you can write a new subclass and use it in the same working, tested code. This is the essence of the Open/Closed principle; the class you want to use the new subclass with should be open for extension but closed for modification; in this case, it’s closed for modification as you don’t need to change the class, but simply the object you’re passing. Other benefits include more loosely coupled code as well as encouraging code reusability.
When to use it?
You should be applying the LSP whenever you use sub classing.
Example
Here is the classic example of shapes which violates the LSP. Given we have a class Rectangle class:
class Rectangle {
height: number;width: number;area() {
return this.height * this.width;
}
}
And we have a class Square which inherits from Rectangle:
class Square extends Rectangle {
_height: number;_width: number;get height() {
return this._height;
}
set height(value: number) {
this._height = value;
this._width = value;
}
get width() {
return this._width;
}
set width(value) {
this._width = value;
this._height = value;
}
}
Looks good right? In the Square class, we set both the width and the height to the same value, each time any of them changes as they both always have the same value.
If both objects had the same width and height, would we see the same result? We should! The subclass must override the base class in a way that doesn’t break functionality. Here is a jasmine test demonstrating the problem:
describe("Liskov Substitution Principle", function () {
var square;
var rectangle;
beforeEach(function () {
rectangle = new Rectangle();
rectangle.width = 6 rectangle.height = 2;
square = new Square();
square.width = 6 square.height = 2;
});
describe("when the area is calculated for a square", function () {
it("should return an area of 12", function () {
expect(rectangle.area).toEqual(12);
});
});
describe("when the area is calculated for a rectangle", function () {
it("should return an area of 12", function () {
expect(square.area).toEqual(12);
});
});
});
To resolve this issue and abide by LSP, we should change our code to:
abstract class Shape {
get area(): number;
}
class Rectangle extends Shape {
height: number;width: number;get area() {
return this.height * this.width;
}
}
class Square extends Shape {
length: number;get area() {
return this.length * this.length;
}
}
Now we specify that both Square and Rectangle are types of Shape, which must implement the area method. Now, if a Rectangle is required, you won’t even have the option to provide a Square.
TL;DR
OOP is all about mapping our class to real life objects, but the LSP shows we need to be careful; Even though a square is a rectangle, the external behaviour of the two classes is incompatible, so inheritance is invalid. In general, your subclass should behave in the same way as your parent class.
Ps. If an override method does nothing or just throws an exception or returns null, then you're probably violating the LSP.
Happy coding!
Comments