SOLID PRINCIPLES
Published: 2023-02-20 04:44:24

Object-Oriented Design is essential in software development when it comes to writing code that is flexible, scalable, maintainable, and reusable.

There are numerous advantages to using OOD, but every developer should understand the SOLID principle in order to create good object-oriented design in programming.

Robert C. Martin, also known as Uncle Bob, introduced the SOLID principle, which is now a programming coding standard.

The SOLID principle aids in lowering tight coupling.

Tight coupling in code should be avoided because it means a collection of classes are heavily dependent on one another.

Therefore when your classes are loosely coupled, your code is deemed to be of high quality.

Your code will be less likely to change due to the presence of loosely connected classes, which makes the code to be more reusable, maintainable, versatile, and stable. 

This principle is an abbreviated version of the five principles listed below. 

- S - Single-responsiblity Principle
- O - Open-closed Principle
- L - Liskov Substitution Principle
- I - Interface Segregation Principle
- D - Dependency Inversion Principle


## Single-Responsibility Principle (SRP):

According to this principle “a class should have only one reason to change” which means every class should have a single responsibility or single job or single purpose. 

```sh
class Birds:
    def __init__(self, name: str):
        self.name = name
    
    def get_name(self) -> str:
        pass

    def save(self, Birds: Birds):
        pass
```

The aforementioned Birds class transgresses the SRP.

SRP stipulates that classes should only have one job; nevertheless, in this case, we may identify two , managing the Birds database and managing the Birds properties.

While the save controls how the Birds are stored in a database, the constructor and get name control how the properties of the Birds are managed.

If modifications to the program have an impact on how databases are managed, to account for the new modifications, the classes that utilise Birds attributes will need to be modified and recompiled.

We develop a different class that will be responsible for the sole task of maintaining Birds in a database in order to comply with SRP. 

```sh
class Birds:
    def __init__(self, name: str):
            self.name = name
    
    def get_name(self):
        pass


class BirdsDB:
    def get_Birds(self) -> Birds:
        pass

    def save(self, Birds: Birds):
        pass
```


## Open-Closed Principle (OCP) :

According to this principle "Objects or entities should be open for extension but closed for modification". 

This means that without modifying the class behaviour , it needs to be extended.

```sh
class Birds:
    def __init__(self, name: str):
        self.name = name
    
    def get_name(self) -> str:
        pass

birds = [
    Birds('parrot'),
    Birds('crow')
]

def Birds_Colour(birds: list):
    for bird in birds:
        if bird.name == 'parrot':
            print('green')

        elif bird.name == 'crow':
            print('black')

Birds_Colour(birds)
```

The function Birds_Colour does not conform to the open-closed principle because it cannot be closed against new kinds of birds.

If we add a new bird, crane, We have to modify the Birds_Colour function.

You see, for every new bird, a new logic is added to the Birds_Colour function. 

This is quite a simple example. When your application grows and becomes complex, you will see that the if statement would be repeated over and over again in the Birds_Colour function each time a new bird is added, all over the application.


```sh
birds = [
    Birds('parrot'),
    Birds('crow'),
    Birds('crane')
]

def Birds_Colour(birds: list):
    for bird in birds:
        if bird.name == 'parrot':
            print('green')
        elif bird.name == 'crow':
            print('black')
        elif bird.name == 'crane':
            print('white')

Birds_Colour(birds)
```


```sh
class Birds:
    def __init__(self, name: str):
        self.name = name
    
    def get_name(self) -> str:
        pass

    def Birds_Colour(self):
        pass


class parrot(Birds):
    def Birds_Colour(self):
        return 'green'


class crow(Birds):
    def Birds_Colour(self):
        return 'black'


class crane(Birds):
    def Birds_Colour(self):
        return 'white'


def Birds_Colour(Birds: list):
    for bird in birds:
        print(bird.Birds_Colour())

Birds_Colour(Birds)
```
Birds now has a virtual method Birds_Colour. 

We have each bird extend the Birds class and implement the virtual Birds_Colour method.

Every bird adds its own implementation on its colour in the Birds_Colour. 

The Birds_Colour iterates through the array of birds and just calls its Birds_Colour method.

Now, if we add a new bird, Birds_Colour doesn’t need to change. 

All we need to do is add the new bird to the birds array.
Birds_Colour now conforms to the OCP principle.


## Liskov’s Substitution Principle (LSP):

The principle was introduced by Barbara Liskov in 1987 and according to this principle “Derived or child classes must be substitutable for their base or parent classes“. 

This principle ensures that any class that is the child of a parent class should be usable in place of its parent without any unexpected behavior.

For example we have a class called Amphibian for animals that can live on both land and water. 

This class has two methods to show the features of an amphibian – swim() and walk().

```sh
class Amphibian(ABC):
    def swim() -> bool:
        print("Can Swim") 

    def walk() -> bool:
        print("Can Walk") 
        
```

The Amphibian class can extend to a turtle class because turtles are amphibians, so they can inherit the properties of the Amphibian class without altering the logic and purpose of the class.

```sh
class turtle(Amphibian):
    def swim():
        return print("turtles can swim") 

    def walk():
        return print("turtles can walk") 

```

