Code360 powered by Coding Ninjas X Naukri.com. Code360 powered by Coding Ninjas X Naukri.com
Last Updated: Mar 27, 2024
Difficulty: Medium

Liskov Substitution Principle in System Design

Leveraging ChatGPT - GenAI as a Microsoft Data Expert
Speaker
Prerita Agarwal
Data Specialist @
23 Jul, 2024 @ 01:30 PM

Introduction

Imagine you are planning a road trip with your friends. You have decided to rent a car for the journey, and you have set your sights on a sleek sports car. However, as the rental agency doesn't have any sports cars available, you are offered a compact sedan as an alternative.

Now, suppose that you go ahead and rent the sedan, but when you hit the highway, you notice that the car is not as responsive and agile as you had hoped. You struggle to maintain a consistent speed and find it challenging to navigate tight corners, making the journey less enjoyable than you had anticipated.

This situation is a real-world example of violating the Liskov Substitution Principle (LSP). The LSP is one of the SOLID principles of object-oriented design that states that if a program is using a base class, it should be able to use any of its derived classes without knowing it.

In other words, any subclass should be able to replace its parent class without affecting the correctness of the program. However, in our road trip scenario, the sedan (subclass) could not adequately replace the sports car (parent class) as it did not meet the user's expectations.

Liskov Substitution Principle in System Design

In this blog, we will dive deeper into the Liskov Substitution Principle and explore real-world examples of how it can be violated and how to apply it correctly in our software design. We will also discuss the benefits of following this principle, including improved code reusability, maintainability, and extensibility. 

So, fasten your seatbelt, and let's hit the road to learn more about the Liskov Substitution Principle.

Liskov Substitution Principle Violation in C++: An Example with Shape Hierarchy

Consider an example in C++ to demonstrate a violation of the Liskov Substitution Principle.

Suppose we have a class hierarchy for shapes that includes a base class ‘Shape’ and two subclasses ‘Rectangle’ and ‘Square’.

class Shape {
public:
  virtual int area() = 0;
};

class Rectangle : public Shape {
public:
  int width;
  int height;
  int area() override {
      return width * height;
  }
};

class Square : public Shape {
public:
  int side;
  int area() override {
      return side * side;
  }
};


Now suppose we have a function that takes an object of type ‘Shape’ and calculates its area:

void printArea(Shape* shape) {
    std::cout << "Area: " << shape->area() << std::endl;
}

This function takes an object of type ‘Shape’ as a parameter and calls the ‘area()’ function to calculate the area of the shape.

However, if we create a new subclass of ‘Rectangle’ called ‘SquareRectangle’, which has a different implementation of the ‘area()’ function:

class SquareRectangle : public Rectangle {
public:
  int area() override {
      return width * width;
  }
};

This implementation calculates the area of a ‘SquareRectangle’ object using the width instead of the width and height. This violates the Liskov Substitution Principle because a ‘SquareRectangle’ object cannot be substituted for a ‘Rectangle’ object in the ‘printArea()’ function without causing errors.

For example, consider the following code:

Rectangle* rectangle = new Rectangle();
printArea(rectangle); // prints "Area: 20"

SquareRectangle* squareRectangle = new SquareRectangle();
printArea(squareRectangle); // prints "Area: 16"

The ‘printArea()’ function produces different results when called with a ‘SquareRectangle’ object instead of a ‘Rectangle’ object, which violates the Liskov Substitution Principle.

Get the tech career you deserve, faster!
Connect with our expert counsellors to understand how to hack your way to success
User rating 4.7/5
1:1 doubt support
95% placement record
Akash Pal
Senior Software Engineer
326% Hike After Job Bootcamp
Himanshu Gusain
Programmer Analyst
32 LPA After Job Bootcamp
After Job
Bootcamp

Fixing the Liskov Substitution Principle Violation in C++ with Shape Hierarchy

To fix the violation of the Liskov Substitution Principle in the example with the Shape hierarchy in C++, we need to ensure that any subclass of Rectangle behaves in the same way as Rectangle in the context of the printArea() function. 

One solution is to create a new base class that defines the interface for Rectangle and its subclasses:

class RectangleBase {
public:
  virtual int getWidth() = 0;
  virtual int getHeight() = 0;
};

class Shape : public RectangleBase {
public:
  virtual int area() = 0;
  int getWidth() override { return 0; }
  int getHeight() override { return 0; }
};

class Rectangle : public Shape {
public:
  int width;
  int height;
  int area() override {
      return width * height;
  }
  int getWidth() override { return width; }
  int getHeight() override { return height; }
};

class Square : public Shape {
public:
  int side;
  int area() override {
      return side * side;
  }
  int getWidth() override { return side; }
  int getHeight() override { return side; }
};

class SquareRectangle : public Rectangle {
public:
  int area() override {
      return getWidth() * getWidth();
  }

In this solution, we create a new base class RectangleBase that defines the interface for Rectangle and its subclasses. The Shape class now inherits from RectangleBase and implements the getWidth() and getHeight() functions with default implementations that return 0.

The Rectangle and Square classes inherit from Shape and override the getWidth() and getHeight() functions to return their respective dimensions.

The SquareRectangle class, which caused the Liskov Substitution Principle violation, now inherits from Rectangle and can no longer override the area() function. Instead, it uses the getWidth() function to calculate the area of the shape.

With this solution, we have fixed the violation of the Liskov Substitution Principle by ensuring that any subclass of Rectangle behaves in the same way as Rectangle in the context of the printArea() function.

Tips for Applying Liskov Substitution Principle

