SOLID Principles in Python - Part 2

·

4 min read

In Part 1, I wrote about the first two principles of SOLID. Part 2 is going to be about the third and fourth principles of SOLID which are the Liskov Substitution Principle and Interface Segregation Principle. Let's start!

Liskov Substitution Principle

The Liskov substitution principle (LSP) is called strong behavioral subtyping, which was initially introduced by Barbara Liskov in a 1987 conference keynote. It states that a class can be replaced by a sub-class without breaking the program. This means that subclasses should be behaving almost the same as their parent class.

Assume that we need to register students in a variety of courses; math, chemistry, physics etc. We could create a superclass RegisterCourse to define the blueprint for the course registry. Per course, we can create subclasses such as RegisterMath overrides the register method per course. But, superclass defines the input argument and a draft behavior. That's why it is called a blueprint.

But assume that, we need to register students also for the Computer Science course. For this course, the requirement is different; GitHub account instead of student_name. We can create classes as following.

Bad Example

from abc import ABC, abstractmethod

class RegisterCourse(ABC):
    def register(self, student_name):
        pass

class RegisterMath(RegisterCourse):
    def register(self, student_name):
        print(f"Student with name: {student_name} is registered to math course.")

class RegisterChemistry(RegisterCourse):
    def register(self, student_name):
        print(f"Student with name: {student_name} is registered to chemistry course.")

class RegisterComputerScience(RegisterCourse):
    def register(self, github_account):
        print(f"Student with github account: {github_account} is registered to computer science course.")

math_registerer = RegisterMath()
math_registerer.register("Enis")
computerscience_registerer = RegisterComputerScience()
computerscience_registerer.register("enis@github.com")

Is this correct? No. We modified the blueprint given by RegisterCourse superclass by modifying the input argument type. Registering for a computer science course requires a different input which deserves to be in a different superclass. Let's create a superclass for this; RegisterCourseWithGithubAccount. The nice thing now is that any computer science related course can be introduced as a subclass of RegisterCourseWithGithubAccount.

Good Example

from abc import ABC, abstractmethod

class RegisterCourse(ABC):
    def register(self, student_name):
        pass

class RegisterMath(RegisterCourse):
    def register(self, student_name):
        print(f"Student with name: {student_name} is registered to math course.")

class RegisterChemistry(RegisterCourse):
    def register(self, student_name):
        print(f"Student with name: {student_name} is registered to chemistry course.")


class RegisterCourseWithGithubAccount(ABC):
    def register(self, github_account):
        pass

class RegisterComputerScience(RegisterCourseWithGithubAccount):
    def register(self, github_account):
        print(f"Student with github account: {github_account} is registered to computer science course.")


math_registerer = RegisterMath()
math_registerer.register("Enis")
computerscience_registerer = RegisterComputerScience()
computerscience_registerer.register("enis@github.com")

This way, we do not change the behavior of the inherited method which is the underlying message in LSP. This was the third principle of SOLID. Now, let's have a look at the fourth principle.

Interface Segregation Principle

The interface Segregation Principle (ISP) states that sub-classes cannot be forced to depend on methods that they do not use. When we see a bulky interface that some of its methods are abundant for the client, then it is a bad smell that this principle is violated. Let's have a look at the following example for this.

Bad Example

In this example, we allocated the courses inside RegisterCourses class where FirstYearStudent class can inherit these courses as a subclass. It is the same for SecondYearStudent with one difference. Second Year students can register for both courses, however, first year students cannot register for the Advanced Algorithms course because it is only open from second year onwards.

from abc import ABC, abstractmethod

class RegisterCourses(ABC):
    def RegisterIntroductionToComputerScience(self):
        pass

    def RegisterAdvancedAlgorithms(self):
        pass

class FirstYearStudent(RegisterCourses):
    def RegisterIntroductionToComputerScience(self):
        print("Register to Introduction to Computer Science Course")

    def RegisterAdvancedAlgorithms(self):
        raise Exception("Registery to Advanced Algorithms Course is not possible.")

class SecondYearStudent(RegisterCourses):
    def RegisterIntroductionToComputerScience(self):
        print("Register to Introduction to Computer Science Course")

    def RegisterAdvancedAlgorithms(self):
        print("Register to Advanced Algorithms Course")

The problem here is that FirstYearStudent class inherits RegisterAdvancedAlgorithms method which is not in use. Therefore, it needs to handle the method by raising an exception error. This violates the ISP principle. Instead, we can divide the superclass RegisterCourses into two classes; RegisterBeginnerCoursesand RegisterAdvanceCourses.

Good Example

from abc import ABC, abstractmethod

class RegisterBeginnerCourses(ABC):
    def RegisterIntroductionToComputerScience(self):
        pass

class RegisterAdvanceCourses(ABC):
    def RegisterAdvancedAlgorithms(self):
        pass

This way we can have two distinct and smaller classes. FirstYearStudent can directly be a subclass of RegisterBeginnerCourses. SecondYearStudent can be a subclass of both RegisterBeginnerCourses and RegisterAdvanceCourses superclasses. This is possible in Python and is called multiple inheritance. This way, we do not have to handle exceptions for inherited methods that are not used. It looks way more clean and neat.

class FirstYearStudent(RegisterBeginnerCourses):
    def RegisterIntroductionToComputerScience(self):
        print("Register to Introduction to Computer Science Course")

class SecondYearStudent(RegisterBeginnerCourses, RegisterAdvanceCourses):
    def RegisterIntroductionToComputerScience(self):
        print("Register to Introduction to Computer Science Course")

    def RegisterAdvancedAlgorithms(self):
        print("Register to Advanced Algorithms Course")

RECAP

This was the fourth principle of SOLID. I tried to keep the examples as short as possible to focus on the concepts. I hope you enjoyed reading this. If this kind of stuff is interesting to you, you might also enjoy my write-up about the first two principles of SOLID. Stay tuned for my last write-up! It is going to be about the last principle of SOLID: Dependency Inversion. Coming soon!