Liskov Substitution Principle (LSP)
The Liskov Substitution Principle states that objects of a superclass should be replaceable with objects of its subclasses without breaking the application.
LSP enforces something discussed so far: public inheritance should model an is-a relationship.
If class B
inherits from class A
: we can use pointers / references to B
objects in the place of pointers / references to A
objects: C++ gives us this.
Liskov Substitution is stronger: not only can we perform the substitution, but we should be able to do so at any place in the program, without affecting its correctness.
Precisely
- If an invariance is true for class
A
, then it should also be true for classB
- If an invariant is true for method
A::f
, andB
overridesf
, then this invariant must be true forB::f
- If
B::f
overridesA::f
- If
A::f
has a preconditionP
and a postconditionQ
, thenB::f
should have a preconditionP'
such thatP=>P'
and a postconditionQ'
such thatQ'=>Q
B::f
needs a weaker precondition and a stronger postcondition
Ex: Contravariance problem
- Happens whenever we have a binary operator where the other parameter is the same type as
*this
.
class Shape {
public:
virtual bool operator==(const Shape& other) const;
}
class Circle: public Shape {
public:
bool operator==(const Shape& other) const override;
}
As we’ve seen before, we must take in the same parameter when overriding. C++ enforces this, which actually enforces LSP for us.
- A
Circle
is-aShape
- A
Shape
can be compared with otherShape
s - A
Circle
can be compared with any otherShape
To satisfy LSP, we must support comparison between different types of Shape
s.
bool Circle::operator==(const Shape& other) const {
if (typeid(other) != typeid(Circle)) return false;
const Circle& cother=static_cast<const Circle&>(other);
...
}
typeidreturns
std::type_info` objects that can be compared.
dynamic_cast
: Is other
a Circle
, or a subclass of Circle
?
typeid
: Is other
exactly a Circle
?
typeid uses the dynamic-type so long as the class has at least one virtual method.
Example of Violation of LSP
Ex: Is a Square
a Rectangle
?
4th Grader: Yes, a Square is a rectangle.
A square has all the properties of a rectangle.
class Rectangle {
int length, width;
public:
Rectangle(int l, int w): length{l}, width{w} {}
int getLength() const;
int getWidth() const;
virtual void setWidth(int w) {width=w;};
virtual void setLength(int l) {length=l;};
int area() const {return length * width;};
};
class Square: public Rectangle {
public:
Square(int s): Rectangle{s,s} {}
void setWidth(int w) override {
Rectangle::setWidth(w);
Rectangle::setLength(w);
}
void setLength(int l) override {
Rectangle::setWidth(l);
Rectangle::setLength(l);
}
};
int f(Rectangle& r) {
r.setLength(10);
r.setWidth(20);
return r.area(); // surely this returns 200
}
// But:
Square s{100};
f(s); // gives us 400
What is the issue here?
we expect postcondition for
Rectangle::setWith
to be that the width is set and nothing else changes. But this is violated by ourSquare
class.
Violates LSP, does not satisfy an is-a relationship. Conclusion: Square are not Rectangles.
Possibility: Restructure inheritance hierarchy to make sure LSP is respected.
In general: how can we prevent LSP violations?
We should restrain what our subclasses can do, only allows them to customize what is truly necessary.
We can use a design pattern: Template Method Pattern, to make this process easier.