How to apply the Liskov substitution principle in Java
Twenty-three years ago, in his Design Principles and Design Patterns article, Robert "Uncle Bob" Martin boiled down the Liskov substitution principle to the idea that "derived classes should be substitutable for their base classes."
That's a good start to help developers understand LSP, but Martin's quick description initially reads like a summary of Java's out-of-the-box inheritance and polymorphism mechanisms. We need to dig deeper to understand what he really meant, to then properly understand LSP.
What is the Liskov substitution principle?
The Liskov substitution principle -- the "L" in the SOLID principles of object-oriented design -- simply demands that any time an instance of a parent class is required, any instance of a subclass should suffice. The goal with LSP is philosophical and behavioral sufficiency, not simply the syntactic sufficiency that inheritance, polymorphism and the Java compiler provide.
Consider a simple problem domain for which we need to calculate the area of various shapes, specifically squares and rectangles. The creation of a Square
class that inherits from a Rectangle
class, along with a runnable main method, is an easy way to explore the relationship between inheritance, polymorphism and LSP.
class Rectangle {
int height;
int width;
Rectangle(int h, int w){
height = h;
width = w;
}
}
class Square extends Rectangle {
Square(int h) {
super(h,h);
}
}
public class Main {
public static void main(String[] args) {
calcArea(new Rectangle(4,5)); //prints 20
calcArea(new Square(5)); //prints 25
}
public static void calcArea(Rectangle rect){
System.out.println(rect.height * rect.width);
}
}
LSP example in Java
The calcArea
method in the Main
class takes a Rectangle
as an argument. However, a Square
inherits from Rectangle
and thus it is a special type of Rectangle
, so we can legally pass an instance of the Square
into the calcArea
method as well:
calcArea(new Rectangle(4,5)); //prints 20
calcArea(new Square(5)); //prints 25
This looks like the Liskov substitution principle that Uncle Bob Martin described: Substitute a derived class (Square
) for a base class (Rectangle
).
From the standpoint of the compiler, the above code properly implements polymorphism and abides by the rules of inheritance in Java. However, this code fails to meet the philosophical and behavioral standard that Liskov substitution requires.
Proof by contradiction
One obvious failure of this code is that a Square
can set its height without setting its width. For example, the following Square
has a height of 5 and width of 4 when the code runs:
Square square = new Square(5);
Square.width = 4;
This issue isn't specifically an LSP problem, but it is a problem, and fixing it can violate the LSP.
Instead, we simply introduce setters and getters, and force the height and width to always synchronize when a Square
changes its size:
class Rectangle {
int height;
int width;
int getHeight(){return height;}
int getWidth() {return width;}
void setHeight(int h) { height = h;}
void setWidth(int w) { width = w;}
Rectangle(int h, int w){
height = h;
width = w;
}
}
class Square extends Rectangle {
Square(int h) {
super(h,h);
}
void setHeight(int h) {
height = h;
width = h;
}
void setWidth(int w) {
height = w;
width = w;
}
}
The code now synchronizes the height and width when using the Square
class's setter methods. The code compiles and the Square
's expected behavior is enforced.
However, this apparent improvement creates new difficulties of LSP noncompliance. Take the following code snippet that inspects the behavior of a Rectangle
:
public static void testRectangle(Rectangle rect) {
rect.setHeight(4);
rect.setWidth(5);
assertTrue(rect.getHeight() == 4) ;
}
If you attempt to pass a Square
into this method, the code would run, the rules of inheritance would apply and polymorphism would work -- but the application would not behave as expected. One would expect to separately change the height and width of a rectangle, but this expected behavior fails when we substitute a Square
for a Rectangle
.
It's not good enough to simply use inheritance and apply the rules of polymorphism to your code. To write SOLID code, and properly implement the Liskov substitution principle in Java, your code must be philosophically and behaviorally compliant as well.
Ashik Patel is an associate full-stack developer with a background as a back-end developer and API developer. He has worked with various programming languages and frameworks, including Java, JavaScript, Go and Python.