Some of the content and diagrams in this article have been quoted from the Uncle Bob’s speech in the link below.
I highly recommend you to watch the whole video for better understanding.
We will go through with Uncle Bob’s examples first, followed by a straightforward Dart example that has been adapted.
What does actually Liskov Substitution Principle (LSP) means using the derived class and the base class without knowing the difference?
Let’s have a look at the diagram below:
Base Class: Abstract Server
Derived Class: Concrete Server
In the example, Client is dependent on AbstractServer and ConcreteServer inherits the AbstractServer. According to LSP if an instance of Client class is using any action on AbstractServer, when ConcreteServer is replaced with that, it should be still usable.
Concisely, base class and derived class must be substitutable.
Let’s go ahead with a simple use case.
We can basically say a square is a rectangle, right? Assuming that means we can derive a Square class from Rectangle. However square does not have different hight and width, that’s why whenever width or height is set, both width and height must be set.
Even though it looks correct, we better ask some questions to grasp if our design is well structured.
Let’s ask some questions to clarify:
How many fields does a rectangle have?
Two. Width and Height
How many fields does a square have?
One. Width (widht = height)
How many fields square inherits from rectangle?
One.
Then something is wrong because it means we are going to waste memory. You might think memory is cheap and would like to ignore it for now.
Imagine a user of Rectangle calling SetHeight function. Does the user expect that width will not change when height is set? Yes.
Then here we have a problem.
This is a violation of the LSP because Square is not substitutable for rectangle. Even though it seems to fit the definition of “is a”, it does fit the substitution rule.
Whenever we violate the substitution rule, we will eventually have an if statement that checks the type of the object to prevent us from crashing our system. Basically it means that we create another dependency using those if statements which is also violating the Open Closed Principle.
Here we have a Dart example violating the LSP.
class Rectangle {
Rectangle(this.width, this.height);
int width;
int height;
void setWidth(int width) => this.width = width;
void setHeight(int height) => this.height = height;
int calculateArea() => width * height;
}
class Square extends Rectangle {
Square(int side) : super(side, side);
@override
void setWidth(int width) {
super.setWidth(width);
super.setHeight(width);
}
@override
void setHeight(int height) {
super.setWidth(height);
super.setHeight(height);
}
}
void increaseWidth(Rectangle rectangle) {
rectangle.setWidth(rectangle.width + 1);
}
void main() {
final rectangle = Rectangle(10, 20);
final square = Square(10);
print(rectangle.calculateArea());
print(square.calculateArea());
increaseWidth(rectangle);
increaseWidth(square);
print(rectangle.calculateArea());
print(square.calculateArea());
}
In the main function body rectangle and square instances are created and their area is displayed using the calculateArea function. And Here is what it prints out.
200 // 10 * 20 rectangle area
100 // 10 * 10 square area
Other than that we have an increateWidth functions which takes a Rectangle object (can be rectangle and square) as a parameter.
Once increateWidth function is called for both rectangle and square, we calculate their area again and print out the results.
220 // 11 * 10 rectangle area
121 // 11 * 11 square area
Wait a minute. Even if square area is calculated correctly, would we really expect the height is increased after width is increased? As you have seen, we just complicated the situation. Therefore violating the LSP can be dangerous.
In the example above for increaseWidth function, derived class (Square) and base class (Rectangle) is not substitutable.
Let’s take a look at this example:
abstract interface class Shape {
void calculateArea();
}
class Rectangle extends Shape {
Rectangle(this.width, this.height);
int width;
int height;
void setWidth(int width) => this.width = width;
void setHeight(int height) => this.height = height;
@override
void calculateArea() => width * height;
}
class Square extends Shape {
Square(this.side);
int side;
void setSide(int side) => this.side = side;
@override
void calculateArea() => side * side;
}
...
In this approach, we provided a Shape class to be derived by Rectangle and Square. And using the new version of the classes, we can substitute rectangle and square where they can be used by a client.
As a conclusion considering the LSP during implementation is going to prevent us to complicate the class relations and usage.