Liskov Substitution Principle (LSP)
The Liskov Substitution Principle (LSP) is the “L” in SOLID. It was introduced by Barbara Liskov in 1987. It states:
“Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.”
In simpler terms: A derived class should complement its base class, not contradict it.
The Classic Violation: Square and Rectangle
In geometry, a square is a rectangle. However, in software, this can lead to logic errors if not handled carefully.
The Violation (Python)
class Rectangle:
def __init__(self, width, height):
self._width = width
self._height = height
def set_width(self, w): self._width = w
def set_height(self, h): self._height = h
def get_area(self): return self._width * self._height
class Square(Rectangle):
def set_width(self, w):
self._width = w
self._height = w # Constraints of a square
def set_height(self, h):
self._width = h
self._height = h
Now, imagine a function that expects a Rectangle:
def increase_width(rect: Rectangle):
rect.set_width(10)
rect.set_height(5)
# For a classic Rectangle, area should be 50.
# For a Square, area will be 25 because set_height(5) overrode the width!
assert rect.get_area() == 50 # This fails if rect is a Square!
The Square class is not a valid substitute for Rectangle in this context.
LSP Rules
- Contravariance of method arguments: A subclass should not require more than the superclass.
- Covariance of return types: A subclass should not return less than the superclass.
- No new exceptions: A subclass should not throw new exceptions that the client doesn’t expect.
- Preconditions cannot be strengthened: You can’t require more input data.
- Postconditions cannot be weakened: You must still guarantee the output.
Correct Design: Use Interfaces
Instead of inheritance, use a shared interface or rethink the hierarchy.
class Shape:
def get_area(self):
pass
class Rectangle(Shape):
def __init__(self, w, h):
self.w, self.h = w, h
def get_area(self): return self.w * self.h
class Square(Shape):
def __init__(self, side):
self.side = side
def get_area(self): return self.side ** 2
LSP in Java: Empty Implementations
A common LSP violation is when a subclass implements a method but leaves it empty or throws a “NotImplementedException”.
public interface Bird {
void fly();
}
public class Duck implements Bird {
public void fly() { System.out.println("Flying..."); }
}
public class Ostrich implements Bird {
public void fly() {
throw new UnsupportedOperationException("Ostriches can't fly!");
}
}
If you pass an Ostrich to a function expecting Bird, and that function calls .fly(), the program crashes. Ostrich broke the contract of Bird.
How to Fix LSP Violations
- Refactoring to Interface: Split the interface (e.g.,
FlyingBirdvsSwimmingBird). - Composition: Instead of inheriting, have the class contain an instance of the other.
- Is-A vs Behaves-Like: Ensure the subclass truly behaves like the parent.
Benefits
- Predictability: You can trust that any object of type
Twill behave according toT’s contract. - Robustness: Reduces runtime errors and “special case” handling in calling code.