Today, we discuss principle #3 of SOLID principles to help write clean code in Python.
Here are the first and second principles if you missed them. Let me know in the comments in case you have any questions.
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:
<img src=“https://drive.google.com/uc?export=view&id=1q-mdEhhFjA5_XYLIkLnV6W0HJ2EcJZhW”,alt=“ISettings class with two methods: get_settings() and set_settings(). Three classes inherit from it: UserSettings, SectionSettings, and GlobalSettings.",width=“50%">
Classes for a customizable portal application (Designed by Plantuml)
In the UML diagram above, the ISettings class is an interface defining
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
SectionSettings , and
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
encapsulates the logic of retrieving and saving the settings for all
types of settings. Look at this class in the figure below:
<img src=“https://drive.google.com/uc?export=view&id=1tR0ZuKCTMVtRsnqS3y8u2mvRk7GRtSyj”,alt=“SettingsHelper class with two methods: get_all_settings() and set_all_settings().",width=“50%">
SettingsHelper class. (Designed by Plantuml)
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
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
set_settings() will be called.
See the pseudo-code below:
# item is an ISettings object # inside get_all_settings() for item in items: item.get_settings() # inside set_all_settings() for item in items: item.set_settings(values)
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:
def set_settings(settings: Dict(str, str)): raise NotImplementedError()
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:
<img src=“https://drive.google.com/uc?export=view&id=1S6tt_B1Otf5ceLJNNfXMLMQ2s_qrKkOt”,alt=“Two interfaces: IWritableSettings with set_settings() method, and IReadableSettings with get_settings() method. There are four classes: UserSettings, SectionSettings, GlobalSettings, and GuestSettings. All of them inherit from IReadableSettings interface while all of them except GuestSettings inherit from the IWritableSettings interface.",width=“50%">
Modified class design for the portal application. (Designed by Plantuml)
Before refactoring, we had
ISettings interface with two methods:
set_settings() . We then said that we won’t
set_settings() method in
That means, these two methods are split into two separate interfaces:
IWritableSettings . The
IReadableSettings interface has only
Notice that all classes:
UserSettings , and
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