Today, we discuss principle #3 of SOLID principles to help write clean code in Python.
And let's dive into the third design principle (the 'L' in SOLID).
Here, we explain the concept with a real example implemented in Python and shown in a UML diagram to better illustrate the design of the classes and the relationships between them and have a visual understanding of what's going on.
Let's start with what it states in a simple sentence:
Derived classes should be substitutable for their base classes.
The Liskov Substitution principle was named after Barbara Liskov. More simply put, it states that any subtype class should be 100% compatible with its base class. That is to say, an object of type Parent should take an object of type Child without breaking anything.
You can think of it this way, when you make a base class you must make sure that you abstract it enough to apply polymorphism to it. In other words, make sure that there might be some other classes (of different types) that can inherit from this base class.
Let's see LSP in action through the following practical example.
Example: Customizing App Settings
Assume you are developing a big portal. It's required to provide some customizations to the end-users. The customization varies from one level to another across the system, such as global-level customization, section-level customization, and user-specific customization.
When you consider the previous requirement, you arrive at this design:
In the UML diagram above, the ISettings class is an interface defining two methods,
When these methods are implemented, they are used to customize the portal settings to retrieve them from the database and save them to the database respectively.
Three classes then implement
GlobalSettingsclass: Used to retrieve and save global portal settings such as the title, theme, and communication.
SectionSettingsclass: Used to reflect on the individual sections of the portal and customize their appearance and placement on the page.
UserSettingsclass: Used to customize the portal for a specific user, such as e-mail, language, notification preferences, and time zone.
Further, let's assume that you create a class
SettingsHelper that encapsulates the logic of retrieving and saving the settings for all types of settings. Look at this class in the figure below:
SettingsHelper class consists of two methods:
set_all_settings() accepts a list of objects implementing the
ISettings interface. Inside that method, two things are done: retrieving the settings using
get_settings() and setting the settings using
Although we won't go into the exact code of these two methods when implemented, it is obvious that both methods will have a loop. With every iteration
set_settings() will be called. See the pseudo-code below:
So far so good. This design seems fine and working as expected. Now, suppose that the product owner requests a new feature to support guest users.
What's the difference then?
The difference is that the guest users do certain things that the registered users do not. For example, they can't view the private sections of the portal. They can't save any customization settings or change their preferences.
Like KDnuggets, when I wrote a piece of a guest blog post there I wasn't able to set any settings there. I just handed it over to the owner and he was able to set the settings himself.
So to incorporate these changes, we need to create a new class
GuestSettings that is supposed to only get settings not to save anything to the database.
In this case,
set_settings() method won't be implemented. So that method will look like:
But here we introduced a problem. Can you guess what it is?
GuestSettings class to
ISettings, we cause
SettingsHelper to break. When
set_all_settings() method is called, there will be another call to
set_settings() inside the loop and will raise an exception.
The reason for this problem is that a type implementing the base class
GuestSettings in this case) violates the Liskov Substitution Principle by breaking the application (through throwing an exception in our case).
How to refactor to adhere to LSP
To refactor such a design, we need to break
ISettings class into two interfaces: one to let the user read and the other to write. Let them be
So the modified design would be:
Before refactoring, we had
ISettings interface with two methods:
set_settings(). We then said that we won't implement
set_settings() method in
That means, these two methods are split into two separate interfaces:
IReadableSettings interface has only
IWritableSettings has only
Notice that all classes:
GuestSettings inherit from
On the other hand, all of them except
GuestSettings inherit from
Now, for the
SettingsHelper class to work, we need to implement
get_all_settings() differently because now it accepts a list of
IReadableSettings objects whereas
set_all_settings() will similary accept a list of
Now, this design conforms to LSP, because objects that we derived from base classes are substituted correctly.
The Liskov Substitution principle is applied when you have derived classes that are good substitutes for their base classes. Good substitutes are the classes that are compatible with their base classes as we say that
GuestSettings is a good substitute for
IReadableSettings not for
ISettings; the old interface.
See you in principle #4 :)
- Beginning SOLID Principles and Design Patterns for ASP.NET Developers by Bipin Joshi
Articles in this series: