DEV Community

Ankit Kumar
Ankit Kumar

Posted on • Edited on

SOLID Principle in Python

The SOLID Principles are five principles of Object-Oriented class design. They are a set of rules for certain design patterns and software architecture in general.

  • S - Single responsibility principle
  • O - Open-Closed principle
  • L - Liskov substitution principle
  • I - Interface segregation principle
  • D - Dependency inversion principle

Single responsibility principle

A class should have only one job. If a class has more than one responsibility,
it becomes coupled. A change to one responsibility results to modification of
the other responsibility.

class FileReader:
    def __init__(self, filename):
        self.filename = filename

    def read_file(self):
        with open(self.filename, 'r') as file:
            data = file.read()
        return self.parse_data(data)

    def parse_data(self, data):
        # Parsing logic here
        return parsed_data
Enter fullscreen mode Exit fullscreen mode

Above example, the FileReader class is violating the Single Responsibility Principle because it has two responsibilities: reading data from a file and parsing the data.

To fix this, we can create a separate class for parsing the data:

class FileReader:
    def __init__(self, filename):
        self.filename = filename

    def read_file(self):
        with open(self.filename, 'r') as file:
            data = file.read()
        return data

class DataParser:
    def parse_data(self, data):
        # Parsing logic here
        return parsed_data
Enter fullscreen mode Exit fullscreen mode

Open-Closed principle

Software entities (Classes, modules, functions) should be open for extension, not for modification.

class Animal:
    def __init__(self, name: str):
        self.name = name

    def get_name(self) -> str:
        pass

animals = [
    Animal('lion'),
    Animal('mouse')
]

def animal_sound(animals: list):
    for animal in animals:
        if animal.name == 'lion':
            print('roar')

        elif animal.name == 'mouse':
            print('squeak')

animal_sound(animals)
Enter fullscreen mode Exit fullscreen mode

Above example function animal_sound does not conform to the open-closed principle because it cannot be closed against new kinds of animals. If we add a new animal . We have to modify the animal_sound function.

To fix this we can create a new class for every animal which extend Animal and implement make_sound method.

class Animal:
    def __init__(self, name: str):
        self.name = name

    def get_name(self) -> str:
        pass

    def make_sound(self):
        pass


class Lion(Animal):
    def make_sound(self):
        return 'roar'


class Snake(Animal):
    def make_sound(self):
        return 'hiss'


def animal_sound(animals: list):
    for animal in animals:
        print(animal.make_sound())

animal_sound(animals)
Enter fullscreen mode Exit fullscreen mode

Now, if we add a new animal, animal_sound doesn’t need to change. All we need to do is add the new animal to the animal array.


Liskov substitution principle

The Liskov Substitution Principle is a design principle that states that objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program.

class Animal:
    def make_sound(self):
        pass

class Dog(Animal):
    def make_sound(self):
        return "Woof!"

class Cat(Animal):
    def make_sound(self):
        return "Meow!"

def animal_sound(animal):
    return animal.make_sound()

dog = Dog()
cat = Cat()

print(animal_sound(dog)) # Output: "Woof!"
print(animal_sound(cat)) # Output: "Meow!"
Enter fullscreen mode Exit fullscreen mode

Dog or a Cat object to the animal_sound function without affecting the correctness of the program. This is because both Dog and Cat are subtypes of Animal, and they implement the same make_sound method in their own way.


Interface segregation principle

The Interface Segregation Principle (ISP) is a design principle that states that no client should be forced to depend on methods it does not use. In other words, we should break interfaces into smaller, more specific interfaces so that clients only need to implement the methods they actually use.

class IAnimal:
    def eat(self):
        pass

    def sleep(self):
        pass

    def make_sound(self):
        pass

class Dog(IAnimal):
    def eat(self):
        return "Dog is eating."

    def sleep(self):
        return "Dog is sleeping."

    def make_sound(self):
        return "Woof!"

class Cat(IAnimal):
    def eat(self):
        return "Cat is eating."

    def sleep(self):
        return "Cat is sleeping."

    def make_sound(self):
        return "Meow!"
Enter fullscreen mode Exit fullscreen mode

In above example class is forcing its clients to implement all three methods of the Animal class, even if they only need to use one or two methods for training.

To fix this, we can break down the Animal interface into smaller, more specific interfaces:

class IEatable:
    def eat(self):
        pass

class ISleepable:
    def sleep(self):
        pass

class IMakeSoundable:
    def make_sound(self):
        pass

class Dog(IEatable, IMakeSoundable):
    def eat(self):
        return "Dog is eating."

    def make_sound(self):
       return "Woof!"

class Cat(IEatable, ISleepable, IMakeSoundable):
    def eat(self):
        return "Cat is eating."

    def sleep(self):
        return "Cat is sleeping."

    def make_sound(self):
        return "Meow!"
Enter fullscreen mode Exit fullscreen mode

we have broken down the Animal interface into three smaller interfaces - IEatable, ISleepable, and IMakeSoundable. The Dog and Cat classes now implement these smaller interfaces instead of the larger Animal interface.


Dependency inversion principle

The Dependency Inversion Principle (DIP) is a design principle that states that high-level modules should not depend on low-level modules, but both should depend on abstractions. This means that instead of depending on concrete implementations, we should depend on abstract interfaces or classes.

Here's a simple example in Python to illustrate the Dependency Inversion Principle:

class IA:
    def get(self):
        pass

class A(IA):
    def __init__(self):
        self.a=[1,2,3,4]

    def get(self):
        return self.a


class B:
    def __init__(self, obj:IA):
        self.obj=obj

    def get(self):
        print(self.obj.get())

class C(IA):
    def __init__(self):
        self.b=[1,2,3]
    def get(self):
        return self.b

a=A()
c=C()
b1=B(c) # output : [1,2,3]
b2=B(a) # output : [1,2,3,4]
b.get()

Enter fullscreen mode Exit fullscreen mode

we have introduced an abstract interface called ILightSource that defines the turn_on and turn_off methods that the LightSwitch class needs to use. The LightBulb class now implements this interface instead of having its own methods.

References

GeeksforGeeks
freecodecamp

Top comments (0)