# Why SOLID Principles

Writing code not only involves proper syntax, reaching customer requirements, applying large algorithms but also includes maintaining the code for long run, writing code understandable, structuring the code.

The **SOLID principles** are a set of five design principles in **object-oriented programming (OOP)**. They’re meant to make software:

* Easier to understand
    
* More flexible
    
* Less error-prone when changing or extending
    

---

Software systems or application **grow over time** — new features, bug fixes, changing business needs. Without discipline, the codebase turns into spaghetti code.

* Hard to understand
    
* Risky to change
    
* Expensive to maintain
    

The **SOLID principles** is a **guideline** to write code that is:

## Maintainability

* Code written with SOLID is easier to maintain because responsibilities are clear and separated.
    
* ```python
      class Notification:
          def send(self, message, type):
              if type == "email":
                  print(f"Sending Email: {message}")
              elif type == "sms":
                  print(f"Sending SMS: {message}")
    ```
    
    For example, imagine you write one class that sends **all kinds of notifications**
    

Say need to add *“Add WhatsApp notifications”*, you must open this class and change it.

* Risk: You might break Email or SMS code while adding WhatsApp.
    
* *Testing becomes harder because everything is mixed together*.
    

## Reusability

Write code once and reuse it everywhere without *rewriting the same logic* again and again. This makes software faster to build, easier to maintain, and less error prone.

```python
# ❌ Bad: Duplicated logic
class Rectangle:
    def area(self, width, height):
        return width * height

class Square:
    def area(self, side):
        return side * side
```

Problem:

* Every shape writes its own `area()` method differently.
    
* If you add a `Circle`, you must repeat code again.
    
* No shared structure → harder to maintain.
    

Smaller, well-defined classes/methods are easier to reuse across projects.

## Scalability

System/application requirements keep on growing on one feature. To handle growth safely, we need a design that allows

* Adding new feature easily
    
* Keeping existing classes untouched
    
* Testing each part separately
    

When requirements change you can extend existing code instead of rewriting it.

```python
elif type == "whatsapp":
    print(f"WhatsApp: {message}")
```

Considering the same example given to maintainability, if the business wants ‘WhatsApp notifications’, and again for Slack, Telegram, Teams… → this class keeps growing

* Each time you edit this class, there’s a chance of breaking existing email/SMS logic.
    
* Bugs creep in when requirements grow.
    
* *You can’t test email separately from SMS — everything is mixed inside one method*.
    
* The more types you add, the harder testing becomes.
    
* The service is directly tied to all notification types.
    
* You can’t reuse “just email” or “just SMS” in another project without copying everything.
    

## Flexibility & Adaptability

In real projects, requirements **always change** (*new features, new APIs, new rules*). If your code is rigid, every small change means breaking things.

```python
class ReportGenerator:
    def generate(self, data, format):
        if format == "pdf":
            print("Generating PDF Report...")
        elif format == "excel":
            print("Generating Excel Report...")
        elif format == "html":
            print("Generating HTML Report...")
```

Why is this not flexible?

1. Every time a *new format* (e.g., CSV, JSON) is needed → you must ***edit this class.***
    
2. Old code keeps changing → ***risk of breaking existing functionality.***
    
3. Hard to adapt → you can’t reuse just “PDF report” somewhere else.
    

## Reduced Complexity

* **Complexity** = when code is hard to read, understand, and reason about.
    
* **Reduced complexity** = breaking problems into **smaller, understandable pieces**.
    

```python
class Order:
    def __init__(self, items):
        self.items = items

    def process_order(self):
        # calculate total
        total = 0
        for item in self.items:
            total += item["price"] * item["qty"]

        # apply discount
        if total > 500:
            total *= 0.9

        # save to database
        print("Saving order to database...")

        # send confirmation email
        print("Sending confirmation email...")
```

### Why is this complex?

* **Too many responsibilities** (calculation, discount, DB, email).
    
* Hard to **read** — must scroll through everything.
    
* Hard to **maintain** — changing discount logic risks breaking email sending.
    
* Hard to **test** — can’t test discount separately.
