Programming Paradigms

What is a Programming Paradigm?

A programming paradigm is a fundamental style or approach to programming. It defines how you structure your code, organize logic, and think about problem-solving.

Key paradigms:

Important: Most modern languages support multiple paradigms (multi-paradigm languages).

Object-Oriented Programming (OOP)

Core Idea

Model the problem domain as a collection of objects that contain both data (attributes) and behavior (methods). Objects interact through well-defined interfaces.

The Four Pillars of OOP

1. Encapsulation

Bundle data and methods that operate on that data within a single unit (class). Hide internal implementation details.

Python Example:
class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.__balance = balance  # Private attribute (name mangling)

    def deposit(self, amount):
        if amount > 0:
            self.__balance += amount
            return True
        return False

    def withdraw(self, amount):
        if 0 < amount <= self.__balance:
            self.__balance -= amount
            return True
        return False

    def get_balance(self):
        return self.__balance

# Usage
account = BankAccount("Alice", 1000)
account.deposit(500)
print(account.get_balance())  # 1500

# Direct access prevented (by convention/name mangling)
# account.__balance  # AttributeError
Benefits:
  • Control how data is accessed and modified
  • Can change internal implementation without breaking external code
  • Prevent invalid states

2. Inheritance

Create new classes based on existing classes, inheriting their attributes and methods. Promotes code reuse.

Python Example:
class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        raise NotImplementedError("Subclass must implement")

    def sleep(self):
        return f"{self.name} is sleeping"

class Dog(Animal):  # Inherit from Animal
    def speak(self):
        return f"{self.name} says Woof!"

class Cat(Animal):
    def speak(self):
        return f"{self.name} says Meow!"

