SOLID Principles
Published: 2023-05-14 17:12:09



The acronym originated by **Robert C. Martin**, and it stands for five principles of Object-Oriented Programming. These are a set of rules for certain design patterns and software architecture.



The SOLID principles are:



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



> _“A class should have one, and only one, reason to change”_



- Every component of our code should have only one responsibility.



```python

class User:

    def __init__(self, name, email, password):

        self.name = name

        self.email = email

        self.password = password

    

    def login(self):

        # Some code to authenticate user

        return True

    

    def change_password(self, new_password):

        # Some code to change user's password

        return True

```



- Here, User class is responsible for authentication from login and password management through change_password method.

- This violates SRP because the class is responsible for two distinct tasks.



```python

class UserAuthenticator:

    def __init__(self, user):

        self.user = user

    

    def login(self):

        # Some code to authenticate user

        return True





class PasswordManager:

    def __init__(self, user):

        self.user = user

    

    def change_password(self, new_password):

        # Some code to change user's password

        return True

```

- Now we have separated the responsibilities, each class is now focused on a single task.

- This makes code easier to understand and maintain.



##  The Open–closed principle (OCP)



> “_Software entities … should be open for extension but closed for modification”_



- If we need to add a new functionality or method, we don't need to modify the class, rather we should be able to extend it.



```python

class checkShape:

    def handle_shape(self, shape):

        if request.type == 'Circle':

            # Handle request type Circle

        elif request.type == 'Rectangle':

            # Handle request type Rectangle

        elif request.type == 'Parallelogram':

            # Handle request type Parallelogram

        else:

            # Handle other request types

```



- Now, suppose we want to check for **Triangle**, we would need to modify if else ladder by adding another elif statement for **Triangle**.

- This violates OCP.

- Better solution would be to use polymorphism and create separate classes for each request type that inherit from a common abstract class or interface.

```python

from abc import ABC, abstractmethod



class Shape(ABC):

    @abstractmethod

    def process_shape(self):

        pass



class RequestCircle(Shape):

    def process_shape(self):

        # Handle request type Circle



class RequestRectangle(Shape):

    def process_shape(self):

        # Handle request type Rectangle



class RequestParallelogram(Shape):

    def process_shape(self):

        # Handle request type Parallelogram



class checkShape:

    def __init__(self, request):

        self.request = request

    

    def handle_shape(self):

        self.request.process_request()

```



## The Liskov substitution principle (LSP)



> _“Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it”_



- “_Derived classes must be substitutable for their base classes_”.

- If a subclass redefines a function also present in the parent class, a client-user should not be noticing any difference in **behaviour,** and it is a **substitute** for the base class.





> If a subclass violates the structure of Parent class, this would result in violation of LSP.



```python

class Rectangle:

    def __init__(self, width, height):

        self.width = width

        self.height = height

    

    def area(self):

        return self.width * self.height





class Square(Rectangle):

    def __init__(self, side):

        self.width = self.height = side

```

 - The `Square` class sets both the width and height to the same value, which means that the `area` method will return an incorrect value for rectangles.

 - We can change the design to ensure that `Square` and `Rectangle` have a common interface but different implementations.

 ```python
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 Square(Shape):
    def __init__(self, side):
        self.side = side
    
    def area(self):
        return self.side ** 2



```

- `Rectangle` and `Square` can both inherit from the common abstract `Shape` class and implement the `area` method in a way that is consistent with their behaviour.

## The Interface Segregation Principle (ISP)



> “_Many client-specific interfaces are better than one general-purpose interface_”



- A class should only have the interface needed (SRP) and avoid methods that won’t work or that have no reason to be part of that class.

- This problem arises, primarily, when, a subclass inherits methods from a base class that it does not need.



```python

class Vehicle:

    def flight_speed(self):

        pass

    def sail_speed(self):

        pass

    def speed(self):
        pass

    def acceleration(self):
        pass

    def brake(self):

        pass

    

class Car(Vehicle):

    # class would inherit all methods which might not be necessary

```


- Vehicle interface defines 3 methods and forces Car class to use all 3 methods.

- Car class might not require other methods defined in the Vehicle, which could be used by other classes like Ship and Aeroplanes.

- Splitting the `Vehicle` interface into smaller interfaces that group together related methods. Then, the `Car` class can inherit from only the interfaces that it needs to implement.

```python

class LandVehicle:

    def speed(self):

        pass

class AirVehicle:

    def flightspeed(self):

        pass 

class WaterVehicle:

    def sailspeed(self):
        pass

class Acceleration(self):

    def acceleration(self):
        pass

    def brake(self):

        pass 

class Car(LandVehicle, Acceleration):

    def speed(self):

        # implementation of speed method

        pass

    def accelerate(self):

        # implementation of accelerate method

        pass

    def brake(self):

        # implementation of brake method

        pass

class Aeroplane(AirVehicle, Acceleration):
        pass
        # define the methods from both classes

```

- This way class Car would inherit only those interfaces it needs to implement.



## # The Dependency Inversion Principle (DIP)



> “_Abstractions should not depend on details. Details should depend on abstraction. High-level modules should not depend on low-level modules. Both should depend on abstractions”_



```python

class Engine:

    def start(self):

        print("Engine started")



class Vehicle:

    def __init__(self):

        self.engine = Engine()

    

    def start(self):

        self.engine.start()

```

- The `Vehicle` class depends directly on the low-level `Engine` class to start the vehicle's engine. This violates the Dependency Inversion Principle because the `Vehicle` class depends directly on a low-level module.

- Dependency injection can be used to invert the dependencies and decouple the high-level `Vehicle` class from the low-level `Engine` class.

```python

class Engine:

    def start(self):

        print("Engine started")



class Vehicle:

    def __init__(self, engine: Engine):
        self.engine = engine

    def start(self):

        self.engine.start()

```

- We inject the `engine` object into the `Vehicle` constructor as a dependency. 

- This way, the `Vehicle` class is decoupled from the specific implementation of the engine and can work with any implementation that conforms to a common interface. This meets the Dependency Inversion Principle.

```python

class ElectricEngine(Engine):
    def start(self):
        # implementation of start method for electric engine
        print("Electric Engine")
class FuelEngine(Engine):
	def start(self):
		print("Fuel Engine")

electric_engine = ElectricEngine()
fuel_engine = FuelEngine()
vehicle = Vehicle(electric_engine)
vehicle = Vehicle(fuel_engine)
vehicle.start()

```

- We can now create an `ElectricEngine` or `GasEngine` class that conforms to this interface and pass it to the `Vehicle` constructor.

- The `Vehicle` class is decoupled from the specific implementation of the engine and can work with any implementation that conforms to the `engine` interface.



## References

- [Blog on SOLID by Mattia Cinelli](https://towardsdatascience.com/solid-coding-in-python-1281392a6a94)

- [dmmeteo gist SOLID explained in Python](https://gist.github.com/dmmeteo/f630fa04c7a79d3c132b9e9e5d037bfd)

- [Code specialist](https://code-specialist.com/code-principles/solid)