SOLID Concepts
Tags: programming, oop, python, beginners
Published: 2023-02-20 06:38:08


- Solid is a principle that is written in conjunction with code to improve the problems of bad code which are rigidity, fragility and non-usability. 

- Implementing the solid principle into the code helps the programmer to easily add the changes required onto the code with lesser effort.


## **Why Solid?**

- In the world of software development, programmers tend to tangle up while making changes in the code. 
- A good programmer writes readable, clear and concise code.
- Even if the quality of the code is good, the programmer could find it hard to make some changes to it.

## **Why solid should be used?** 


- To understand this let us look at the below example:

    - A bank company hires a programmer to make software which stores the credential id and could only be accessed by the customer.
    - The programmer makes the software and hands it over to the bank company.
    - After using it for a while, the bank company asks the programmer for some changes to it.
    - The programmer looks up to the code and do the changes required.
    - The highest probability is that the software will crash if the quality of the code is bad.
    - He has to make changes to the other modules as well.
    - The bigger picture in the development field is that the programmer goes to such lengths for making those small changes.
    - If we don't follow solid principles we end up with tight or strong coupling of the code with many other modules/applications.
    - Tight coupling causes time to implement any new requirement, feature or bug fixes and sometimes it creates some unknown issues.

- Therefore, to resolve such problems an American software engineer and instructor [Robert C. Martin](https://en.wikipedia.org/wiki/Robert_C._Martin) came up with a solid principle.


## **Getting started with Solid**

- The acronyms for class design principles in the solid are:
    
    - **S**RP - Single Responsibility Principle
    - **O**CP - Open Close Principle
    - **L**SP - Liskov Substitution Principle
    - **I**SP - Interface Segregation Principle
    - **D**IP - Dependency Inversion Principle 

### **Single Responsibility Principle**

**"A class should have only one reason to change"**

- Every module or class should have responsibility over a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by the class.

- Each class and module focuses on a single task at a time.

- If a class has more than one responsibility, it becomes coupled.

    ```python
    class Vehicle:
        def __init__(self, name):
                self.name = name
        
        def get_name(self):
            pass


    class VehicleDB:
        def get_vehicle(self):
            pass

        def type(self, vehicle: Vehicle):
            pass

    ```
- When designing our classes, we should aim to put related features together, so whenever they tend to change they change for the same reason. 


### **Open Close Principle**
**"Modules should be open for extension, but closed for modification."**

- To understand more about the open-close principle let us take an example.
- Let’s imagine you have a store, and you give a discount of 20% to your favourite customers using this class.
- When you decide to offer double the 20% discount to VIP customers. You may modify the class like this:

    ```python
    class Discount:
        def __init__(self, customer, price):
            self.customer = customer
            self.price = price

        def give_discount(self):
                if self.customer == 'fav':
                    return self.price * 0.2
                if self.customer == 'vip':
                    return self.price * 0.4
    ```

- This fails the OCP principle due to the changes made in the module.

- To make it follow the OCP principle, we will add a new class that will extend the Discount.

- In this new class, we would implement its new behaviour.

    ```python
    class Discount:
        def __init__(self, customer, price):
            self.customer = customer
            self.price = price

        def get_discount(self):
                return self.price * 0.2


    class VIPDiscount(Discount):
        def get_discount(self):
            return super().get_discount() * 2
    ```

- You see, extension without modification.

### **Liskov Substitution Principle**
**"Derived class must be usable through the base class interface, without the need for the user to know the difference."**

- Liskov principle is a particular definition of a subtyping relation called behavioural subtyping.
- The Liskov substitution principle can be implemented by the following:
    
    - No new exceptions can be thrown by the subtype.
    
    - Clients should not know which specific subtype they are calling.
    
    - New derived classes just extend without replacing the functionality of old classes.

    - This principle aims to ascertain that a sub-class can assume the place of its super-class without errors.

- If the code finds itself checking the type of class then, it must have violated this principle.

    ```python
    def animal_leg_count(animals: list):
        for animal in animals:
            if isinstance(animal, Lion):
                print(lion_leg_count(animal))
            elif isinstance(animal, Mouse):
                print(mouse_leg_count(animal))
            elif isinstance(animal, Pigeon):
                print(pigeon_leg_count(animal))
            
    animal_leg_count(animals)
    ```
- The above example does not follow liskov's principle, the following changes should be made to make it follow liskov's principle.

    ```python
        def animal_leg_count(animals: list):
            for animal in animals:
                print(animal.leg_count())
        
    animal_leg_count(animals)
    ```


- The animal_leg_count function cares less about the type of Animal passed, it just calls the leg_count method. 
- All it knows is that the parameter must be of an Animal type, either the Animal class or its sub-class.
- The Animal class now have to implement/define a leg_count method and its sub-classes have to implement the leg_count method.


    ```python
    class Animal:
        def leg_count(self):
            pass


    class Lion(Animal):
        def leg_count(self):
            pass
    ```
- animal_leg_count just calls the leg_count method of the Animal type because by contract a sub-class of the Animal class must implement the leg_count function.

### **Interface Segregation Principle**

**"No client should be forced to depend on the interface/methods it does not use."**

- Classes should not have to implement interfaces with methods they don't need.
- One large interface should be split into many smaller and relevant interfaces so that the clients can know about the interfaces that are relevant to them.

- Let’s look at the below IShape interface

    ```python
    class Circle(IShape):
        def draw_square(self):
            pass

        def draw_rectangle(self):
            pass
        
        def draw_circle(self):
            pass

    class Square(IShape):
        def draw_square(self):
            pass

        def draw_rectangle(self):
            pass
        
        def draw_circle(self):
            pass

    class Rectangle(IShape):
        def draw_square(self):
            pass

        def draw_rectangle(self):
            pass
        
        def draw_circle(self):
            pass
    ```

- It’s quite funny looking at the code above. Class Rectangle implements methods (draw_circle and draw_square) it has no use of, likewise Square implements draw_circle, and draw_rectangle, and class Circle (draw_square, draw_rectangle).

- The classes must implement the new method or an error will be thrown.
- We see that it is impossible to implement a shape that can draw a circle but not a rectangle or a square or a triangle. 
- We can just implement the methods to throw an error that shows the operation cannot be performed.
- Clients (here Rectangle, Circle, and Square) should not be forced to depend on methods that they do not need or use. 
- To make our IShape interface conform to the ISP principle, we segregate the actions into different interfaces.
- Classes (Circle, Rectangle, Square, Triangle, etc) can just inherit from the IShape interface and implement their draw behaviour.

    ```python
    class IShape:
        def draw(self):
            raise NotImplementedError

    class Circle(IShape):
        def draw(self):
            pass

    class Square(IShape):
        def draw(self):
            pass

    class Rectangle(IShape):
        def draw(self):
            pass
    ```

- We can then use the I-interfaces to create Shape specifics like Semi Circle, Right-Angled Triangles, Equilateral Triangle, Blunt-Edged Rectangle, etc.

### **Dependency Inversion Principle**
**"High-level modules should not depend on low-level modules."**

- Abstraction should not depend on details. Details should depend on abstraction.
- The interaction between high-level and low-level modules should be thought of as an abstract interaction between them.

- There comes a point in software development where our app will be largely composed of modules. 
- When this happens, we have to clear things up by using dependency injection. 
- High-level components depend on low-level components to function.

    ```python
    class XMLHttpService(XMLHttpRequestService):
        pass

    class Http:
        def __init__(self, xml_http_service: XMLHttpService):
            self.xml_http_service = xml_http_service
        
        def get(self, url: str, options: dict):
            self.xml_http_service.request(url, 'GET')

        def post(self, url, options: dict):
            self.xml_http_service.request(url, 'POST')
    ```

- Here, Http is the high-level component whereas HttpService is the low-level component. 
- This design violates DIP A: High-level modules should not depend on low-level level modules. It should depend upon its abstraction.
- This Http class is forced to depend upon the XMLHttpService class. 

    ```python
    class XMLHttpService(Connection):
        xhr = XMLHttpRequest()

        def request(self, url: str, options:dict):
            self.xhr.open()
            self.xhr.send()

    class NodeHttpService(Connection):
        def request(self, url: str, options:dict):
            pass

    class MockHttpService(Connection):
        def request(self, url: str, options:dict):
            pass
    ```

- We can create many Http Connection types and pass them to our Http class without any fuss about errors.

## Conclusion

- In the field of software development, solid principles are used to achieve flexibility in the code.
- One can refactor their codes and revise it again and again very easily.
- Implementing the five key principles in the code maintains the reusability of code as well.

## **References**

- To know more about solid refers to the below section
    - [Solid](https://en.wikipedia.org/wiki/SOLID)
    - [Solid principles by Bob Martin](https://www.youtube.com/watch?v=TMuno5RZNeE)
    - [Examples on the five principles](https://www.hashbangcode.com/article/solid-principles-python)