Programing paradims

  • impreative
  • procedural
  • object oriented
  • functional
  • aspect oriented
  • logic

1. Follow SOLID Principles:

  • S: Single Responsibility – One class, one job.
  • O: Open/Closed – Extend without modifying.
  • L: Liskov Substitution – Subclasses should fit without breaking.
  • I: Interface Segregation – No forced methods.
  • D: Dependency Inversion – Depend on abstractions, not concrete classes.
class Order:
    def __init__(self):
        self.payment = PayPal()  # Hardcoded dependency


class Order:
    def __init__(self, payment_method):
        self.payment = payment_method  # Inject dependency

2. Encapsulation: Hide internal details; expose only necessary methods.
3. Prefer Composition Over Inheritance: Avoid deep class hierarchies.
4. Law of Demeter: Don’t chain method calls, use delegation.
5. Keep It Simple (KISS): Avoid overengineering; don’t use patterns unnecessarily.
6. Use Design Patterns When Needed: Factory, Singleton, Observer, Strategy, etc.

Before explaining SOLID, it helps to identify the minimal building blocks the idea depends on. The principles only make sense if you already understand these concepts:

  1. Classes and objects
  2. Methods and responsibilities inside a class
  3. Interfaces / abstract types
  4. Inheritance and polymorphism
  5. Coupling and dependencies between modules

You’re a developer, so you likely know these already. If any of them feel unclear, tell me and we can unpack them first.
For now, I’ll build the mental model starting from the problem SOLID tries to solve.

Imagine software as a network of modules that depend on each other.

Example:

UI → Service → Database

Each arrow means dependency.

The moment dependencies become tangled:

UI → Service → Database
       ↓
    Payment
       ↓
     Email

Changes in one place start breaking many others.

This creates three major engineering problems:

  1. Fragility – small changes break many things

  2. Rigidity – code becomes hard to modify

  3. Immobility – code becomes hard to reuse

SOLID is essentially a set of constraints on how dependencies should be structured to avoid those problems.

Think of SOLID as rules for shaping the dependency graph of your program.

2. S — Single Responsibility Principle (SRP)

Idea

A module should have only one reason to change.

This is not just about “one job”.
It is about one axis of change.

Bad Design

class UserService {
  createUser()
  sendWelcomeEmail()
  generateUserReport()
}

Why might this class change?

  • User creation logic changes
  • Email system changes
  • Reporting format changes

Three unrelated reasons.

So the class mixes three responsibilities.

Better Design

class UserService {
  createUser()
}

class EmailService {
  sendWelcomeEmail()
}

class UserReportService {
  generateReport()
}

Now each class changes for one reason only.

Why This Works Mechanistically

Because it reduces the probability that a change affects unrelated behavior.

Instead of:

Change A → class → breaks feature B

you get:

Change A → small module

This reduces blast radius.

3. O — Open / Closed Principle (OCP)

Idea

Software should be:

Open for extension
Closed for modification

Meaning:

You should add behavior without changing existing code.

Problem Example

class PaymentProcessor {
  process(paymentType) {
    if (paymentType == "card") ...
    if (paymentType == "paypal") ...
  }
}

Adding a new payment method requires editing the class.

This is dangerous because modifying stable code risks introducing bugs.

OCP Design

Use polymorphism.

interface PaymentMethod {
  pay()
}

class CardPayment implements PaymentMethod {
  pay()
}

class PaypalPayment implements PaymentMethod {
  pay()
}

Processor:

class PaymentProcessor {
  process(method: PaymentMethod) {
    method.pay()
  }
}

Now adding a new payment:

class CryptoPayment implements PaymentMethod

No existing code changes.

4. L Liskov Substitution Principle (LSP)

Idea

If B is a subtype of A, then B should behave like A.

Formally:

Objects of subclass should replace base class
without breaking correctness.

Classic Violation

class Bird {
  fly()
}

class Penguin extends Bird {
  fly() { throw Error }
}

Penguins cannot fly.

But the type system says they can.

Why This Breaks Programs

