Liskov’s Substitution Principle | SOLID as a Rock

Vishal Chovatiya - May 18 '20 - - Dev Community

So you know how to code in general, understand the object-oriented programming, learned C++, and completed at least one Software Development Course (if you're not there yet, these articles aren't for you). You can write software easily if you know at least one programming language, but is your code any good? Could it be done any better? Is it clean (and what on earth does that mean)? Is your architecture any good? Should you use a different one? What about Design Patterns? These were some of the questions I've had when I started, and answering them helped me to step up to a professional level. Which is why I have written these series SOLID as a Rock design principle. Liskov's Substitution Principle in C++ is the second principle in this series which I will discuss here.

/!\: This article has been originally published on my blog. If you are interested in receiving my latest articles, please sign up to my newsletter.

By the way, If you haven't gone through my previous articles on design principles, then below is the quick links:

  1. SRP -- Single Responsibility Principle
  2. OCP -- Open/Closed Principle
  3. LSP -- Liskov Substitution Principle
  4. ISP -- Interface Segregation Principle
  5. DIP -- Dependency Inversion Principle

The code snippets you see throughout this series of articles are simplified not sophisticated. So you often see me not using keywords like override, final, public(while inheritance) just to make code compact & consumable(most of the time) in single standard screen size. I also prefer struct instead of class just to save line by not writing "public:" sometimes and also miss virtual destructor, constructor, copy constructor, prefix std::, deleting dynamic memory, intentionally. I also consider myself a pragmatic person who wants to convey an idea in the simplest way possible rather than the standard way or using Jargons.

Note:

  • If you stumbled here directly, then I would suggest you go through What is design pattern? first, even if it is trivial. I believe it will encourage you to explore more on this topic.
  • All of this code you encounter in this series of articles are compiled using C++20(though I have used Modern C++ features up to C++17 in most cases). So if you don't have access to the latest compiler you can use https://wandbox.org/ which has preinstalled boost library as well.

Intent

Subtypes must be substitutable for their base types without altering the correctness of the program

  • If I address this in the context of C++, this literally means that functions that use pointers/references to base classes must be able to substitute by its derived classes.
  • The Liskov Substitution Principle revolves around ensuring that inheritance is used correctly.

Violating the Liskov's Substitution Principle

  • A great & traditional example illustrating LSP was how sometimes something that sounds right in natural language doesn't quite work in code.
  • In mathematics, 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 behaviour as follows:
struct Rectangle {
    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; }

protected:
    uint32_t m_width, m_height;
};

struct Square : Rectangle {
    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;
}
Enter fullscreen mode Exit fullscreen mode
  • As you can see above, we have violated Liskovs's Substitution Principle in the void process(Rectangle &r) function. Therefore Square is not a valid substitute of Rectangle.
  • If you see from the design perspective, the very idea of inheriting Square from Rectangle is not a good idea. Because Square does not have height & width, rather it has the size/length of sides.

Liskov's Substitution Principle Example

Not so good

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());
}
Enter fullscreen mode Exit fullscreen mode
  • A common code smell that frequently indicates an LSP violation is the presence of type checking code within a code block that is polymorphic.
  • For instance, if you have a std::for_each loop over a collection of objects of type Foo, and within this loop, there is a check to see if Foo is in fact Bar(a subtype of Foo), then this is almost certainly an LSP violation. Rather you should ensure Bar is in all ways substitutable for Foo, there should be no need to include such a check.

An OK way to do it

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());
}
Enter fullscreen mode Exit fullscreen mode
  • 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.

Use proper inheritance hierarchy

struct Shape {
    virtual uint32_t area() const = 0;
};

struct Rectangle : Shape {
    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;
};

struct Square : Shape {
    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()
}
Enter fullscreen mode Exit fullscreen mode

With Factory Pattern

struct ShapeFactory {
    static Shape CreateRectangle(uint32_t width, uint32_t height);
    static Shape CreateSquare(uint32_t size);
};
Enter fullscreen mode Exit fullscreen mode

Benefits of Liskov's Substitution Principle

=> Compatibility

  • It enables the binary compatibility between multiple releases & patches. In other words, It keeps the client code away from being impacted.

=> Type Safety

  • It's the easiest approach to handle type safety with inheritance, as types are not allowed to vary when inheriting.

=> Maintainability

  • Code that adheres to LSP is loosely dependent on each other & encourages code reusability.
  • Code that adheres to the LSP is code that makes the right abstractions.

Yardstick to Craft Liskov's Substitution Principle Friendly Software

  • In most introductions to object-oriented programming, inheritance discussed as an "IS-A" relationship with the inherited object. However, this is necessary, but not sufficient. It is more appropriate to say that one object can be designed to inherit from another if it always has an "IS-SUBSTITUTABLE-FOR" relationship with the inherited object.
  • The whole point of using an abstract base class is so that, in the future, you can write a new subclass & insert it into existing, working, tested code. A noble goal, but how to achieve it? First, start with decomposing your problem space --- domain. Second, express your contract/interfaces/virtual-methods in plain English.

Closing Notes

Don't get me wrong, I like SOLID and the approaches it promotes. But it's just a shape of deeper principles lying in its foundation. The examples above made it clear what this principle is striving for i.e. loose coupling & ensuring correct inheritance.

Now, go out there and make your subclasses swappable, and thank Dr. Barbara Liskov for such a useful principle.

Have Any Suggestions, Query or Wants to Say Hi? Take the Pressure Off, You Are Just a Click Away.🖱️

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .