Deeper Dive into S.O.L.I.D Principles with Python.
Tags: designpatterns, tutorial, python, architecture
Published: 2023-04-29 17:19:17


**SOLID** is an acronym that stands for five object-oriented design principles that help to make software more **maintainable**, **scalable**, and **easy to extend**. The principles are a subset of many principles promoted by [Robert C. Martin](https://en.wikipedia.org/wiki/Robert_C._Martin) (AKA Uncle Bob) which were first introduced in his 2000 paper *Design Principles and Design Patterns* discussing [software rot](https://en.wikipedia.org/wiki/Software_rot). The SOLID acronym was introduced later, around 2004, by Michael Feathers.
<br>
Here are the high level description of each principle. (Take a look, don’t worry too much. They are explained with examples later) -

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; **1. Single Responsibility Principle (SRP)** : Software artefacts (usually a class, module, or interface) should have only one reason to change. 

![Spork vs Spoon and Fork](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l6kqddlh6d4ddqrv7dll.png)


&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; **2. Open-Closed Principle (OCP)** : Software entities should be open for extension but closed for modification.

![OCP](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uayrjvdk2s2lrp5tn266.png)


&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; **3. Liskov Substitution Principle (LSP)** : Sub-types must be substitutable for their base types.


![Duck LSP](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yre9hgycy0ycnqf7xd0m.png)

    
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; **4. Interface Segregation Principle (ISP):** : Clients should not be forced to depend on interfaces they do not use.

![ISP](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1uqd1rz2f6ku8918g3m8.png)


&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; **5. Dependency Inversion Principle (DIP):** :  High-level modules should not depend on low-level modules, but both should depend on abstractions.

![DIP](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fpcikjp7fitrqx0qjjrj.png)


Rest assured, you don’t have memorize or understand all these now. Let's dive a little deeper into each one.
<br>
<br>


### ***A. Single Responsibility Principle : SRP***

The SRP states that **a class should have only one responsibility or reason to change.** In other words, a class should have only **one job or function**, and should not be responsible for doing more than that one job. 


![SRP](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uni49178t288tr131f51.png)


#### So, How do we achieve SRP?

1. **Separating concerns:** Separate the concerns of the class into smaller, more specialised classes. Each class should be responsible for a single concern, and not overlap with other classes. For example, a class that handles both database operations and user interface interactions could be split into two classes, one for database operations and one for user interface interactions.
2. **Creating helper classes:** A class can have a single responsibility, but the responsibility may be complex or involve multiple tasks. In such cases, the class can create helper classes that perform specific tasks. For example, a class that generates reports could have a helper class that formats the report, and another helper class that retrieves data for the report.
3. **Encapsulating responsibilities:** Another way to achieve SRP is to encapsulate the responsibilities of a class into well-defined interfaces or modules. By encapsulating responsibilities, changes to one module or interface will not affect other modules or interfaces. This allows the code to be more modular and easier to maintain over time.
4. **Delegating responsibilities:** A class can also delegate responsibilities to other classes, rather than performing all operations itself.

Overall, achieving SRP in code involves breaking down classes into smaller, more focused units of responsibility, encapsulating responsibilities, delegating responsibilities, and refactoring code. By following these principles, we can create code that is more maintainable, extensible, and easier to understand over time.

No code examples here, use your own judgement for SRP. However, here's a blog that has a nice exaple in it - [The Single Responsibility Principle](https://exceptionnotfound.net/simply-solid-the-single-responsibility-principle/)
Now,
<br>
<br>

## ***B. The Open-Closed Principle : OCP***

OCP is achieved in code by designing software entities such as classes, modules, functions, etc. that are **open for extension but closed for modification**. This means that we should be able to **extend the behavior of a software entity without changing its existing implementation**.
<br>
#### A Python Example -
Here, we have created a hierarchy of classes that represent shapes, including **`Shape`**, **`Rectangle`**, and **`Circle`**. Each of these classes implements the **`area()`** method, which calculates the area of the respective shape.

```python
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 AreaCalculator:
    def calculate(self, shapes):
        total_area = 0
        for shape in shapes:
            total_area += shape.area()
        return total_area
```

We have also created an **`AreaCalculator`** class, which takes a list of shapes as input and calculates the total area of all the shapes. This class is closed for modification, meaning we can add new shapes to our hierarchy without having to modify the **`AreaCalculator`** class.

For example, if we wanted to add a new shape, such as a triangle, we could simply create a new **`Triangle`** class that implements the **`area()`** method, and the **`AreaCalculator`** class would be able to handle it without any modification. This demonstrates the Open-Closed Principle, which states that software entities (classes, modules, etc.) should be open for extension but closed for modification.
<br>
#### Ways of the OCP -

1. **Abstraction:** We can use abstraction to define a generic interface for a set of related operations, and then create concrete implementations of that interface for specific use cases. This allows us to add new implementations without modifying the existing ones.
2. **Polymorphism:** We can use polymorphism to allow objects of different types to be treated as if they were of the same type. This allows us to write code that can work with different implementations of the same interface without having to know the specifics of each implementation.
3. **Inheritance:** We can use inheritance to create a hierarchy of classes, where each class inherits behavior from its parent classes. This allows us to create new classes that inherit existing behavior, and add new behavior on top of it without modifying the existing classes.
4. **Composition:** We can use composition to combine smaller, more specialized classes into larger, more complex classes. This allows us to create new behavior by composing existing behavior, without having to modify the existing classes.

Overall, the key idea behind achieving the OCP is to **separate behavior that is likely to change from behavior that is unlikely to change, and to create well-defined interfaces between them**. By doing this, we can make our software more flexible, maintainable, and extensible. In the end, we just don’t want to face a situation like this :
https://twitter.com/i/status/1064871830358503425
Sorry for the lame meme. Let's move on to-
<br>
<br>

## ***C. Liskov Substitution Principle : LSP***

LSP states that objects of a superclass should be able to be replaced with objects of its subclasses without affecting the correctness of the program. In other words, if we have a program that is designed to work with objects of a particular type, we should be able to use objects of any of its subtypes without breaking the program.


To achieve LSP, we can follow a few guidelines when designing our classes and interfaces:

1. **All subclasses should behave in the same way as their superclass**. This means that any methods or properties defined in the superclass should also be present in the subclasses, and should behave in the same way.
2. **Subclasses should not add any additional preconditions or stronger postconditions to the methods defined in the superclass.** In other words, they should not impose any additional constraints on the inputs or outputs of the methods.
3. **Subclasses should not override any methods of the superclass in a way that changes their semantics.** The overridden methods should behave in the same way as the superclass methods.

#### LSP explained with Code -

First, take a look at this meme:

![LSP with car](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0t5sk2u1s7n6jfcfudet.png)

This is a good analogy for the LSP. In the same way that you can learn to drive one car and then be able to drive any car afterwards, if we design our classes and interfaces according to LSP, we can write code that works with one object and then be able to work with any object that is a subtype of that object without modifying the code.

For example, if we have a program that works with objects of a class **`Vehicle`**, and we follow the LSP when designing our classes, we should be able to use any object that is a subtype of **`Vehicle`**, such as a **`Car`**, **`Truck`**, or **`Motorcycle`**, without having to change the code that works with **`Vehicle`** objects.

This is because all of the subtypes of **`Vehicle`** should have the same behavior as **`Vehicle`**, and should be able to respond to the same methods and properties in the same way. This means that we can use them interchangeably without affecting the correctness of the program.

Just like you can use your knowledge of driving one car to drive any other car, we can use our knowledge of working with one type of object to work with any subtype of that object, as long as we follow the LSP.

```python
class Vehicle:
    def __init__(self, make, model):
        self.make = make
        self.model = model
        
    def start_engine(self):
        print("Starting engine...")
        
    def stop_engine(self):
        print("Stopping engine...")
        
class Car(Vehicle):
    def __init__(self, make, model, num_doors):
        super().__init__(make, model)
        self.num_doors = num_doors
        
    def start_engine(self):
        print("Starting car engine...")
        
class Motorcycle(Vehicle):
    def __init__(self, make, model, num_wheels):
        super().__init__(make, model)
        self.num_wheels = num_wheels
        
    def start_engine(self):
        print("Starting motorcycle engine...")
```

In this example, we have a **`Vehicle`** class that defines basic properties and methods for a vehicle, such as **`make`**, **`model`**, **`start_engine()`**, and **`stop_engine()`**. We also have two subclasses of **`Vehicle`**: **`Car`** and **`Motorcycle`**. Each of these subclasses overrides the **`start_engine()`** method to provide their own implementation of starting the engine.

We can create objects of these classes and use them interchangeably, because they all share the same interface and behavior:

```python
def drive(vehicle):
    vehicle.start_engine()
    print("Driving...")
    vehicle.stop_engine()
    
car = Car("Toyota", "Camry", 4)
motorcycle = Motorcycle("Harley-Davidson", "Sportster", 2)

drive(car)        # Starting car engine... Driving... Stopping engine...
drive(motorcycle) # Starting motorcycle engine... Driving... Stopping engine...
```

In this code, the **`drive()`**function takes an object of the **`Vehicle`**class, which can be any subtype of **`Vehicle`**, and calls its **`start_engine()`**, **`drive()`**, and **`stop_engine()`**methods. We can pass in either a **`Car`**or a **`Motorcycle`**object, and the code works the same way for both, without having to modify the **`drive()`**function. This is an example of LSP in action.
<br>
<br>
## *D. Interface Segregation Principle  : ISP*

ISP states that "**clients should not be forced to depend on interfaces they do not use.**" This means that we should design our interfaces to be as **specific and focused** as possible (Look at the cookie meme), so that clients only need to depend on the parts of the interface that they actually use.


![dvav](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3a1a5bqrez6074f0f8zi.png)
Check this link if you don't understand the meme-
https://stackoverflow.com/questions/56174598/understanding-the-motivational-poster-for-the-interface-segregation-principle


#### Idea

The idea behind this one is that “no client should be forced to depend on methods it does not use”. What this actually means is that a class should be correctly abstracted so only have the methods it needs (think Single Responsibility). It also allows for larger classes (more common in historical work) where clients can interface with only the methods they need and not be exposed to those they don’t. ISP is basically "Single Responsibility Principle applied to interfaces".

<br>

#### Common ways to achieve ISP

1. **Break down large interfaces** into smaller, more focused interfaces: Instead of creating a single large interface that includes many methods, break it down into smaller interfaces that each have a specific purpose. This allows clients to depend only on the interfaces they need.
2. Use **Adapter classes**: If you have a class that implements an interface, but not all of the methods in that interface are relevant to that class, you can create an adapter class that implements the interface and provides default implementations for the irrelevant methods. This allows clients to depend on the adapter class instead of the original class.
3. **Default implementations:** For interfaces that have many methods, you can provide default implementations that do nothing or raise an exception. This allows clients to only implement the methods they need and ignore the rest.
4. By **Delegation**: If a class needs to implement multiple interfaces, but not all of the methods in those interfaces are relevant to that class, you can delegate the implementation of the irrelevant methods to other classes or objects. This allows the main class to only implement the methods it needs.

<br>

## Here's an example of how we can achieve ISP in Python code


We have a **`Vehicle`** class with a **`start_engine()`** method, which is implemented by its subclasses **`Car`** and **`Motorcycle`**. These subclasses only provide the functionality that is relevant to them, and do not implement any unnecessary methods. We also have an **`Airplane`** class with a **`fly()`** method, which is not related to the **`Vehicle`** hierarchy. 

```python
class Vehicle:
    def start_engine(self):
        pass

class Car(Vehicle):
    def start_engine(self):
        print("Starting car engine...")

class Motorcycle(Vehicle):
    def start_engine(self):
        print("Starting motorcycle engine...")

class Airplane:
    def fly(self):
        print("Flying airplane...")

class AirplaneAdapter(Vehicle):
    def __init__(self, airplane):
        self.airplane = airplane
        
    def start_engine(self):
        self.airplane.fly()
```

However, we can still make use of the **`Airplane`** class by creating an **`AirplaneAdapter`** class, which implements the **`Vehicle`** interface by wrapping an instance of the **`Airplane`** class. This allows us to use an **`Airplane`** object as if it were a **`Vehicle`** object, even though it does not inherit from the **`Vehicle`** class.

By using the **`AirplaneAdapter`** class, we are following the ISP, because clients of the **`Vehicle`** interface only need to depend on the **`start_engine()`** method, which is the only method that is relevant to them. They do not need to depend on the **`fly()`** method, which is specific to the **`Airplane`** class.

```python
def start_and_stop_engine(vehicle):
    vehicle.start_engine()
    print("Stopping engine...")

car = Car()
motorcycle = Motorcycle()
airplane = Airplane()
adapter = AirplaneAdapter(airplane)

start_and_stop_engine(car)         # Starting car engine... Stopping engine...
start_and_stop_engine(motorcycle)  # Starting motorcycle engine... Stopping engine...
start_and_stop_engine(adapter)     # Flying airplane... Stopping engine...
```

In this code, we have a **`start_and_stop_engine()`** function that takes any object that implements the **`Vehicle`** interface, and calls its **`start_engine()`** method followed by a print statement. We can pass in either a **`Car`**, **`Motorcycle`**, or **`AirplaneAdapter`** object, and the code works the same way for all of them, without having to modify the **`start_and_stop_engine()`** function. This is an example of ISP in action.

 

This will sound a bit confusing the first time you read it. If you couldn’t comprehend all that mumbo-jumbo, just remember that the main goal of this principle is to write code in a way so that devs using our code doesn’t feel like this- 


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/67fmsjh329ijup9ad5mz.png)
<br>
<br>
## ***E. Dependency Inversion Principle  : DIP***

