Principle 3 | How to Write Clean Code (in Python) With SOLID Principles

Created on Jan 2, 2022
Updated on Jan 8, 2022

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 .

  1. GlobalSettings class: Used to retrieve and save global portal settings such as the title, theme, and communication.
  2. SectionSettings class: Used to reflect on the individual sections of the portal and customize their appearance and placement on the page.
  3. 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