# Usage
dog = Dog("Buddy")
cat = Cat("Whiskers")
print(dog.speak())   # Buddy says Woof!
print(cat.sleep())   # Whiskers is sleeping (inherited)
Types of Inheritance:
  • Single: Class inherits from one parent
  • Multiple: Class inherits from multiple parents (Python supports, Java doesn't)
  • Multilevel: Chain of inheritance (A → B → C)
  • Hierarchical: Multiple classes inherit from one parent

3. Polymorphism

"Many forms" - Same interface, different implementations. Allows treating objects of different types uniformly.

Python Example:
# Method overriding (runtime polymorphism)
class Shape:
    def area(self):
        pass

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.14159 * self.radius ** 2

# Polymorphic behavior
shapes = [Rectangle(5, 10), Circle(7), Rectangle(3, 4)]

for shape in shapes:
    print(f"Area: {shape.area()}")  # Same method, different behavior

# Works because all shapes implement area()
# Don't need to know specific type
Types:
  • Compile-time (Static): Method overloading, operator overloading
  • Runtime (Dynamic): Method overriding, duck typing

4. Abstraction

Hide complex implementation details, expose only essential features. Focus on "what" an object does, not "how".

Python Example:
from abc import ABC, abstractmethod

class PaymentProcessor(ABC):  # Abstract Base Class
    @abstractmethod
    def process_payment(self, amount):
        """Process payment - must be implemented by subclasses"""
        pass

    @abstractmethod
    def refund(self, transaction_id, amount):
        """Process refund - must be implemented by subclasses"""
        pass

class StripePayment(PaymentProcessor):
    def process_payment(self, amount):
        # Stripe-specific implementation
        return f"Processing ${amount} via Stripe"

    def refund(self, transaction_id, amount):
        return f"Refunding ${amount} via Stripe"

class PayPalPayment(PaymentProcessor):
    def process_payment(self, amount):
        # PayPal-specific implementation
        return f"Processing ${amount} via PayPal"

    def refund(self, transaction_id, amount):
        return f"Refunding ${amount} via PayPal"

# Client code works with abstraction
def checkout(processor: PaymentProcessor, amount):
    result = processor.process_payment(amount)
    print(result)

# Can swap implementations easily
checkout(StripePayment(), 100)
checkout(PayPalPayment(), 100)

SOLID Principles

Five design principles for maintainable OOP code:

S - Single Responsibility Principle (SRP)

A class should have one, and only one, reason to change.

# Bad: Multiple responsibilities
class User:
    def __init__(self, name):
        self.name = name

    def save_to_database(self):  # Database concern
        pass

    def send_email(self):  # Email concern
        pass

# Good: Separate concerns
class User:
    def __init__(self, name):
        self.name = name

class UserRepository:
    def save(self, user):
        pass

class EmailService:
    def send(self, user, message):
        pass

O - Open/Closed Principle

Classes should be open for extension, closed for modification.

# Bad: Must modify class to add new discount type
class DiscountCalculator:
    def calculate(self, order, discount_type):
        if discount_type == "percentage":
            return order.total * 0.9
        elif discount_type == "fixed":
            return order.total - 10

# Good: Extend without modifying
class Discount(ABC):
    @abstractmethod
    def apply(self, total):
        pass

class PercentageDiscount(Discount):
    def apply(self, total):
        return total * 0.9

class FixedDiscount(Discount):
    def apply(self, total):
        return total - 10

L - Liskov Substitution Principle

Subclasses should be substitutable for their base classes without breaking functionality.

# Bad: Violates LSP
class Bird:
    def fly(self):
        return "Flying"

class Penguin(Bird):
    def fly(self):
        raise Exception("Penguins can't fly!")  # Breaks contract

# Good: Design hierarchy correctly
class Bird:
    pass

class FlyingBird(Bird):
    def fly(self):
        return "Flying"

class Penguin(Bird):
    def swim(self):
        return "Swimming"

I - Interface Segregation Principle

Clients shouldn't be forced to depend on interfaces they don't use.

# Bad: Fat interface
class Worker(ABC):
    @abstractmethod
    def work(self): pass

    @abstractmethod
    def eat(self): pass

class Robot(Worker):
    def work(self): return "Working"
    def eat(self): pass  # Robots don't eat! Forced to implement

# Good: Segregated interfaces
class Workable(ABC):
    @abstractmethod
    def work(self): pass

class Eatable(ABC):
    @abstractmethod
    def eat(self): pass

class Human(Workable, Eatable):
    def work(self): return "Working"
    def eat(self): return "Eating"

class Robot(Workable):
    def work(self): return "Working"

D - Dependency Inversion Principle

Depend on abstractions, not concretions. High-level modules shouldn't depend on low-level modules.

# Bad: High-level depends on low-level
class MySQLDatabase:
    def save(self, data): pass

class UserService:
    def __init__(self):
        self.db = MySQLDatabase()  # Tightly coupled!

# Good: Depend on abstraction
class Database(ABC):
    @abstractmethod
    def save(self, data): pass

class MySQLDatabase(Database):
    def save(self, data): pass

class UserService:
    def __init__(self, database: Database):
        self.db = database  # Can inject any Database implementation

Composition vs Inheritance

Inheritance ("is-a")

class Engine:
    def start(self):
        return "Engine starting"

class Car(Engine):  # Car IS-A Engine?
    pass

# Problems:
# - Tight coupling
# - Can't change at runtime
# - Fragile base class problem

Composition ("has-a")

class Engine:
    def start(self):
        return "Engine starting"

class Car:
    def __init__(self):
        self.engine = Engine()  # Car HAS-A Engine

    def start(self):
        return self.engine.start()

# Benefits:
# - Loose coupling
# - Can change at runtime
# - More flexible
Principle: Favor composition over inheritance. Use inheritance only when there's a true "is-a" relationship and shared behavior.

Functional Programming (FP)

Core Idea

Treat computation as the evaluation of mathematical functions. Avoid changing state and mutable data. Functions are first-class citizens.

Key Concepts

1. Pure Functions

Same input always produces same output. No side effects (doesn't modify external state).

# Impure function (side effects)
total = 0
def add_to_total(x):
    global total
    total += x  # Modifies external state!
    return total

# Pure function (no side effects)
def add(x, y):
    return x + y  # Only depends on inputs, no state change

# Benefits of pure functions:
# - Testable (no setup needed)
# - Cacheable (memoization)
# - Parallelizable (no shared state)
# - Predictable (referential transparency)

2. Immutability

Data cannot be changed after creation. Create new data instead of modifying existing.

# Mutable (imperative style)
numbers = [1, 2, 3]
numbers.append(4)  # Modifies original list

# Immutable (functional style)
numbers = (1, 2, 3)  # Tuple (immutable)
new_numbers = numbers + (4,)  # Creates new tuple

# Python immutable types: int, float, str, tuple, frozenset
# Mutable types: list, dict, set

# Immutability benefits:
# - Thread-safe (no race conditions)
# - Easier to reason about
# - Can share safely

3. First-Class Functions

Functions are values - can be assigned to variables, passed as arguments, returned from other functions.

# Assign function to variable
def greet(name):
    return f"Hello, {name}"

say_hello = greet
print(say_hello("Alice"))  # Hello, Alice

# Pass function as argument
def apply_twice(func, x):
    return func(func(x))

def double(x):
    return x * 2

print(apply_twice(double, 5))  # 20

# Return function from function
def create_multiplier(n):
    def multiplier(x):
        return x * n
    return multiplier

times_three = create_multiplier(3)
print(times_three(10))  # 30

4. Higher-Order Functions

Functions that take functions as arguments or return functions.

# Map: Apply function to each element
numbers = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x**2, numbers))
# [1, 4, 9, 16, 25]