DIP states that **high-level modules should not depend on low-level modules**. Instead, **both** should **depend on abstractions**.

In other words, DIP suggests that classes or modules should depend on abstractions or interfaces, rather than on concrete implementations. This allows for greater flexibility and modularity in the design, as **changes to the low-level modules do not affect the high-level modules.**


![fbfb f](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o0c1gech4i3xm11pokyy.png)



## **Here's an example of how to apply the Dependency Inversion Principle (DIP) in Python:**

Let's say we have two classes, **`FileReader`** and **`FileProcessor`**, where **`FileReader`** reads data from a file and **`FileProcessor`** processes the data. The **`FileProcessor`** class depends on the **`FileReader`** class to get the data. However, if we want to change the way the data is read (for example, from a different file format or from a database instead of a file), we would have to change the **`FileProcessor`** class as well.

```python
# Without DIP:

class FileReader:
    def read(self, filename):
        # read data from file
        pass

class FileProcessor:
    def __init__(self):
        self.reader = FileReader()

    def process(self, filename):
        data = self.reader.read(filename)
        # process data
        pass**
```

```python
# With DIP:

class IReader:
    def read(self, filename):
        pass

class FileReader(IReader):
    def read(self, filename):
        # read data from file
        pass

class FileProcessor:
    def __init__(self, reader):
        self.reader = reader

    def process(self, filename):
        data = self.reader.read(filename)
        # process data
        pass
```

