SOLID Principles
Tags: python
Published: 2023-05-15 08:48:20

The aim of this paper is to make the readers familiar with the concepts of SOLID principles. They are essentially a set of practices that leads to well designed software (Readable,Modular,Scalable)

## Single Responsibility Principle

### Basic gist :

- Every class must have a single responsibility to fulfill
- If a class is performing too many tasks, especially if the tasks are logically unrelated, then you would be better off writing another class.

### How it plays out :

**A bad example :**

        #Class below is reponsilble for user properties and user database management
        class  User:
            def __init__(self, name: str):
                self.name = name
        
            def get_name(self) -> str:
                pass

            def save(self, user: User):
                pass

- If some changes are passed to the database management system, all the classes working with user properties will have to be recompiled to compensate for new changes.

**The correct way :**

        #Simply split the classes.
        class User:
            def __init__(self, name: str):
                    self.name = name
            
            def get_name(self):
                pass


        class UserDB:
            def get_user(self, id) -> User:
                pass

            def save(self, user: User):
                pass

## Open/Closed principle

- This principle states that classes should be open to extension, not to modification. If you want to add some functionality to a class, you are better off extending it, avoiding violating the Single responsibility principle.

**A bad example:**

        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

The above class is responsible for giving out discounts, however modifying the class to add the vip discount condition violates the open/closed principle, instead do this:

**The correct way:**

        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

Here, we create a new class that handles the vip discount method.

## Liskov Substitution principle

The objects of a sub class should be able to replace the objects of its parent class without any changes in behavior.

        class Bird():
            def fly():
                pass

        class Duck(Bird):
            pass

        class Ostrich(Bird):
            pass

The above example is a bad implementation, Considering ostriches can't actually fly, We need our subclasses to be able to replace parent classes while still being logically consistent.

        class Bird():
            pass

        class FlyingBirds(Bird):
            def fly():
                pass

        class Duck(FlyingBirds):
            pass

        class Ostrich(Bird):
            pass


The example above is a good implementation of the concept.

## Interface Segregation principle

- An interface can be imagined as a contract, if you decide to implement an interface, you have to do so in its entirety

- So what if you have interfaces with too many functions, all the classes will have
 to implement all the functions even if it just needs one or two of them.

        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

You can implement multiple interfaces, so its better to have smaller and more specific interfaces.

## Dependency Injection principle

- Imagine a scenario where you're directly instantiating classes into other classes. Everytime a class needs to be instantiated, all the classes that its dependent on will need to be instantiated as well, incurring a massive overhead.

- This is solvable by simply giving the responsinbility of instantiation of objects to another entity.

        class AuthenticationForUser():
            def __init__(self, connector:Connector):
                self.connection = connector.connect()
            
            def authenticate(self, credentials):
                pass
            def is_authenticated(self):
                pass	
            def last_login(self):
                pass

        class AnonymousAuth(AuthenticationForUser):
            pass

        class GithubAuth(AuthenticationForUser):
            def last_login(self):
                pass

        class FacebookAuth(AuthenticationForUser):
            pass

        class Permissions()
            def __init__(self, auth: AuthenticationForUser)
                self.auth = auth #Object is injected here.
                
            def has_permissions():
                pass
                
        class IsLoggedInPermissions (Permissions):
            def last_login():
                return auth.last_log

The `AuthenticationForUser` object is passed into the constructor instead of being instantiated inside the class