# Filter: Keep elements matching predicate
evens = list(filter(lambda x: x % 2 == 0, numbers))
# [2, 4]

# Reduce: Combine elements into single value
from functools import reduce
total = reduce(lambda acc, x: acc + x, numbers, 0)
# 15

# List comprehensions (Pythonic alternative)
squared = [x**2 for x in numbers]
evens = [x for x in numbers if x % 2 == 0]

5. Closures

Function that captures variables from enclosing scope.

def make_counter():
    count = 0  # Captured by closure

    def increment():
        nonlocal count
        count += 1
        return count

    return increment

counter1 = make_counter()
counter2 = make_counter()

print(counter1())  # 1
print(counter1())  # 2
print(counter2())  # 1 (separate closure)

6. Function Composition

Combine simple functions to build complex behavior.

def compose(f, g):
    return lambda x: f(g(x))

def add_one(x):
    return x + 1

def double(x):
    return x * 2

# Compose functions
add_then_double = compose(double, add_one)
print(add_then_double(5))  # (5 + 1) * 2 = 12

# Pipe operator (compose in reverse)
def pipe(*funcs):
    def piped(x):
        result = x
        for func in funcs:
            result = func(result)
        return result
    return piped

process = pipe(add_one, double, lambda x: x - 3)
print(process(5))  # ((5 + 1) * 2) - 3 = 9

7. Recursion

Function calls itself. Replaces loops in functional programming.

# Imperative (loop)
def factorial_loop(n):
    result = 1
    for i in range(1, n + 1):
        result *= i
    return result

# Functional (recursion)
def factorial_recursive(n):
    if n <= 1:
        return 1
    return n * factorial_recursive(n - 1)

# Tail recursion (optimizable)
def factorial_tail(n, acc=1):
    if n <= 1:
        return acc
    return factorial_tail(n - 1, n * acc)

print(factorial_recursive(5))  # 120

Functional Programming Benefits

Benefit Explanation
Easier Testing Pure functions = no mocks, no setup, deterministic
Concurrency Immutability eliminates race conditions
Reasoning No hidden state changes, easier to understand
Modularity Small, composable functions
Debugging No temporal coupling, easier to reproduce bugs

Functional Programming Drawbacks

Procedural Programming

Core Idea

Organize code into procedures (functions/subroutines) that operate on data. Focus on step-by-step instructions.

C Example:
// Procedural style - data and functions separate
struct BankAccount {
    char owner[50];
    double balance;
};

void deposit(struct BankAccount* account, double amount) {
    if (amount > 0) {
        account->balance += amount;
    }
}

void withdraw(struct BankAccount* account, double amount) {
    if (amount > 0 && amount <= account->balance) {
        account->balance -= amount;
    }
}

// Usage
struct BankAccount account = {"Alice", 1000.0};
deposit(&account, 500);
withdraw(&account, 200);
Python Procedural Style:
# Data and functions separate
bank_accounts = {}

def create_account(owner, initial_balance=0):
    bank_accounts[owner] = initial_balance

def deposit(owner, amount):
    if owner in bank_accounts and amount > 0:
        bank_accounts[owner] += amount

def withdraw(owner, amount):
    if owner in bank_accounts and 0 < amount <= bank_accounts[owner]:
        bank_accounts[owner] -= amount

# Usage
create_account("Alice", 1000)
deposit("Alice", 500)
withdraw("Alice", 200)

Characteristics

Procedural vs OOP

Aspect Procedural OOP
Organization Around functions Around objects (data + behavior)
Data/Functions Separate Encapsulated together
Access Data often global or passed explicitly Data hidden, accessed through methods
Reuse Function reuse Inheritance, polymorphism
Best For Small programs, scripts, algorithms Large, complex systems

