Solid Principles in Python
Tags: python, beginners, programming
Published: 2023-02-20 07:14:21

## The SOLID Principles:
* The Single-Responsibility Principle (SRP)
* The Open-Closed Principle (OCP)
* The Liskov Substitution Principle (LSP)
* The Interface Segregation Principle (ISP)
* The Dependency inversion Principle (DIP)

### The Single-Responsibility Principle (SRP)
* This principle state that the Class should have only one responsibility.
* Code That violates **SRP** is:
```py
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def greet(self):
        print(f"Hello, my name is {self.name} and I'm {self.age} years old.")

    def celebrate_birthday(self):
        self.age += 1
        print(f"{self.name} is now {self.age} years old!")
```

* In the above code, the Person class have two responsibility one is to greet and another one is to celebrate a birthday to follow the **SRP** create a different class one which handles greeting and the other handles birthday celebration.
```py
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def greet(self):
        print(f"Hello, my name is {self.name} and I'm {self.age} years old.")

class BirthdayCelebrator:
    def celebrate_birthday(self, person):
        person.age += 1
        print(f"{person.name} is now {person.age} years old!")
```

### The Open-Closed Principle (OCP)
* This principle states that the class should be closed for modification but open for extension.
* Code that violates **OCP** is:
```py
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.14 * self.radius ** 2

class Triangle(Shape):
    def __init__(self, base, height):
        self.base = base
        self.height = height

    def area(self):
        return 0.5 * self.base * self.height

class AreaCalculator:
    def calculate_area(self, shape):
        if isinstance(shape, Rectangle):
            return shape.area()
        elif isinstance(shape, Circle):
            return shape.area()
        elif isinstance(shape, Triangle):
            return shape.area()
```
* In the above code if we try to create a new class such as a hexagon we need to modify the AreaCalculator class which will violate **OCP**.
* To correct the code we just need to change the method of AreaCalculator class so there will be no need to modify AreaCalculator class.
```py
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.14 * self.radius ** 2

class Triangle(Shape):
    def __init__(self, base, height):
        self.base = base
        self.height = heigh.t

    def area(self):
        return 0.5 * self.base * self.height

class AreaCalculator:
    def calculate_area(self, shape):
        return shape.area()
```


### Liskov’s Substitution Principle (LSP)
* This principle states that the object created using the child class can replace the object created using The parent class.
* Code that violates **LSP** is:
```py
class Bird:
    def fly(self):
        print("The bird is flying")

class Penguin(Bird):
    def fly(self):
        raise Exception("I can't fly")

def make_bird_fly(bird):
    bird.fly()

bird = Bird()
make_bird_fly(bird)  # Output: The bird is flying

penguin = Penguin()
make_bird_fly(penguin)  # Output: An exception is raised
```
* In the above code, the penguin will raise the error when we call the fly method. To correct it simply do method ovveridding.
```py
class Bird:
    def fly(self):
        print("The bird is flying")

class Penguin(Bird):
    def fly(self):
        print("I can't fly")

def make_bird_fly(bird):
    bird.fly()

bird = Bird()
make_bird_fly(bird)  # Output: The bird is flying

penguin = Penguin()
make_bird_fly(penguin) # output: I can't fly
``` 

### Interface Segregation Principle (ISP)
* This principle states that it is better to have many small, specific interfaces, rather than one large, general-purpose interface.
* Code that violates **ISP** is:
```py
from abc import ABC, abstractmethod


class Animal(ABC):
    @abstractmethod
    def eat(self):
        pass
    @abstractmethod
    def sleep(self):
        pass
    @abstractmethod
    def move(self):
        pass

class Dog(Animal):
    def eat(self):
        print("The dog is eating")

    def sleep(self):
        print("The dog is sleeping")

    def move(self):
        print("The dog is running")
```
* Above code violates the **ISP** as if we try to create a fish class using an animal class but it doesn't have ability to move so we need to create methods which are common in all animals such as eating, sleep, and reproduction. 
* Code that adheres **ISP** is:

```py
from abc import ABC, abstractmethod


class Animal(ABC):
    @abstractmethod
    def eat(self):
        pass
    @abstractmethod
    def sleep(self):
        pass
    @abstractmethod
    def reproduce(self):
        pass    
```

* above method is common in all animals hence it is the simplest interface for animals and another method will be added according to other animals in their class.

### Dependency Inversion Principle (DIP)
* This principle state that Entities must depend on abstractions, not on concretions. It states that the high-level module must not depend on the low-level module, but they should depend on abstractions.
* Code that violates **DIP** is:
```py
class Shape:
    def area(self):
        pass

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.14 * self.radius ** 2

class Triangle(Shape):
    def __init__(self, base, height):
        self.base = base
        self.height = heigh.t

    def area(self):
        return 0.5 * self.base * self.height

class AreaCalculator:
    def calculate_area(self, shape):
        return shape.area()
```
* Code that adheres **DIP** is:
```py  
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.14 * self.radius ** 2

class Triangle(Shape):
    def __init__(self, base, height):
        self.base = base
        self.height = heigh.t

    def area(self):
        return 0.5 * self.base * self.height

class AreaCalculator:
    def calculate_area(self, shape):
        return shape.area()
```
## References
* Geek For Geeks. "SOLID Principle in Programming: Understand With Real Life Examples". Retrieved from https://www.geeksforgeeks.org/solid-principle-in-programming-understand-with-real-life-examples/
* Geek For Geeks. "OOD Principles | SOLID". Retrieved from https://www.geeksforgeeks.org/ood-principles-solid/