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:
Definition:
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
two methods, get_settings()
and set_settings()
.
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 ISettings
interface: GlobalSettings
, SectionSettings
, and UserSettings
.
GlobalSettings
class: Used to retrieve and save global portal settings such as the title, theme, and communication.SectionSettings
class: Used to reflect on the individual sections of the portal and customize their appearance and placement on the page.UserSettings
class: 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:
<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)
The SettingsHelper
class consists of two methods: get_all_settings()
and set_all_settings()
where 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 set_settings()
.
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 get_settings()
or 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?
By implementing 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 ISettings
( 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
IReadableSettings
and IWritableSettings
.
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: get_settings()
and set_settings()
. We then said that we won’t
implement set_settings()
method in GuestSettings
class.
That means, these two methods are split into two separate interfaces: IReadableSettings
and IWritableSettings
. The IReadableSettings
interface has only get_settings()
while IWritableSettings
has
only set_settings()
.
Notice that all classes: GlobalSettings
, SectionSettings
, UserSettings
, and GuestSettings
inherit from IReadableSettings
interface.
On the other hand, all of them except GuestSettings
inherit from IWritableSettings
interface.
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 IWritableSettings
objects.
Now, this design conforms to LSP, because objects that we derived from base classes are substituted correctly.
Final thoughts:
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 :)
Credit
- Beginning SOLID Principles and Design Patterns for ASP.NET Developers by Bipin Joshi