S.O.L.I.D Principles
Published: 2023-05-15 10:36:35

# SOLID Principles

These are the oldest principles but we are still using these priciples due to maintains's
OO systems improve reusabulity and easy testing.
### S.O.L.I.D 
- SRP - Single responsibility principle 
- DIP - Dependency inversion principle 
- OCP - Open/closed principle 
- LSP - Liskov substitution principle
- ISP - Interface segregation principle


# SRP - Single responsibility principle:

A class should have only one responsibilty all the methods should be associated with the 
that responsibility.

example:
``` python
class Email:
    def __init__(self, subject, body, sender, recipient):
         self.subject = subject
        self.body = body
        self.sender = sender
        self.recipient = recipien
class EmailSender:
    def send_email(self, email):
        # Code to send the email using the SMTP server
        pass
class EmailSaver:
    def save_email(self, email):
        # Code to save the email to a database
        pass
```

In this example, we have separate classes for each responsibility: `Email` for storing email data, `EmailSender` for sending emails, and `EmailSaver` for saving emails to a database. Each class has a single responsibility, making the system easier to understand, modify, and maintain.

### OCP - Open/closed principle  :
The Open-Closed Principle (OCP) is a fundamental principle in object-oriented programming that states that software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. In other words, the behavior of a system should be extendable without modifying its existing code.

``` python
from abc import ABC, abstractmethod

# Abstract class representing a shape
class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

# Concrete implementation of a Rectangle shape
class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    def area(self):
        return self.width * self.height

# Concrete implementation of a Circle shape
class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius
    
    def area(self):
        return 3.14 * self.radius ** 2

# Function that calculates the sum of areas for multiple shapes
def calculate_total_area(shapes):
    total_area = 0
    for shape in shapes:
        total_area += shape.area()
    return total_area
```
, if we want to add a `Triangle` shape, we can create a new class `Triangle` that inherits from `Shape` and provides its own implementation of the `area()` method. We can then pass an instance of `Triangle` to the `calculate_total_area()` function, and it will work seamlessly without modifying the existing code.

# LSP - Liskov substitution principle:

The Liskov Substitution Principle (LSP) is a principle in object-oriented programming that states that objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program. In other words, if a program is designed to work with a certain type of object, it should also work correctly with any subtype of that object.

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

class Dog(Animal):
    def make_sound(self):
        return "Woof!"

class Cat(Animal):
    def make_sound(self):
        return "Meow!"

def animal_sounds(animals):
    for animal in animals:
        print(animal.make_sound())
```
The `animal_sounds()` function demonstrates the Liskov Substitution Principle. It takes a list of `Animal` objects and calls the `make_sound()` method on each object. By designing the program to work with the `Animal` superclass, it can also work correctly with any subtype of `Animal`, such as `Dog` or `Cat`.

```python
animals = [Dog(), Cat()]

animal_sounds(animals)
```

The `animal_sounds()` function doesn't need to know the exact type of each object in the list. It treats them all as `Animal` objects and calls the `make_sound()` method, which is overridden in the subclasses. As a result, it will print "Woof!" and "Meow!" without any issues.


### ISP - Interface segregation principle:
The Interface Segregation Principle (ISP) is a principle in object-oriented programming that states that clients should not be forced to depend on interfaces they do not use. It suggests that classes should have narrowly focused interfaces, tailored to the specific needs of the clients that use them, rather than having large, bloated interfaces that encompass more functionality than necessary.

```pyhton
from abc import ABC, abstractmethod

# Interface for a document
class Document(ABC):
    @abstractmethod
    def open(self):
        pass

    @abstractmethod
    def close(self):
        pass

# Interface for a printable document
class Printable(ABC):
    @abstractmethod
    def print_document(self):
        pass

# Interface for a scannable document
class Scannable(ABC):
    @abstractmethod
    def scan_document(self):
        pass

# Implementation of a Word document
class WordDocument(Document, Printable):
    def open(self):
        print("Opening Word document")

    def close(self):
        print("Closing Word document")

    def print_document(self):
        print("Printing Word document")

# Implementation of a PDF document
class PdfDocument(Document, Printable, Scannable):
    def open(self):
        print("Opening PDF document")

    def close(self):
        print("Closing PDF document")

    def print_document(self):
        print("Printing PDF document")

    def scan_document(self):
        print("Scanning PDF document")

# Client code that only depends on the required interfaces
def print_document(printable):
    printable.print_document()

def scan_document(scannable):
    scannable.scan_document()
```
The client code demonstrates the usage of the specific interfaces. The `print_document()` function only depends on the `Printable` interface, allowing it to print any document that implements that interface. Similarly, the `scan_document()` function only depends on the `Scannable` interface, allowing it to scan any document that implements that interface.

### DIP - Dependency inversion principle :

The Dependency Inversion Principle (DIP) is a principle in object-oriented programming that states that high-level modules should not depend on low-level modules. Instead, both should depend on abstractions. It suggests that the dependencies between modules should be based on abstractions rather than concrete implementations.

``` pyhton
from abc import ABC, abstractmethod

# Abstract class representing a notifier
class Notifier(ABC):
    @abstractmethod
    def send_notification(self, message):
        pass

# High-level module that depends on the Notifier abstraction
class NotificationService:
    def __init__(self, notifier):
        self.notifier = notifier

    def send_notification(self, message):
        self.notifier.send_notification(message)

# Low-level module that implements the Notifier abstraction
class EmailNotifier(Notifier):
    def send_notification(self, message):
        print(f"Sending email notification: {message}")

# Low-level module that implements the Notifier abstraction
class SMSNotifier(Notifier):
    def send_notification(self, message):
        print(f"Sending SMS notification: {message}")
```

By having the `NotificationService` depend on the `Notifier` abstraction rather than concrete implementations, we adhere to the Dependency Inversion Principle. The `NotificationService` is not tightly coupled to specific notifiers (e.g., `EmailNotifier` or `SMSNotifier`), but instead depends on the abstraction provided by the `Notifier` class. This allows for flexibility and extensibility in the system. We can introduce new notifiers by creating classes that implement the `Notifier` interface, and the `NotificationService` will work with them without any modifications.

```python
email_notifier = EmailNotifier()
sms_notifier = SMSNotifier()

notification_service = NotificationService(email_notifier)
notification_service.send_notification("Hello, this is an email notification")

notification_service = NotificationService(sms_notifier)
notification_service.send_notification("Hello, this is an SMS notification")
```