If code expects:

function makeBirdFly(bird: Bird) {
  bird.fly()
}

Passing Penguin breaks it.

Correct Model

class Bird {}

class FlyingBird extends Bird {
  fly()
}

class Penguin extends Bird {}

Now the behavior matches the abstraction.

Mechanistic Rule

Subtypes must preserve the behavioral contract.

They must not:

  • strengthen preconditions
  • weaken guarantees
  • violate invariants

5. I — Interface Segregation Principle (ISP)

Idea

Clients should not depend on methods they don’t use.

Bad Interface

interface Worker {
  work()
  eat()
}

Now robots must implement:

class Robot implements Worker {
  work()
  eat() { throw error }
}

The interface forced unnecessary behavior.


Good Design

Split interfaces.

interface Workable {
  work()
}

interface Eatable {
  eat()
}

Now:

class Robot implements Workable
class Human implements Workable, Eatable

Why This Works

It reduces unnecessary dependencies.

Instead of:

module → large interface

you get:

module → small focused interface

6. D — Dependency Inversion Principle (DIP)

This is the most important principle in SOLID.


Default Dependency Direction

Without DIP:

High-level module → low-level module

Example:

OrderService → MySQLDatabase

This creates tight coupling.


DIP Rule

Both should depend on abstractions.

OrderService → DatabaseInterface
MySQLDatabase → DatabaseInterface

Graph becomes:

        DatabaseInterface
        ↑              ↑
OrderService      MySQLDatabase

Example

interface Database {
  save(order)
}

Implementation:

class MySQLDatabase implements Database
class MongoDatabase implements Database

Service:

class OrderService {
  constructor(db: Database) {
    this.db = db
  }
}

Now database can change without touching business logic.

If you’d like, I can also show something very useful for interviews and real engineering:

How the 5 SOLID principles collapse into just 2 deeper rules used in modern architecture.
Once you see that, SOLID becomes much easier to reason about.

Imperative Programming

  • Focus: Step-by-step instructions to achieve a task.
  • You tell the computer HOW to do something.
total = 0
numbers = [1, 2, 3, 4, 5]
 
for num in numbers:
    total += num  # Explicitly updating total
 
print(total)  # 15
 

Declarative Programming

  • Focus: Describe what should be done, not how.
  • You tell the computer WHAT you want, and it figures out the details.
numbers = [1, 2, 3, 4, 5]
total = sum(numbers)  # No explicit loop
 
print(total)  # 15
 

Program to an interface, not to an implementation.

  • Interface = a contract that defines what a class must do, without saying how it does it.
  • Implementation = the concrete logic (how it’s done).

When you program to an interface, your code only cares about what the object can do, not how it does it.

This makes our code:

  • Flexible
  • Maintainable
  • Easier to test
  • Loosely coupled

Example

  • “I need a writing tool.” ✅ This is like programming to an interface.
  • “I need a blue Parker fountain pen.” ❌ This is like programming to an implementation.

By depending on “a writing tool”, you leave the door open for:

  • A pen
  • A pencil
  • A typewriter
  • A keyboard

OOPS Principle

 
class FlyBehavior {
  fly() {
    throw new Error("This method should be overridden");
  }
}
 
class FlyWithWings extends FlyBehavior {
  fly() {
    console.log("I'm flying with wings!");
  }
}
 
class FlyNoWay extends FlyBehavior {
  fly() {
    console.log("I can't fly.");
  }
}
 
// === Quack Behaviors ===
class QuackBehavior {
  quack() {
    throw new Error("This method should be overridden");
  }
}
 
class Quack extends QuackBehavior {
  quack() {
    console.log("Quack!");
  }
}
 
class Squeak extends QuackBehavior {
  quack() {
    console.log("Squeak!");
  }
}
 
class MuteQuack extends QuackBehavior {
  quack() {
    console.log("<< Silence >>");
  }
}
 
// === Duck Base Class ===
class Duck {
  constructor(flyBehavior, quackBehavior) {
    this.flyBehavior = flyBehavior;
    this.quackBehavior = quackBehavior;
  }
 
