OOP: Liskov’s Substitution principle
Summary
- Introduction
- Liskov’s Substitution Principle
- The Problem with Square inheriting from Rectangle
- Examples of LSP Violation
- How to improve the Square/Rectangle/Shape hierarchy
- Conclusion
Introduction
Today in my Holberton School cursus, while learning C#, I encountered a new hurdle: Liskov’s Substitution Principle.
Here’s the context: I had assignments about creating a class Shape, a class Rectangle and a class Square.
Rectangle inherits from Shape and Square inherits from Rectangle.
Square has a field and property size. When it’s size is modified, it sets the height and width of the rectangle itself.
However, after writing that assignment, I realized that I could still access the properties width and height of rectangle from an instance of square, which allowed for a square to have uneven sides length. There, I heard of Liskov’s Substitution Principle.
Liskov’s Substitution Principle (LSP)
What is it?
Liskov’s Substitution Principle, or LSP is one of the five SOLID principles of Object-Oriented Programming and Design. Introduced by Barbara Lisk in 1987, it is one of the core guidelines for designing class hierarchies and maintaining a coherence in the behavior of objects when using them interchangeably in a program.
The principle is stated as follows:
“Subtypes must be substitutable for their base types without affecting the correctness of the program.”
To put it in other terms, if you have a base class, and a class that derives from it, any instance of the derived class should be able to replace instances of the base class without causing any unexpected behavior or violating the rules from the base class.
Multiple conditions need to be satisfied for your program to adhere to LSP:
- Behavior Preservation
The class derived from a base class shouldn’t override or change the methods it inherits in a way that contradicts the base class’s functionalities. Any code relying on the base class’s behavior should continue to work as expected when using classes that derived from it.
- Preconditions Strengthening
The input requirements for methods overriden in the child class should not be more restrictive than those from the parent class. The child class should respect the same inputs as the parent class without causing errors.
- PostConditions Weakening
The actions produced by a method from the child class must not be less than those from the parent class. For example a ToString method from the parent class must return a string representation of the class, and if the child class overrides it, it must at least do the same.
- Invariant Preservation
Any condition or properties that should be true throughout the lifetime of an object must me maintained by it’s derived classes. Example: a Rectangle’s angles are all but right angles.
The Problem with Square inheriting from Rectangle
The concept at first looks pretty intuitive: A Square is a Rectangle, but a rectangle is not a square. So it would make sense to have the Square class derive from the Rectangle class.
However, the fact is that a square has one less property than Rectangle: it only has one dimension for all of it’s sides while a Rectangle has two.
This means that if I make Square inherit from Rectangle and access its base class’s width and height, I can end up with a square that has different width and height. This breaks Preconditions Strengthening as well as Invariant Preservation since it breaks the conditions for a shape to be a square by weakening the inputs of its base class.
How to improve the Square/Rectangle/Shape hierarchy
The simplest solution is to just detach the square class from the rectangle class, and have both derive from a Shape class.
How does that help? Since Shape class has a small number of properties, methods and conditions to respect, this means that the implementation of a Square and a Rectangle will be able to be used interchangeably with Shape without much work.
This way of respecting LSP is just by using a more abstract base class, limiting the amount of actions that need to be done in order for the hierarchy to work properly.
//code example here
Alternative
Another way to improve the hierarchy would be by making sure that the properties of the Rectangle class are properly implemented by Square, but that ends up being more work to do, and it would be less and less practical depending on the properties of the parent class.
Conclusion
In wrapping up our exploration of the Liskov Substitution Principle (LSP), it’s clear that this principle is like a golden rule for building trustworthy and adaptable software. Just like puzzle pieces that fit perfectly together, LSP ensures that new or specialized parts of our code can replace the old ones without causing surprises.
By understanding and following LSP, we prevent those “uh-oh” moments where a supposedly new and improved class doesn’t quite fit in with the rest. This principle encourages us to design classes that not only share qualities but also play by the same rules, making sure everything runs smoothly no matter what shape it takes.
Remember, LSP isn’t just about avoiding headaches today — it’s about building a strong foundation for the future. So as we write code, let’s keep LSP in mind, creating software that’s not only functional but also flexible and ready for whatever comes next.