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:
- Classes and objects
- Methods and responsibilities inside a class
- Interfaces / abstract types
- Inheritance and polymorphism
- 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:
-
Fragility – small changes break many things
-
Rigidity – code becomes hard to modify
-
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
Duckclass.
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,FlyNoWayinherit fromFlyBehavior.MallardDuck,RedheadDuckinherit fromDuck.
Composition over Inheritance
- Rather than hardcoding behavior in
Duck, we compose it with external behavior objects. - This allows changing behavior at runtime via
setFlyBehavior()orsetQuackBehavior().
Open/Closed Principle (SOLID)
- The
Duckclass 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);
}
}
NotificationServiceis tightly coupled toSmtpEmailSender.- 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