SOLID Principle in Python
Tags: webdev, beginners, programming, tutorial
Published: 2023-05-14 12:35:14

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
```
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
```
---

###  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)
```
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)
```
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!"
```
`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!"
```
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!"
```
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()

```
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](https://www.geeksforgeeks.org/solid-principle-in-programming-understand-with-real-life-examples/)
[freecodecamp](https://www.freecodecamp.org/news/solid-principles-explained-in-plain-english/)