But we cannot extend the Amphibian class to a shark class because shark only live in water which implies that the walk() method would be irrelevant to the shark class.

So, when you extend a class, if some of the properties of the initial class are not useful for the new class, the Liskov substitution principle has been violated.

The solution to this is simple, create interfaces that match the needs of the inheriting class.


```sh
class Swim(ABC):
    @abstractmethod
    def swim(self) -> bool:
        pass

class Walk(ABC):
    @abstractmethod
    def walk(self) -> bool:
        pass

class Amphibian(Swim, Walk):
    def swim(self) -> bool:
        print("Can Swim")
        return True

    def walk(self) -> bool:
        print("Can Walk")
        return True

class Turtle(Swim, Walk):
    def swim(self) -> bool:
        print("Turtles can swim")
        return True

    def walk(self) -> bool:
        print("Turtles can walk")
        return True
```

In this updated implementation, we define two interfaces, "Swim" and "Walk", which define the contract that any class implementing them must follow. 

The "Amphibian" class implements both "Swim" and "Walk" interfaces, while the "Turtle" class also implements both interfaces. 

By using interfaces, we ensure that any class implementing the "Swim" and "Walk" interfaces must have the same method signatures as defined in the interface, ensuring adherence to the Liskov Substitution Principle (LSP).


## Interface Segregation Principle (ISP):

This principle is the first principle that applies to Interfaces instead of classes in SOLID and it is similar to the single responsibility principle. 

It states that “do not force any client to implement an interface which is irrelevant to them“. 

Here your main goal is to focus on avoiding fat interface and give preference to many small client-specific interfaces.

You should prefer many client interfaces rather than one general interface and each interface should have a specific responsibility.

In summary, if a class inherits another, it should do so in a manner that all the properties of the inherited class would remain relevant to its functionality.


```sh
class Walker(ABC):
  def walk() -> bool:
    return print("Can Walk") 

class Swimmer(ABC):
  def swim() -> bool:
    return print("Can Swim") 

class turtle(Walker, Swimmer):
  def walk():
    return print("turtles can walk") 
  def swim():
    return print("turtles can swim") 
    
```
To run the above code we need to run

```sh

class Whale(Swimmer):
  def swim():
    return print("Whales can swim") 

if __name__ == "__main__":
  turtle.walk()
  turtle.swim()

  Whale.swim()
```


## Dependency Inversion Principle (DIP): 

According to this principle "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".

Consider the below example, where the DIP has been violated where , switch is a concept that is logically in a layer above the light bulb, and the switch relies on it. 

This will lead to poor extensibility or even circular imports that prevent the program from being interpreted or compiled.


```sh
class LightBulb:
  
  def __init__(self, initial_state: bool=False):
    self.power = initial_state
    
  def turn_on(self):
    self.power = True
    
  def turn_off(self):
    self.power = False

class Switch:
  
  def __init__(self, light_bulb: LightBulb, pressed: bool=False):
    self.light_bulb = light_bulb
    self.pressed = pressed
    
  def toggle(self):
    self.pressed = not self.pressed # Toggle
    if self.pressed:
      self.light_bulb.turn_on()
    else:
      self.light_bulb.turn_off()
        
```

      
 Instead of the light bulb telling the switch how the bulb should be handled, the switch should tell the light bulb how to implement it. The naive approach would be to define an interface that tells the light bulb how it should behave to be used with a switch.     

```sh
class Device(ABC):
  power: boolean
    
  def __init__(self, initial_state: bool=False):
    self.power = initial_state
    
  def turn_on(self):
    raise NotImplementedError
  
  def turn_off(self):
    raise NotImplementedError
    
class Switch:
  
  def __init__(self, device: Device, pressed: bool=False):
    self.device = device
    self.pressed = pressed
    
  def toggle(self):
    self.pressed = not self.pressed # Toggle
    if self.pressed:
      self.device.turn_on()
    else:
      self.device.turn_off()  

class LightBulb(Device):
  
  def turn_on(self):
    self.power = True
    
  def turn_off(self):
    self.power = False

```

The dependency has been inverted. Instead of the switch relying on the light bulb, the light bulb now relies on an interface in a higher module. Also, both rely on abstractions, as required by the DIP. 

Last but not least, we also fulfilled the requirement "Abstractions should not depend upon details. Details should depend upon abstractions" - The details of how the device behaves rely on the abstraction (Device interface).


## Conclusion

SOLID principles are a set of design principles that aim to make software designs more maintainable, scalable, and flexible. By adhering to these principles, developers can create software that is easier to modify, extend, and test, leading to fewer bugs and better performance. Incorporating SOLID principles into the development process helps to reduce the complexity of the codebase and make it easier to understand, resulting in high-quality, maintainable code that meets the needs of users and stakeholders alike.

## References

The references used are linked below.

https://www.youtube.com/watch?v=TMuno5RZNeE 

https://www.youtube.com/playlist?list=PL6n9fhu94yhXjG1w2blMXUzyDrZ_eyOme

https://www.digitalocean.com/community/conceptual_articles/s-o-l-i-d-the-first-five-principles-of-object-oriented-design

https://medium.com/mindorks/solid-principles-explained-with-examples-79d1ce114ace

https://blog.damavis.com/en/solid-principles-illustrated-in-simple-python-examples/