Declarative vs Imperative

Imperative Programming

"How to do it" - Explicit step-by-step instructions to achieve a goal.

# Imperative: Describe HOW to filter and transform
numbers = [1, 2, 3, 4, 5, 6]
result = []

for num in numbers:
    if num % 2 == 0:  # Filter even numbers
        result.append(num * 2)  # Double them

print(result)  # [4, 8, 12]

# You control the flow: loop, if, append

Declarative Programming

"What you want" - Describe the desired result, not the steps.

Python (Functional style - declarative):
# Declarative: Describe WHAT you want
numbers = [1, 2, 3, 4, 5, 6]
result = [num * 2 for num in numbers if num % 2 == 0]
print(result)  # [4, 8, 12]

# Don't specify how to iterate, just what to produce
SQL (Declarative):
-- Declarative: What data you want, not how to get it
SELECT name, salary * 1.1 AS new_salary
FROM employees
WHERE department = 'Engineering'
ORDER BY salary DESC;

-- Database engine figures out HOW to execute efficiently
HTML (Declarative):
<!-- Describe what you want displayed, not how to render -->
<div class="card">
    <h2>Title</h2>
    <p>Content</p>
</div>

<!-- Browser handles rendering details -->
React (Declarative UI):
// Declarative: Describe UI based on state
function Counter() {
    const [count, setCount] = useState(0);

    return (
        <div>
            <p>Count: {count}</p>
            <button onClick={() => setCount(count + 1)}>
                Increment
            </button>
        </div>
    );
}

// vs Imperative (manual DOM manipulation):
// document.getElementById('count').textContent = count;
// document.getElementById('button').addEventListener('click', ...)

Comparison

Aspect Imperative Declarative
Focus How (control flow) What (desired result)
Abstraction Low-level High-level
Examples C, Java, Python (imperative parts) SQL, HTML, CSS, React
Control Explicit loops, conditionals Hidden, handled by framework
Best For Performance-critical code, algorithms Business logic, UI, queries

Other Paradigms

Logic Programming

Express logic in terms of relations and rules. Program is a set of logical facts and rules.

Prolog Example:
% Facts
parent(tom, bob).
parent(tom, liz).
parent(bob, ann).
parent(bob, pat).

% Rules
grandparent(X, Y) :- parent(X, Z), parent(Z, Y).

% Query
?- grandparent(tom, ann).  % true
?- grandparent(tom, Who).  % Who = ann; Who = pat

Event-Driven Programming

Program flow determined by events (user actions, sensor outputs, messages).

JavaScript Example:
// Register event handlers
button.addEventListener('click', () => {
    console.log('Button clicked!');
});

window.addEventListener('load', () => {
    console.log('Page loaded!');
});

// Flow controlled by user interactions, not linear execution

Reactive Programming

Asynchronous data streams and propagation of change.

RxJS Example:
// Stream of click events
const clicks = fromEvent(button, 'click');

// Transform stream
clicks
    .pipe(
        debounceTime(300),
        map(event => event.clientX)
    )
    .subscribe(x => console.log(x));

Choosing the Right Paradigm

When to Use Each

Paradigm Best For Examples
OOP - Large, complex systems
- Modeling real-world entities
- Code reuse through inheritance
- Team collaboration
Enterprise apps, games, GUIs
Functional - Data transformations
- Concurrent/parallel processing
- Mathematical computations
- Avoiding side effects
Data pipelines, compilers, parsers
Procedural - Small programs
- Scripts and utilities
- Performance-critical code
- Simple algorithms
System utilities, embedded systems
Declarative - Database queries
- UI descriptions
- Configuration
- Business rules
SQL queries, HTML/CSS, React

Multi-Paradigm Approach

Modern best practice: Use the paradigm that best fits each problem within the same codebase.

Python Example - Combining Paradigms:
# OOP for domain models
class Order:
    def __init__(self, items):
        self.items = items
        self.status = "pending"

    def total(self):
        # Functional approach for calculation
        return sum(item.price for item in self.items)

# Functional for data transformation
def apply_discount(orders, discount_rate):
    return [
        Order([item for item in order.items])
        for order in orders
        if order.total() > 100
    ]

# Procedural for script-like tasks
def main():
    orders = load_orders()
    discounted = apply_discount(orders, 0.1)
    save_orders(discounted)

if __name__ == "__main__":
    main()

Key Takeaways

Interview Tips

When asked about paradigms: