What is Liskov Substitution Principle?
In simpler terms, this principle advocates that objects of a superclass should be replaceable with objects of its subclass without affecting the correctness of the program.
It states that: Objects of a superclass should be replaceable with objects of its subclasses without altering the correctness of the program.
This means that a subclass should behave in such a way that it can seamlessly substitute its parent class without breaking the program's functionality. LSP ensures that subclasses remain consistent with the expectations set by their parent class.
In other words, a subclass should be able to be substituted for its parent class without causing any errors or unexpected behavior.
Key Concepts
- Behavioral Compatibility: A subclass must exhibit the behavior expected by the parent class. It should not introduce unexpected behavior.
- Contract Preservation: Subclasses must adhere to the "contract" defined by the parent class (method definitions, preconditions, postconditions, invariants).
- No Side Effects: Substitution should not lead to program errors, misbehavior, or inconsistencies.
Understanding with example:
Violation of LSP
Consider an example where Rectangle is a parent class, and Square is a subclass:
Example: LSP Violation
class Rectangle {
protected:
int width;
int height;
public:
void setWidth(int w) { width = w; }
void setHeight(int h) { height = h; }
int getArea() { return width * height; }
};
class Square : public Rectangle {
public:
void setWidth(int w) override {
width = w;
height = w; // Ensure square's sides are equal
}
void setHeight(int h) override {
width = h;
height = h; // Ensure square's sides are equal
}
};
// Client code
void printArea(Rectangle& rect) {
rect.setWidth(5);
rect.setHeight(10);
cout << "Area: " << rect.getArea() << endl; // Expect 50
}
int main() {
Rectangle rect;
printArea(rect); // Works as expected
Square sq;
printArea(sq); // Output is incorrect because setWidth affects height
return 0;
}
Why This Violates LSP:
- Behavioral Mismatch: The client code expects the area to be computed as
width * height
. However,Square
overrides these methods to maintain equal sides, which disrupts the expected behavior. - Substitution Breaks: Replacing
Rectangle
withSquare
causes logical errors in the program.
Adhering to LSP
To adhere to LSP, we can redesign the hierarchy to avoid such conflicts. One way is to avoid inheriting Square
from Rectangle
and instead use composition.
Correct Design: Composition Instead of Inheritance
class Shape {
public:
virtual int getArea() const = 0;
virtual ~Shape() = default;
};
class Rectangle : public Shape {
protected:
int width;
int height;
public:
Rectangle(int w, int h) : width(w), height(h) {}
void setWidth(int w) { width = w; }
void setHeight(int h) { height = h; }
int getArea() const override { return width * height; }
};
class Square : public Shape {
protected:
int side;
public:
Square(int s) : side(s) {}
void setSide(int s) { side = s; }
int getArea() const override { return side * side; }
};
// Client code
void printArea(Shape& shape) {
cout << "Area: " << shape.getArea() << endl;
}
int main() {
Rectangle rect(5, 10);
printArea(rect); // Output: Area: 50
Square sq(5);
printArea(sq); // Output: Area: 25
return 0;
}
Why This Works:
- No Inheritance Conflicts:
Rectangle
andSquare
are independent implementations of theShape
interface. - Behavioral Compatibility: Each class maintains its unique behavior without violating client expectations.
Example
Let's consider an example of a Bird
class hierarchy that includes different types of birds, such as Eagle
, Penguin
, and Ostrich
. Each bird has a different ability to fly.
class Bird {
public:
virtual void fly() = 0;
};
class Eagle : public Bird {
public:
void fly() override;
};
class Penguin : public Bird {
public:
void fly() override;
};
class Ostrich : public Bird {
public:
void fly() override;
};
Now, let's suppose that we have a function that takes a Bird
object and makes it fly.
void makeBirdFly(Bird& bird) {
bird.fly();
}
According to the LSP, we should be able to pass any Bird
subclass object to this function without affecting the correctness of the program. For example, we should be able to pass an Eagle
object, a Penguin
object, or an Ostrich
object to this function.
Eagle eagle;
Penguin penguin;
Ostrich ostrich;
makeBirdFly(eagle); // okay, eagle can fly
makeBirdFly(penguin); // error, penguin can't fly
makeBirdFly(ostrich); // error, ostrich can't fly
As we can see, passing a Penguin
or an Ostrich
object to this function would result in an error because they cannot fly. Therefore, these classes do not adhere to the same behavior as their parent class, Bird
, and violate the LSP.
Example:
Imagine we have a set of geometric shapes: Rectangle
, Shape
, and Circle
. All these shapes have a area calculation method:
#include <iostream>
class Shape {
public:
virtual double area() const = 0;
};
class Rectangle : public Shape {
private:
double width, height;
public:
Rectangle(double width, double height){
this->width = width;
this->height = height;
}
double area() const override {
return width * height;
}
};
class Square : public Shape {
private:
double side;
public:
Square(double side){
this->side = side;
}
double area() const override {
return side * side;
}
};
class Circle : public Shape {
private:
double radius;
public:
Circle(double radius){
this->radius = radius;
}
double area() const override {
return 3.14159 * radius * radius;
}
};
Here, Rectangle
, Square
, and Circle
all inherit from Shape
and implement the area()
method. This adheres to LSP because each shape can be substituted for another in contexts where the Shape
base class is expected. For example:
void printArea(const Shape& shape) {
std::cout << "Area: " << shape.area() << std::endl;
}
int main() {
Rectangle rect{4, 5};
Square square{4};
Circle circle{3};
printArea(rect); // Output: Area: 20
printArea(square); // Output: Area: 16
printArea(circle); // Output: Area: 28.27431
return 0;
}
In this example, we can see that Rectangle
, Square
, and Circle
objects are seamlessly substituting Shape
objects in the printArea()
function without affecting the correctness of the program, thus satisfying the LSP.