  1. Use inheritance to model "is-a" relationships: In C++, you can use inheritance to model relationships between classes. If a subclass "is-a" type of its superclass, then it can be used interchangeably with the superclass without changing the behavior of the code.
    For example, let's say you have a Shape class and two subclasses, Rectangle and Square. Since a Square "is-a" Rectangle, you can use a Square anywhere you would use a Rectangle, without changing the behavior of the code.
class Shape {
public:
  virtual int area() = 0;
};

class Rectangle : public Shape {
public:
  int width;
  int height;
  int area() override {
      return width * height;
  }
};

class Square : public Rectangle {
public:
  int side;
  int area() override {
      return side * side;
  }
};

 

2. Use virtual functions to provide a consistent interface: In C++, you can use virtual functions to define a consistent interface for a group of related classes. Each subclass can provide its own implementation of the virtual function, while still maintaining the same behavior as the superclass.
For example, in the Shape class above, we defined a virtual function area(). Both Rectangle and Square provide their own implementation of area(), but they still follow the same behavior as the Shape class.
 

3. Avoid breaking the contract of the superclass: When writing a subclass, you should make sure that it doesn't break any rules or assumptions made by the superclass. For example, if the superclass requires a function to return a positive number, then the subclass should also return a positive number.

For example, if we add a new Circle class, we should make sure that its area() function always returns a positive number, just like the other classes.

class Circle : public Shape {
public:
  int radius;
  int area() override {
      return 3.14 * radius * radius;
  }
};

By following these tips, we can use the Liskov Substitution Principle to create a group of related classes that can be used interchangeably, without causing any unexpected errors or bugs.

Advantages of Using Liskov Substitution Principle

Following are the advantages of using the Liskov Substitution Principle (LSP) in object-oriented programming:

  1. Increases code reusability: By following LSP, we can create a set of related classes that can be used interchangeably without modifying the code. This makes it easier to reuse code and can save us time when we're developing new features.
  2. Simplifies code maintenance: When we follow LSP, we can make changes to one class without affecting the behavior of other classes in the group. This is because the subclass will still behave exactly like the superclass, and any changes made to the superclass will not affect its behavior.
  3. Promotes modularity: When we use LSP to create a set of related classes, each class can be treated as a standalone module, which means it can be developed and tested independently of other classes. It promotes modularity, as it allows us to break down a complex system into smaller and more manageable parts.
  4. Enhances code quality: When we follow LSP, we can be sure that every class in the group behaves consistently. This means that each class will implement the same set of methods and have the same behavior as the parent class. This consistency can help reduce the chances of catching bugs and unexpected behavior, thereby improving the overall quality of our code.

Frequently Asked Questions

What does the Liskov Substitution Principle ensure?

The Liskov Substitution Principle ensures that a class hierarchy is well-designed and behaves in a predictable manner.

What is the purpose of the Liskov Substitution Principle?

The purpose of the Liskov Substitution Principle is to make it easy to add new types to a system without having to change existing code.

How can the Liskov Substitution Principle be applied in practice?

The Liskov Substitution Principle can be applied by designing class hierarchies that follow the "is-a" relationship, ensuring that derived classes can be used in place of their base classes, and testing the behavior of derived classes to ensure that they behave correctly in all situations.

Conclusion

In conclusion, the Liskov Substitution Principle (LSP) is an important principle in object-oriented programming that helps promote code reusability, simplifies code maintenance, promotes modularity, and enhances code quality.

In this article, we discussed an example of how LSP can be violated in a Shape hierarchy in C++, and how this violation can lead to unexpected behavior and bugs. We also provided a solution to fix the violation and ensure that the Shape hierarchy conforms to LSP. Additionally, we provided some tips for applying LSP in your own code and answered some frequently asked questions.

By following LSP and creating a set of related classes that work together, we can create more maintainable, reusable and modular code. Implementing LSP can help us improve the overall quality of our code and can be easier to maintain and improve over time.

We have a step-by-step guided path that will teach you all the important concepts you need to know for system design. You can also consider our System Design Course to give your career an edge over others.

We want to hear from you. What do you think of this article?

If you have any questions or if there is any aspect related to LSP that you feel we missed, please let us know by leaving a comment below.

Topics covered
1.
Introduction
2.
Liskov Substitution Principle Violation in C++: An Example with Shape Hierarchy
3.
Fixing the Liskov Substitution Principle Violation in C++ with Shape Hierarchy
4.
Tips for Applying Liskov Substitution Principle
5.
Advantages of Using Liskov Substitution Principle
6.
Frequently Asked Questions
6.1.
What does the Liskov Substitution Principle ensure?
6.2.
What is the purpose of the Liskov Substitution Principle?
6.3.
How can the Liskov Substitution Principle be applied in practice?
7.
Conclusion