In this example, we define an interface **`IReader`** that defines the **`read`** method, and the **`FileReader`** class implements the interface. The **`FileProcessor`** class no longer depends on the **`FileReader`** class directly, but on the **`IReader`** interface, which can be implemented by any class that reads data.

By using an interface, we can easily swap out the implementation of the **`IReader`** interface (for example, by creating a new class that reads data from a database), without having to modify the **`FileProcessor`** class. This makes our code more flexible and easier to maintain.

### How to achieve DIP

1. Use **interfaces or abstract classes to define dependencies:** Instead of depending on concrete classes directly, use interfaces or abstract classes to define the required behavior. This allows you to swap out the implementation without changing the code that depends on it.
2. Use **Dependency injection:** Instead of creating and managing dependencies within a class or module, use a dependency injection framework to inject the required dependencies at runtime. This allows you to easily swap out dependencies without changing the code that uses them.
3. Use **Inversion of control containers:** An inversion of control (IoC) container is a framework that manages the creation and configuration of objects and their dependencies. By using an IoC container, you can achieve automatic dependency injection and reduce the amount of boilerplate code required for managing dependencies.

Dependency Inversion is a design principle to **limit the effect of changes to low-level classes** in your projects.

Think of a base class like ‘make call’. This class worked great 50 years ago when calls were only made in one way, but now we can make wi-fi calls, skype, whatsapp and more. To add these into our bad example would require modifying the base class to enable support for other methods and means that every time we add or edit one of these we would need to test all other versions because they derive from the same class.





![Irr](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fgqq1yhyak6plhnd4y07.png)




Dependency inversion states that **classes should depend on abstractions**. So in this case, we could define an interface for the call and them implement methods for ‘Landline’, ‘Skype’ etc.

Dependency Inversion isn't really a separate principle, but is rather a combination of Single Responsibility Principle and Liskov Substitution Principle.
<br>
<br>
# ***Wrapping it all up***

So,  In conclusion, the SOLID principles are a cool set of guidelines for writing clean, maintainable, and extensible code. 

While the SOLID principles are not always easy to apply, especially for beginners, it's important to keep in mind that they are **not strict rules**, but rather **guidelines** that can be **adapted to suit different situations**. However, by understanding and applying these principles when appropriate, we can produce higher quality code with flexibility, scalability that will save us time and effort in the long run and ultimately deliver better software products.