SOLID Design Principles

# 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.

So precisely:

• If an invariance is true for class A, then it should also be true for class B
• If an invariant is true for method A::f, and B overrides f, then this invariant must be true for B::f
• If B::f overrides A::f
• If A::f has a precondition P and a postcondition Q, then B::f should have a precondition P' such that P=>P' and a postcondition Q' such that Q'=>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.

1. A Circle is-a Shape
2. A Shape can be compared with other Shapes
3. A Circle can be compared with any other Shape

To satisfy LSP, we must support comparison between different types of Shapes.

bool Circle::operator==(const Shape& other) const {
if (typeid(other) != typeid(Circle)) return false;
const Circle& cother=static_cast<const Circle&>(other);
...
}

 typeidreturnsstd::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 our Square` 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.