Subtypes must be substitutable for their base types without altering the correctness of the program
Methods that use references to base classes must be able to use objects of derived classes without knowing it.
If
S
is a subtype ofT
, then objects of typeT
in a program may be replaced with objects of typeS
without altering any of the desirable properties of that program
Violating the Liskov’s Substitution Principle :
A Square
is a Rectangle
. Indeed it is a specialization of a rectangle. The “IS A” makes you want to model this with inheritance. However if in code you made Square
derive from Rectangle
, then a Square
should be usable anywhere you expect a Rectangle
. This makes for some strange behavior as follows:
class Rectangle {
protected:
uint32_t m_width, m_height;
Public:
Rectangle(const uint32_t width, const uint32_t height) : m_width{width}, m_height{height} {}
uint32_t get_width() const { return m_width; }
uint32_t get_height() const { return m_height; }
virtual void set_width(const uint32_t width) { this->m_width = width; }
virtual void set_height(const uint32_t height) { this->m_height = height; }
uint32_t area() const { return m_width * m_height; }
};
class Square : public Rectangle {
Public:
Square(uint32_t size) : Rectangle(size, size) {}
void set_width(const uint32_t width) override { this->m_width = m_height = width; }
void set_height(const uint32_t height) override { this->m_height = m_width = height; }
};
void process(Rectangle &r) {
uint32_t w = r.get_width();
r.set_height(10);
assert((w * 10) == r.area()); // Fails for Square <--------------------
}
int main() {
Rectangle r{5, 5};
process(r);
Square s{5};
process(s);
return EXIT_SUCCESS;
}
- As you can see above, we have violated Liskovs’s Substitution Principle in the
void process(Rectangle &r)
function. ThereforeSquare
is not a valid substitute ofRectangle
. - If you see from the design perspective, the very idea of inheriting
Square
fromRectangle
is not a good idea. BecauseSquare
does not have height & width, rather it has the size/length of sides.
Solution:
Use dynamic_cast to check the type of the class. This is not a good Idea.
void process(Rectangle &r) {
uint32_t w = r.get_width();
r.set_height(10);
if (dynamic_cast<Square *>(&r) != nullptr)
assert((r.get_width() * r.get_width()) == r.area());
else
assert((w * 10) == r.area());
}
No need to create a separate class for Square
. Instead, you can simply check for bool
flag within the Rectangle
class to validate Square
property. Though not a recommended way. This is just OK.
void process(Rectangle &r) {
uint32_t w = r.get_width();
r.set_height(10);
if (r.is_square())
assert((r.get_width() * r.get_width()) == r.area());
else
assert((w * 10) == r.area());
}
Best Solution: Use proper inheritance hierarchy
Create base class name Shape and inherit this class to Circle and Rectangular derived class.
class Shape {
public:
virtual uint32_t area() const = 0;
};
class Rectangle : public Shape {
Public:
Rectangle(const uint32_t width, const uint32_t height) : m_width{width}, m_height{height} {}
uint32_t get_width() const { return m_width; }
uint32_t get_height() const { return m_height; }
virtual void set_width(const uint32_t width) { this->m_width = width; }
virtual void set_height(const uint32_t height) { this->m_height = height; }
uint32_t area() const override { return m_width * m_height; }
private:
uint32_t m_width, m_height;
};
class Square : public Shape {
public:
Square(uint32_t size) : m_size(size) {}
void set_size(const uint32_t size) { this->m_size = size; }
uint32_t area() const override { return m_size * m_size; }
private:
uint32_t m_size;
};
void process(Shape &s) {
// Use polymorphic behaviour only i.e. area()
}
Another Example:
Violating the Liskov’s Substitution Principle
- There is a abstract base class “Bird” in which there is 1 pure virtual function “Fly”.
- The Bird class derived in Parrot class. Parrot class can have the Fly function. So this is good.
- The Bird class derived in Ostrich class. Ostrich class should not have the Fly function. So this is not a good implementation.
class Bird {
public:
virtual void Fly();
};
class Parrot : public Bird {
public:
void Fly() { throw new NotImplementedException(); } // To implement
};
class Ostrich : public Bird {
public:
void Fly() { throw new NotImplementedException(); } // How to implement this, Ostrih can't fly??
};
Solution:
class Bird {
};
class FlyingBird : public Bird {
public:
virtual void Fly() { throw new NotImplementedException(); }
};
class Parrot : public FlyingBird {
public:
void Fly() { throw new NotImplementedException(); }
};
class Ostrich : public Bird {
};