  performFly() {
    this.flyBehavior.fly();
  }
 
  performQuack() {
    this.quackBehavior.quack();
  }
 
  swim() {
    console.log("All ducks float, even decoys!");
  }
 
  setFlyBehavior(fb) {
    this.flyBehavior = fb;
  }
 
  setQuackBehavior(qb) {
    this.quackBehavior = qb;
  }
 
  display() {
    throw new Error("Subclasses must override display()");
  }
}
 
// === Duck Variants ===
class MallardDuck extends Duck {
  display() {
    console.log("I'm a Mallard Duck");
  }
}
 
class RedheadDuck extends Duck {
  display() {
    console.log("I'm a Redhead Duck");
  }
}
 
class RubberDuck extends Duck {
  display() {
    console.log("I'm a Rubber Duck");
  }
}
 
class DecoyDuck extends Duck {
  display() {
    console.log("I'm a Decoy Duck");
  }
}
 
// === Simulation ===
function simulate() {
  const mallard = new MallardDuck(new FlyWithWings(), new Quack());
  mallard.display();
  mallard.swim();
  mallard.performFly();
  mallard.performQuack();
 
  console.log("\n-- Changing behavior at runtime --");
  mallard.setFlyBehavior(new FlyNoWay());
  mallard.setQuackBehavior(new MuteQuack());
  mallard.performFly();
  mallard.performQuack();
 
  console.log("\n-- Rubber Duck Example --");
  const rubber = new RubberDuck(new FlyNoWay(), new Squeak());
  rubber.display();
  rubber.performFly();
  rubber.performQuack();
}
 
simulate();
 

Encapsulation

  • Fly and quack behaviors are encapsulated in their own classes.
  • Internal implementation details are hidden from the Duck class.

Polymorphism

  • We use interfaces (base classes in JS) and override methods (fly(), `quack()
  • Ducks can “behave differently” depending on which behavior class they use.
  • Polymorphism is changing the code in runtime if you see the base duck class it just have flyBehavior and quackBehavior as interface where they will be choosed on runtime

Inheritance

  • FlyWithWings, FlyNoWay inherit from FlyBehavior.
  • MallardDuck, RedheadDuck inherit from Duck.

Composition over Inheritance

  • Rather than hardcoding behavior in Duck, we compose it with external behavior objects.
  • This allows changing behavior at runtime via setFlyBehavior() or setQuackBehavior().

Open/Closed Principle (SOLID)

  • The Duck class is open for extension (new behaviors), but closed for modification (no need to change existing code).

Dependency Inversion Principle

High-level modules should not depend on low-level modules. Both should depend on abstractions.

Abstractions should not depend on details. Details should depend on abstractions.

  • No variable should hold a reference to a concrete class
  • No class should derive from a concrete class
  • No method should override implemented methods of a base class

Example

// High-level module
class NotificationService {
    private SmtpEmailSender emailSender = new SmtpEmailSender(); // depends on concrete class
 
    public void send(String message) {
        emailSender.sendEmail(message); // tightly coupled
    }
}
 
// Low-level module
class SmtpEmailSender {
    public void sendEmail(String message) {
        System.out.println("Sending email via SMTP: " + message);
    }
}
 
  • NotificationService is tightly coupled to SmtpEmailSender.
  • we can’t switch to another email sender (e.g., SendGrid) without changing the high-level module.
// Abstraction
interface EmailSender {
    void sendEmail(String message);
}
 
// Low-level module
class SmtpEmailSender implements EmailSender {
    public void sendEmail(String message) {
        System.out.println("Sending email via SMTP: " + message);
    }
}
 
// High-level module
class NotificationService {
    private EmailSender emailSender; // depends on abstraction
 
    public NotificationService(EmailSender emailSender) {
        this.emailSender = emailSender;
    }
 
    public void send(String message) {
        emailSender.sendEmail(message);
    }
}
 
  • But we are sure that emailsender will not change then we can voilate the rule