SOLID PRINCIPLES
Published: 2023-02-20 04:52:39

* SOLID is a mnemonic abbreviation for a set of design principles created for software development in object-oriented languages.
* According to Orner, while the practice of software development has changed in the past 20 years, SOLID principles are still the basis of good design.

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

## 1. Single-Responsibility Principle(SRP)
* This states that a class should have a single responsibility, but more than that, a class should only have one reason to change. 

### Example
A change to one responsibility results to modification of the other responsibility.
```
class Animal:
    def __init__(self, name: str):
        self.name = name
    def get_name(self) -> str:
        pass
    def save(self, animal: Animal):
        pass
```
To make this conform to SRP, we create another class that will handle the sole responsibility of storing an animal to a database:
```
class Animal:
    def __init__(self, name: str):
            self.name = name
    def get_name(self):
        pass
class AnimalDB:
    def get_animal(self) -> Animal:
        pass
    def save(self, animal: Animal):
        pass
```

## 2. Open-Closed Principle (OCP)
* In the open/closed principle classes should be open for extension, but closed for modification. 
* Essentially meaning that classes should be extended to change functionality, rather than being altered into something else.

### Example
Let’s imagine you have a store, and you give a discount of 20% to your favorite customers using this class:
When you decide to offer double the 20% discount to VIP customers. You may modify the class like this:
```
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
```
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 behavior:
```
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
class SuperVIPDiscount(VIPDiscount):
    def get_discount(self):
        return super().get_discount() * 2
```

## 3. Liskov Substitution Principle (LSP)
* A sub-class must be substitutable for its super-class.
* The aim of this principle is 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.

### Example
Let’s use our Animal example.
```
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)
```

To make this function follow the LSP principle, we will follow this LSP requirements postulated by Steve Fenton:
If the super-class (Animal) has a method that accepts a super-class type (Animal) parameter. 
Its sub-class(Pigeon) should accept as argument a super-class type (Animal type) or sub-class type(Pigeon type).
If the super-class returns a super-class type (Animal). 
Its sub-class should return a super-class type (Animal type) or sub-class type(Pigeon).
Now, we can re-implement animal_leg_count function:

```
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 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:
```
class Animal:
    def leg_count(self):
        pass
class Lion(Animal):
    def leg_count(self):
        pass
```

## 4. Interface Segregation Principle(ISP)
* Make fine grained interfaces that are client specific
* Clients should not be forced to depend upon interfaces that they do not use.
* This principle deals with the disadvantages of implementing big interfaces.

### Example
Let’s look at the below IShape interface:
```
class IShape:
    def draw_square(self):
        raise NotImplementedError
    def draw_rectangle(self):
        raise NotImplementedError
    def draw_circle(self):
        raise NotImplementedError
```
Here, our IShape interface performs actions that should be handled independently by other interfaces.
To make our IShape interface conform to the ISP principle, we segregate the actions to different interfaces.
Classes (Circle, Rectangle, Square, Triangle, etc) can just inherit from the IShape interface and implement their own draw behavior.
```
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
```

## 5. Dependency Inversion Principle(DIP)
* Dependency should be on abstractions not concretions
  *  High-level modules should not depend upon low-level modules. Both should depend upon abstractions.
  * Abstractions should not depend on details. Details should depend upon abstractions.

### Example
```
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')
```
The Connection interface has a request method. With this, we pass in an argument of type Connection to our Http class:
```
class Http:
    def __init__(self, http_connection: Connection):
        self.http_connection = http_connection
    def get(self, url: str, options: dict):
        self.http_connection.request(url, 'GET')
    def post(self, url, options: dict):
        self.http_connection.request(url, 'POST')
```

We can now re-implement our XMLHttpService class to implement the Connection interface:
```
class XMLHttpService(Connection):
    xhr = XMLHttpRequest()

    def request(self, url: str, options:dict):
        self.xhr.open()
        self.xhr.send()
```
We can create many Http Connection types and pass it to our Http class without any fuss about errors.
```
class NodeHttpService(Connection):
    def request(self, url: str, options:dict):
        pass

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

### References
* https://www.hashbangcode.com/article/solid-principles-python