Encapsulation, abstraction, inheritance, polymorphism and practical OOP design patterns.
| Pillar | Definition | Real-World Analogy |
|---|---|---|
| Encapsulation | Bundling data and methods that operate on it into a single unit; restricting direct access to internal state | A capsule — medicine is hidden inside; you only interact with the shell |
| Abstraction | Hiding complex implementation details and exposing only essential features to the user | Driving a car — you use the steering wheel and pedals, not the internal engine mechanics |
| Inheritance | Creating new classes from existing ones, acquiring their attributes and behaviors | A child inherits eye color and height from their parents, but also has unique traits |
| Polymorphism | The ability of objects to take many forms; same interface, different underlying implementations | A remote control — one "power" button works for TV, AC, and stereo, each responding differently |
| Concept | Description |
|---|---|
| Data Hiding | Making fields private/protected so they cannot be modified directly from outside |
| Accessors | Getters and setters provide controlled read/write access to private fields |
| Immutability | Making objects immutable by removing setters and using final/readonly fields |
| Information Hiding | Hiding implementation details so changes do not affect consumers |
| Tight Encapsulation | All fields are private; all access is through methods — maximizes control |
| Modifier | Same Class | Same Package | Subclass | World |
|---|---|---|---|---|
| private | ✅ | ❌ | ❌ | ❌ |
| default (package) | ✅ | ✅ | ❌ | ❌ |
| protected | ✅ | ✅ | ✅ | ❌ |
| public | ✅ | ✅ | ✅ | ✅ |
// ── Encapsulation in Java ──
public class BankAccount {
private double balance; // hidden data
private String accountNumber; // hidden data
public BankAccount(String accNum, double initialBalance) {
this.accountNumber = accNum;
if (initialBalance >= 0) this.balance = initialBalance;
}
// Controlled access via getters/setters
public double getBalance() {
return this.balance; // read-only access
}
public void deposit(double amount) {
if (amount > 0) {
this.balance += amount; // validation before modification
} else {
throw new IllegalArgumentException("Amount must be positive");
}
}
public void withdraw(double amount) {
if (amount > 0 && amount <= this.balance) {
this.balance -= amount;
} else {
throw new IllegalArgumentException("Invalid withdrawal");
}
}
}
// ── Encapsulation in Python ──
class BankAccount:
def __init__(self, acc_num: str, initial_balance: float):
self.__account_number = acc_num # name-mangled (private)
self.__balance = initial_balance
@property
def balance(self) -> float:
return self.__balance # controlled read
def deposit(self, amount: float):
if amount > 0:
self.__balance += amount
else:
raise ValueError("Amount must be positive")| Feature | Abstract Class | Interface |
|---|---|---|
| Methods | Can have both abstract and concrete methods | Only abstract methods (traditionally); default methods in Java 8+ |
| Fields | Can have instance fields (state) | Cannot have instance fields; only constants |
| Constructors | Can have constructors | Cannot have constructors |
| Multiple Inheritance | A class can extend only ONE abstract class | A class can implement MULTIPLE interfaces |
| Access Modifiers | Any access modifier allowed | Public by default (most languages) |
| Purpose | Share code among closely related classes | Define a contract for unrelated classes |
| Use When | Classes share a common identity (IS-A) | Classes need a common behavior (CAN-DO) |
| Type | Description | Example |
|---|---|---|
| Single | One child extends one parent | Dog extends Animal |
| Multilevel | Chain of inheritance: A → B → C | Grandparent → Parent → Child |
| Hierarchical | Multiple children from one parent | Car, Bike, Truck extend Vehicle |
| Hybrid | Combination of multiple types | Interfaces + single inheritance |
| Multiple | One child extends multiple parents (not in Java) | Python: class C(A, B) — MRO resolves conflicts |
| Advantage | Disadvantage |
|---|---|
| Code reusability — write once, extend many | Tight coupling — child depends on parent implementation |
| Extensibility — add features without modifying existing code | Fragile base class — changes in parent may break children |
| Method overriding enables runtime polymorphism | Diamond problem in multiple inheritance |
| Establishes natural IS-A relationships | Deep inheritance hierarchies become hard to understand |
| Supports polymorphism and dynamic binding | Overuse leads to "banana, gorilla, jungle" problem |
| Type | Binding Time | Also Called | Mechanism |
|---|---|---|---|
| Method Overloading | Compile-time | Static / Early Binding | Same name, different parameters (type, count, order) |
| Operator Overloading | Compile-time | Static Binding | Redefine operators (+, -, ==) for custom types |
| Method Overriding | Runtime | Dynamic / Late Binding | Subclass redefines parent method with same signature |
| Virtual Functions | Runtime | Dynamic Dispatch | Base class declares virtual; derived provides implementation |
// ── Compile-Time Polymorphism (Method Overloading) ──
class Calculator {
public int add(int a, int b) { return a + b; }
public double add(double a, double b) { return a + b; }
public int add(int a, int b, int c) { return a + b + c; }
// Same method name, different parameter list
}
// ── Runtime Polymorphism (Method Overriding) ──
class Animal {
public void speak() { System.out.println("Animal sound"); }
}
class Dog extends Animal {
@Override
public void speak() { System.out.println("Woof!"); }
}
class Cat extends Animal {
@Override
public void speak() { System.out.println("Meow!"); }
}
// ── Dynamic Dispatch ──
Animal myPet = new Dog(); // parent reference, child object
myPet.speak(); // prints "Woof!" — runtime resolution
Animal[] animals = { new Dog(), new Cat(), new Animal() };
for (Animal a : animals) {
a.speak(); // each calls its own overridden version
}# ── Polymorphism in Python ──
from abc import ABC, abstractmethod
from math import pi
# Abstract base class (abstraction)
class Shape(ABC):
@abstractmethod
def area(self) -> float:
pass
@abstractmethod
def perimeter(self) -> float:
pass
# Runtime polymorphism: same interface, different behavior
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
def area(self) -> float:
return pi * self.radius ** 2
def perimeter(self) -> float:
return 2 * pi * self.radius
class Rectangle(Shape):
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
def perimeter(self) -> float:
return 2 * (self.width + self.height)
# Polymorphic usage — same interface, different implementations
shapes: list[Shape] = [Circle(5), Rectangle(3, 4), Circle(10)]
for shape in shapes:
print(f"{shape.__class__.__name__}: area={shape.area():.2f}")Dog extends Animal, then everywhere you use Animal, you should be able to use Dog instead.| Concept | Description |
|---|---|
| Class | A blueprint/template that defines the structure (attributes) and behavior (methods) of objects |
| Object | An instance of a class — has its own state (data) and behavior (methods) |
| State | The values of an object's attributes at a given point in time |
| Behavior | The methods an object can execute; defined by its class |
| Identity | A unique identifier that distinguishes one object from another (memory address, ID) |
| Constructor | A special method called automatically when an object is created; initializes the object |
| Destructor | A special method called when an object is destroyed; releases resources (memory, files, connections) |
| Static Member | Belongs to the class itself, not to any instance; shared across all objects |
| Instance Member | Belongs to a specific object; each object has its own copy |
// ── Constructor Types ──
class Person {
private String name;
private int age;
private String email;
// 1. Default Constructor (no-arg)
public Person() {
this.name = "Unknown";
this.age = 0;
this.email = "N/A";
}
// 2. Parameterized Constructor
public Person(String name, int age) {
this.name = name;
this.age = age;
this.email = "N/A";
}
// 3. Copy Constructor
public Person(Person other) {
this.name = other.name;
this.age = other.age;
this.email = other.email;
}
// Constructor Chaining
public Person(String name, int age, String email) {
this(name, age); // calls parameterized constructor
this.email = email;
}
// Static member
private static int totalPersons = 0;
public Person(String name, int age) {
this.name = name;
this.age = age;
Person.totalPersons++; // static counter
}
public static int getTotalPersons() {
return totalPersons; // static method accesses static field
}
}# ── Classes & Objects in Python ──
class Person:
# Class variable (shared by all instances)
species = "Homo sapiens"
total_persons = 0
# Constructor (__init__ is Python's constructor)
def __init__(self, name: str, age: int, email: str = "N/A"):
self.name = name # instance variable
self.age = age
self.email = email
Person.total_persons += 1
# Instance method (uses self)
def introduce(self) -> str:
return f"Hi, I'm {self.name}, age {self.age}"
# Class method (uses cls, operates on class)
@classmethod
def from_birth_year(cls, name: str, birth_year: int):
import datetime
age = datetime.date.today().year - birth_year
return cls(name, age) # alternative constructor
# Static method (no self/cls — utility function)
@staticmethod
def is_adult(age: int) -> bool:
return age >= 18
# Destructor (called when object is garbage collected)
def __del__(self):
Person.total_persons -= 1
print(f"{self.name} is being destroyed")
# Dunder methods (magic methods)
def __str__(self):
return f"Person(name={self.name}, age={self.age})"
def __repr__(self):
return f"Person('{self.name}', {self.age})"
def __eq__(self, other):
if isinstance(other, Person):
return self.name == other.name and self.age == other.age
return False
def __hash__(self):
return hash((self.name, self.age))
# Usage
p1 = Person("Alice", 30)
p2 = Person.from_birth_year("Bob", 1995) # class method
print(p1.introduce()) # instance method
print(Person.is_adult(25)) # static method| Keyword | Language | Purpose | Example |
|---|---|---|---|
| this | Java, C++, JS | Reference to the current object | this.name = name |
| super | Java | Reference to the parent class | super.speak() / super() for parent constructor |
| self | Python | Reference to the current instance (explicit) | self.name = name |
| cls | Python | Reference to the class itself (in classmethod) | cls(name, age) |
| base | C# | Reference to the parent class | base.Method() |
| parent | PHP | Reference to the parent class | parent::constructor() |
| Feature | Static (Class) | Instance (Object) |
|---|---|---|
| Belongs to | The class itself | A specific object |
| Memory | One copy shared by all objects | Separate copy per object |
| Access | ClassName.member or via object | objectName.member only |
| this/self | Cannot use this/self | Can use this/self |
| Called via | ClassName.method() | objectName.method() |
| Use case | Utility methods, counters, constants | State and behavior unique to each object |
// ── Inner Classes (Java) ──
class OuterClass {
private String outerField = "Outer";
// Non-static nested class (Inner class)
class InnerClass {
public void display() {
// Can access outer class's private members
System.out.println(outerField);
}
}
// Static nested class
static class StaticNestedClass {
public void display() {
// Cannot access outer class's instance members
System.out.println("Static nested class");
}
}
// Local inner class (inside a method)
public void methodWithLocalClass() {
class LocalClass {
public void show() {
System.out.println("Local inner class");
}
}
new LocalClass().show();
}
// Anonymous inner class
public Runnable getTask() {
return new Runnable() {
@Override
public void run() {
System.out.println("Anonymous inner class");
}
};
}
}# ── Friend Classes concept ──
# Python does not have friend keyword (like C++), but you can
# simulate the concept using naming conventions or __init_subclass__
# In C++, friend keyword grants another class access to private members:
#
# class Engine {
# friend class Mechanic; // Mechanic can access Engine's privates
# private:
# void start();
# };
#
# class Mechanic {
# void repair(Engine& e) {
# e.start(); // allowed because of friendship
# }
# };
# Python equivalent — using name mangling or explicit access pattern
class Engine:
def __init__(self):
self._internal_state = "running" # convention: _ = protected
self.__secret = "boosted" # __ = name-mangled
# Provide controlled access via friendship-like patterns
def _friend_access(self, friend_name: str):
if friend_name == "Mechanic":
return self.__secret
raise PermissionError(f"{friend_name} is not a friend")
class Mechanic:
def diagnose(self, engine: Engine):
# Access protected members by convention
state = engine._internal_state
# Request friend access for truly private data
secret = engine._friend_access("Mechanic")
return f"Engine state: {state}, secret: {secret}"this() keyword for chaining. Python uses __init__ (initializer, not the actual constructor — __new__ is the real constructor). Python supports constructor alternatives via @classmethod factory methods (e.g., from_birth_year).| Relationship | Type | Strength | Description | UML Notation |
|---|---|---|---|---|
| Association | Has-A | Weak | General relationship between two classes; objects are independent | Solid line with arrow |
| Aggregation | Has-A | Moderate | Specialized association; whole-part but parts can exist independently | Hollow diamond on whole |
| Composition | Part-Of | Strong | Whole owns parts; parts cannot exist without the whole | Filled diamond on whole |
| Generalization | IS-A | Strong | Inheritance relationship; child specializes parent | Hollow triangle on parent |
| Realization | Implements | Strong | Class implements an interface contract | Dashed line + hollow triangle |
| Dependency | Uses | Weakest | One class uses another temporarily (method parameter, local variable) | Dashed line with arrow |
| Feature | Association | Aggregation | Composition |
|---|---|---|---|
| Type | General relationship | Whole-Part (weak) | Whole-Part (strong) |
| Part independence | Fully independent | Can exist without whole | Cannot exist without whole |
| Lifecycle | Independent | Independent | Dependent on whole |
| Ownership | No ownership | Part ownership | Full ownership |
| Example | Teacher teaches Student | Department has Teachers | House has Rooms |
| Destroy whole? | Parts survive | Parts survive | Parts are destroyed |
| UML | ─────> | ◆─────> | ◆─────> |
| Relationship | Example A | Example B | Example C |
|---|---|---|---|
| Association | Doctor treats Patient | Pilot flies Airplane | Author writes Book |
| Aggregation | Team has Players | Car has Wheels | Library has Books |
| Composition | Heart is part of Body | House has Rooms | Order has LineItems |
| Generalization | Car IS-A Vehicle | Student IS-A Person | Cat IS-A Animal |
| Realization | ArrayList implements List | HashMap implements Map | EmailService implements Notification |
// ── Association (weak Has-A) ──
class Teacher { private String name; /* ... */ }
class Student { private String name; /* ... */ }
class Course {
private Teacher instructor; // association: course has a teacher
private List<Student> enrolled; // association: course has students
// Teacher and Student exist independently of Course
}
// ── Aggregation (weak whole-part) ──
class Department {
private String name;
private List<Teacher> teachers; // aggregation: teachers can exist without dept
public void addTeacher(Teacher t) { teachers.add(t); }
public void removeTeacher(Teacher t) { teachers.remove(t); }
// Removing Department does NOT destroy Teacher objects
}
// ── Composition (strong whole-part) ──
class House {
private List<Room> rooms; // composition: rooms cannot exist without house
public House() {
this.rooms = new ArrayList<>();
this.rooms.add(new Room("Living Room"));
this.rooms.add(new Room("Bedroom"));
}
public void demolish() {
rooms.clear(); // rooms are destroyed when house is destroyed
}
}
// ── Generalization (IS-A / Inheritance) ──
class Vehicle {
protected String brand;
public void start() { System.out.println("Vehicle starting..."); }
}
class Car extends Vehicle {
private int doors;
@Override
public void start() { System.out.println("Car engine starting..."); }
}
// ── Realization (Implements) ──
interface Flyable {
void fly();
}
class Airplane implements Flyable {
@Override
public void fly() { System.out.println("Airplane flying..."); }
}# ── Composition (strongest relationship) ──
class Engine:
def start(self):
return "Engine started"
def stop(self):
return "Engine stopped"
class Car:
def __init__(self, brand: str):
self.brand = brand
self.engine = Engine() # Car creates and owns Engine
def drive(self):
return self.engine.start() # Car delegates to Engine
def __del__(self):
# When Car is destroyed, Engine is also destroyed
self.engine.stop()
# ── Aggregation (weak composition) ──
class Player:
def __init__(self, name: str):
self.name = name
class Team:
def __init__(self, team_name: str):
self.team_name = team_name
self.players = [] # players can exist independently
def add_player(self, player: Player):
self.players.append(player)
def remove_player(self, player: Player):
self.players.remove(player) # player still exists elsewhere
# ── Association ──
class Doctor:
def __init__(self, name: str):
self.name = name
class Patient:
def __init__(self, name: str):
self.name = name
# Association: doctors treat patients (both exist independently)
appointments: list[tuple[Doctor, Patient]] = []| Principle | Acronym | Core Rule | One-Liner Definition |
|---|---|---|---|
| Single Responsibility | S | A class should have only one reason to change | One class, one job, one reason to change |
| Open/Closed | O | Open for extension, closed for modification | Add new features by extending, not editing existing code |
| Liskov Substitution | L | Subtypes must be substitutable for their base types | If it looks like a duck and quacks like a duck, it better be a duck |
| Interface Segregation | I | Many specific interfaces are better than one general interface | Don't force clients to depend on methods they don't use |
| Dependency Inversion | D | Depend on abstractions, not concretions | Program to an interface, not an implementation |
A class should have only one reason to change. If a class has multiple responsibilities, changes to one responsibility may break others. Split large classes into smaller, focused ones.
// ❌ VIOLATION: Class handles both data storage and reporting
class Employee {
public void calculatePay() { /* ... */ }
public void saveToDatabase() { /* ... */ }
public void generateReport() { /* ... */ }
}
// ✅ CORRECT: Each class has one responsibility
class EmployeePayCalculator {
public PaySlip calculate(Employee emp) { /* ... */ }
}
class EmployeeRepository {
public void save(Employee emp) { /* ... */ }
}
class ReportGenerator {
public Report generate(Employee emp) { /* ... */ }
}UserService that validates input, sends emails, logs activity, and connects to the database — violates SRP because it has 4 reasons to change.Software entities should be open for extension but closed for modification. You should be able to add new behavior without changing existing, tested code. Use polymorphism, interfaces, and the Strategy pattern.
// ❌ VIOLATION: Must modify to add new shapes
class AreaCalculator {
public double calculate(Object shape) {
if (shape instanceof Circle) {
return Math.PI * ((Circle)shape).radius * ((Circle)shape).radius;
} else if (shape instanceof Rectangle) {
Rectangle r = (Rectangle)shape;
return r.width * r.height;
}
// Must add else-if for every new shape
}
}
// ✅ CORRECT: Extend via polymorphism
interface Shape {
double area();
}
class Circle implements Shape {
double radius;
public double area() { return Math.PI * radius * radius; }
}
class Rectangle implements Shape {
double width, height;
public double area() { return width * height; }
}
class AreaCalculator {
public double calculate(Shape shape) {
return shape.area(); // works for ANY new shape
}
}Objects of a superclass should be replaceable with objects of a subclass without breaking the program. A subclass must honor the contract of its parent.
# ❌ VIOLATION: Subclass breaks parent contract
class Bird:
def fly(self):
return "Flying high"
class Penguin(Bird):
def fly(self):
raise NotImplementedError("Penguins can't fly!")
# Violates LSP: Bird expects fly() to work
# ✅ CORRECT: Redesign the hierarchy
class Bird:
def move(self):
return "Moving"
class FlyingBird(Bird):
def fly(self):
return "Flying high"
class FlightlessBird(Bird):
def walk(self):
return "Walking"
class Penguin(FlightlessBird):
def walk(self):
return "Waddling on ice"Square class extending Rectangle where setting width also changes height. If code expects a Rectangle but gets a Square, the behavior changes unexpectedly.No client should be forced to depend on methods it does not use. Prefer many small, focused interfaces over one large, general-purpose interface.
// ❌ VIOLATION: Fat interface forces all implementations
interface Worker {
void work();
void eat();
void sleep();
}
class Robot implements Worker {
public void work() { /* ... */ }
public void eat() { /* ?? */ } // Robot doesn't eat!
public void sleep() { /* ?? */ } // Robot doesn't sleep!
}
// ✅ CORRECT: Segregated interfaces
interface Workable {
void work();
}
interface Eatable {
void eat();
}
interface Sleepable {
void sleep();
}
class Human implements Workable, Eatable, Sleepable {
public void work() { /* ... */ }
public void eat() { /* ... */ }
public void sleep() { /* ... */ }
}
class Robot implements Workable {
public void work() { /* ... */ }
// No eat() or sleep() — Robot is happy
}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. Use dependency injection.
// ❌ VIOLATION: High-level depends on low-level concrete class
class MySQLDatabase {
public void save(String data) { /* save to MySQL */ }
}
class UserService {
private MySQLDatabase db = new MySQLDatabase(); // tight coupling
public void saveUser(String user) { db.save(user); }
// Cannot switch to PostgreSQL without changing UserService
}
// ✅ CORRECT: Depend on abstraction, inject dependency
interface Database {
void save(String data);
}
class MySQLDatabase implements Database {
public void save(String data) { /* save to MySQL */ }
}
class PostgresDatabase implements Database {
public void save(String data) { /* save to PostgreSQL */ }
}
class UserService {
private Database db; // depends on abstraction
// Dependency Injection via constructor
public UserService(Database db) {
this.db = db;
}
public void saveUser(String user) {
db.save(user); // works with ANY Database implementation
}
}| Principle | Violation Symptom | Code Smell |
|---|---|---|
| SRP | Class is large, hard to name, has many reasons to change | God class, shotgun surgery |
| OCP | Must modify existing code to add features | Long if-else chains, switch on type |
| LSP | Subclass breaks parent contract or throws exceptions | Override that does nothing or throws |
| ISP | Class implements methods it leaves empty | Fat interfaces, unused methods |
| DIP | Classes directly instantiate their dependencies | New keyword in business logic |
| Category | Patterns | Purpose |
|---|---|---|
| Creational | Singleton, Factory Method, Abstract Factory, Builder, Prototype | Object creation mechanisms |
| Structural | Adapter, Decorator, Facade, Proxy, Composite, Bridge, Flyweight | Composition and relationships |
| Behavioral | Observer, Strategy, Command, Iterator, State, Template Method, Mediator | Object communication and responsibility |
// Thread-safe Singleton (Double-Checked Locking)
public class DatabaseConnection {
private static volatile DatabaseConnection instance;
private DatabaseConnection() { /* private constructor */ }
public static DatabaseConnection getInstance() {
if (instance == null) { // 1st check (no lock)
synchronized (DatabaseConnection.class) {
if (instance == null) { // 2nd check (with lock)
instance = new DatabaseConnection();
}
}
}
return instance;
}
}# Factory Method Pattern
from abc import ABC, abstractmethod
class Transport(ABC):
@abstractmethod
def deliver(self) -> str: pass
class Truck(Transport):
def deliver(self) -> str:
return "Delivering by land in a box"
class Ship(Transport):
def deliver(self) -> str:
return "Delivering by sea in a container"
class Logistics(ABC):
@abstractmethod
def create_transport(self) -> Transport: pass
def plan_delivery(self) -> str:
t = self.create_transport() # factory method
return t.deliver()
class RoadLogistics(Logistics):
def create_transport(self) -> Transport:
return Truck()
class SeaLogistics(Logistics):
def create_transport(self) -> Transport:
return Ship()# Observer Pattern
from abc import ABC, abstractmethod
class Subject(ABC):
def __init__(self):
self._observers: list = []
def attach(self, observer):
self._observers.append(observer)
def detach(self, observer):
self._observers.remove(observer)
def notify(self, message: str):
for obs in self._observers:
obs.update(message)
class YouTubeChannel(Subject):
def upload_video(self, title: str):
self.notify(f"New video: {title}")
class Subscriber:
def __init__(self, name: str):
self.name = name
def update(self, message: str):
print(f"{self.name} received: {message}")
# Usage
channel = YouTubeChannel()
channel.attach(Subscriber("Alice"))
channel.attach(Subscriber("Bob"))
channel.upload_video("OOP Design Patterns Tutorial")// Strategy Pattern
interface SortStrategy {
void sort(int[] array);
}
class BubbleSort implements SortStrategy {
public void sort(int[] arr) { /* bubble sort */ }
}
class QuickSort implements SortStrategy {
public void sort(int[] arr) { /* quick sort */ }
}
class MergeSort implements SortStrategy {
public void sort(int[] arr) { /* merge sort */ }
}
class Sorter {
private SortStrategy strategy;
public Sorter(SortStrategy strategy) {
this.strategy = strategy;
}
public void setStrategy(SortStrategy strategy) {
this.strategy = strategy; // switch at runtime
}
public void sort(int[] data) {
strategy.sort(data);
}
}# Adapter Pattern
class LegacyPrinter:
def print_old_format(self, text: str):
return f"LEGACY: {text}"
class ModernPrinter(ABC):
@abstractmethod
def print_document(self, content: str):
pass
class PrinterAdapter(ModernPrinter):
def __init__(self, legacy: LegacyPrinter):
self.legacy = legacy
def print_document(self, content: str):
return self.legacy.print_old_format(content)
# Usage: modern code works with legacy system
printer = PrinterAdapter(LegacyPrinter())
printer.print_document("Hello World")# Decorator Pattern
from abc import ABC, abstractmethod
class Coffee(ABC):
@abstractmethod
def cost(self) -> float: pass
@abstractmethod
def description(self) -> str: pass
class Espresso(Coffee):
def cost(self) -> float: return 2.0
def description(self) -> str: return "Espresso"
class CoffeeDecorator(Coffee):
def __init__(self, coffee: Coffee):
self.coffee = coffee
class WithMilk(CoffeeDecorator):
def cost(self) -> float: return self.coffee.cost() + 0.5
def description(self) -> str: return f"{self.coffee.description()}, Milk"
class WithMocha(CoffeeDecorator):
def cost(self) -> float: return self.coffee.cost() + 1.0
def description(self) -> str: return f"{self.coffee.description()}, Mocha"
# Usage: chain decorators
order = WithMocha(WithMilk(Espresso()))
print(order.description()) # "Espresso, Milk, Mocha"
print(order.cost()) # 3.5// Facade Pattern
class CPU { public void freeze() {} public void jump(long pos) {} }
class Memory { public void load(long pos, byte[] data) {} }
class HardDrive { public byte[] read(long lba, int size) { return new byte[size]; } }
// Facade: simplified interface to complex subsystem
class ComputerFacade {
private CPU cpu = new CPU();
private Memory memory = new Memory();
private HardDrive hd = new HardDrive();
public void start() {
cpu.freeze();
memory.load(0, hd.read(0, 4096));
cpu.jump(0);
}
}
// Client only sees the simple interface
class Client {
public static void main(String[] args) {
ComputerFacade computer = new ComputerFacade();
computer.start(); // simple!
}
}// Builder Pattern (with method chaining)
public class HttpRequest {
private final String url;
private final String method;
private final Map<String, String> headers;
private final String body;
private HttpRequest(Builder builder) {
this.url = builder.url;
this.method = builder.method;
this.headers = builder.headers;
this.body = builder.body;
}
public static class Builder {
private final String url;
private String method = "GET";
private Map<String, String> headers = new HashMap<>();
private String body = null;
public Builder(String url) { this.url = url; }
public Builder method(String m) { this.method = m; return this; }
public Builder header(String k, String v) { headers.put(k, v); return this; }
public Builder body(String b) { this.body = b; return this; }
public HttpRequest build() { return new HttpRequest(this); }
}
}
// Usage: clean, readable construction
HttpRequest request = new HttpRequest.Builder("https://api.example.com")
.method("POST")
.header("Content-Type", "application/json")
.header("Authorization", "Bearer token123")
.body('{"key": "value"}')
.build();| Feature | OOP | Procedural | Functional |
|---|---|---|---|
| Primary Unit | Objects (data + methods) | Functions/Procedures | Pure Functions |
| Data | Encapsulated in objects | Separate from functions | Immutable data structures |
| State | Mutable, encapsulated | Global or shared mutable | Immutable, no side effects |
| Approach | Bottom-up (objects first) | Top-down (decompose problem) | Declarative (what, not how) |
| Inheritance | Supported | Not applicable | Not applicable (use composition) |
| Polymorphism | Via inheritance/interfaces | Via function pointers | Via higher-order functions |
| Parallelism | Harder (shared state) | Harder (shared state) | Easier (no shared state, pure functions) |
| Reusability | Inheritance + composition | Function libraries | Function composition |
| Testing | Mock objects, unit tests | Unit test functions | Property-based testing, easy (pure functions) |
| Error Handling | Exceptions, try/catch | Return codes | Monads (Option, Result, Either) |
| Examples | Java, C#, Python, C++ | C, Pascal, Fortran | Haskell, Erlang, Clojure, Scala, F# |
| # | Advantage | Explanation |
|---|---|---|
| 1 | Code Reusability | Inheritance and composition allow reusing existing code |
| 2 | Modularity | Code is organized into self-contained objects |
| 3 | Maintainability | Changes are localized; easy to find and fix bugs |
| 4 | Extensibility | New features added via inheritance and polymorphism |
| 5 | Real-World Modeling | Maps naturally to real-world entities |
| 6 | Data Security | Encapsulation protects data from unauthorized access |
| 7 | Polymorphism | Same interface works with different data types |
| 8 | Collaboration | Objects provide clear boundaries for team work |
| # | Disadvantage | Explanation |
|---|---|---|
| 1 | Verbosity | More boilerplate code compared to procedural/functional |
| 2 | Performance | Object creation, message passing have overhead |
| 3 | Steep Learning Curve | Abstractions like polymorphism, SOLID are complex |
| 4 | Not Always a Fit | Scripting, data processing, functional tasks are simpler procedurally |
| 5 | Tight Coupling Risk | Inheritance can create tight coupling between classes |
| 6 | Over-Engineering | Tendency to create too many abstractions for simple problems |
// ── Real-World OOP: Bank Account System ──
abstract class BankAccount {
protected String accountNumber;
protected double balance;
protected String owner;
public BankAccount(String accNum, String owner, double initialBalance) {
this.accountNumber = accNum;
this.owner = owner;
this.balance = initialBalance;
}
public void deposit(double amount) {
if (amount > 0) balance += amount;
}
public abstract void withdraw(double amount); // abstract — each type decides
public double getBalance() { return balance; }
// Encapsulation: balance is protected, accessed via methods
}
class SavingsAccount extends BankAccount {
private double interestRate;
public SavingsAccount(String accNum, String owner, double balance, double rate) {
super(accNum, owner, balance);
this.interestRate = rate;
}
@Override
public void withdraw(double amount) { // polymorphism
if (amount > 0 && balance - amount >= 100) { // min balance
balance -= amount;
}
}
public void applyInterest() {
balance += balance * interestRate;
}
}
class CheckingAccount extends BankAccount {
private double overdraftLimit;
public CheckingAccount(String accNum, String owner, double balance, double overdraft) {
super(accNum, owner, balance);
this.overdraftLimit = overdraft;
}
@Override
public void withdraw(double amount) { // different logic
if (amount > 0 && balance + overdraftLimit >= amount) {
balance -= amount;
}
}
}# ── Real-World OOP: Library Management System ──
from abc import ABC, abstractmethod
from datetime import datetime, timedelta
class LibraryItem(ABC):
def __init__(self, item_id: str, title: str):
self.item_id = item_id
self.title = title
self.is_checked_out = False
self.due_date = None
@abstractmethod
def get_max_loan_days(self) -> int:
pass
def check_out(self) -> None:
self.is_checked_out = True
self.due_date = datetime.now() + timedelta(days=self.get_max_loan_days())
class Book(LibraryItem):
def __init__(self, item_id: str, title: str, author: str, pages: int):
super().__init__(item_id, title)
self.author = author
self.pages = pages
def get_max_loan_days(self) -> int:
return 14 # books can be borrowed for 14 days
class DVD(LibraryItem):
def __init__(self, item_id: str, title: str, duration: int):
super().__init__(item_id, title)
self.duration = duration # minutes
def get_max_loan_days(self) -> int:
return 7 # DVDs for 7 days
class Member:
def __init__(self, member_id: str, name: str):
self.member_id = member_id
self.name = name
self.borrowed_items: list[LibraryItem] = []
def borrow(self, item: LibraryItem) -> None:
if not item.is_checked_out:
item.check_out()
self.borrowed_items.append(item)
class Library:
def __init__(self):
self.catalog: dict[str, LibraryItem] = {}
self.members: dict[str, Member] = {}
def add_item(self, item: LibraryItem) -> None:
self.catalog[item.item_id] = item
def register_member(self, member: Member) -> None:
self.members[member.member_id] = member| Aspect | Interface | Abstract Class |
|---|---|---|
| Keyword | interface (Java), @abstractmethod (Python) | abstract class (Java), ABC (Python) |
| Multiple Inheritance | A class can implement many interfaces | A class can extend only one abstract class |
| Constructors | Not allowed | Allowed (called via super()) |
| Instance Variables | Not allowed (only static final constants) | Allowed (can hold state) |
| Default Methods | Java 8+ default methods (with body) | Can have any concrete methods |
| Access Modifiers | Methods are public by default | Any access modifier |
| Fields | public static final by default | Can be private, protected, public |
| When to Use | Define a contract (CAN-DO) | Share code among related classes (IS-A) |
| Versioning | Easy to add new methods (default) | Hard to add without breaking subclasses |
| Speed | Slightly faster (JVM optimizes) | Slightly slower (dynamic dispatch) |
| Aspect | Overloading | Overriding |
|---|---|---|
| Definition | Same name, different parameters | Same name AND same parameters in subclass |
| Also Called | Compile-time polymorphism | Runtime polymorphism |
| Binding | Static / Early binding | Dynamic / Late binding |
| Parameters | Must differ (type, count, or order) | Must be exactly the same |
| Return Type | Can be different | Must be same (or covariant) |
| Access Modifier | Can be any | Cannot be more restrictive |
| Exceptions | Can be any | Cannot throw broader checked exceptions |
| Class Relation | Same class or subclass | Must be in a subclass |
| Methods Involved | Multiple in same class | One in parent, one in child |
| @Override | Not used | Used (Java) for compile-time checking |
| Feature | Shallow Copy | Deep Copy |
|---|---|---|
| Object References | Copies references to nested objects | Creates new copies of nested objects |
| Nested Objects | Original and copy share nested objects | Original and copy have independent nested objects |
| Modification Impact | Changing nested object affects both | Changing nested object affects only one |
| Implementation | Object.clone(), copy.copy(), = assignment | Custom deep copy, copy.deepcopy(), serialization |
| Performance | Faster (less memory) | Slower (more memory, recursive copying) |
| When to Use | Flat objects, immutable nested objects | Objects with mutable nested structures |
import copy
class Address:
def __init__(self, city: str):
self.city = city
class Person:
def __init__(self, name: str, address: Address):
self.name = name
self.address = address
addr = Address("New York")
p1 = Person("Alice", addr)
# Shallow copy: p2.address references SAME object
p2 = copy.copy(p1)
p2.address.city = "Boston"
print(p1.address.city) # "Boston" — affected!
# Deep copy: p3.address is a NEW object
p3 = copy.deepcopy(p1)
p3.address.city = "Los Angeles"
print(p1.address.city) # "Boston" — NOT affected// Constructor chaining: calling one constructor from another
class Person {
private String name;
private int age;
private String email;
private String phone;
// Constructor 1: minimal
public Person(String name) {
this(name, 0); // chains to Constructor 2
}
// Constructor 2: name + age
public Person(String name, int age) {
this(name, age, "N/A"); // chains to Constructor 3
}
// Constructor 3: name + age + email
public Person(String name, int age, String email) {
this(name, age, email, "N/A"); // chains to Constructor 4
}
// Constructor 4: all fields (the "master" constructor)
public Person(String name, int age, String email, String phone) {
this.name = name;
this.age = age;
this.email = email;
this.phone = phone;
// All initialization happens HERE — single point of truth
}
}this() and super() must be the first statement in a constructor. You cannot use both — either call this() OR super() as the first line.# ── The Diamond Problem ──
# A
# / \
# B C
# \ /
# D
# D inherits from both B and C, which both inherit from A.
# Which version of A's method does D call?
class A:
def greet(self):
return "Hello from A"
class B(A):
def greet(self):
return "Hello from B"
class C(A):
def greet(self):
return "Hello from C"
class D(B, C):
pass
d = D()
print(d.greet()) # "Hello from B" (MRO picks B)
print(D.__mro__) # D -> B -> C -> A -> object
# ── Solutions to Diamond Problem ──
# Python: MRO (Method Resolution Order) using C3 linearization
# Java: Multiple inheritance of interfaces only (no diamond for classes)
# C++: Virtual inheritance (shared base class)
# C#: Interfaces only, explicit interface implementation// Virtual Function — base provides default implementation
class Animal {
public:
// virtual: derived classes CAN override
virtual void speak() {
cout << "Some generic animal sound" << endl;
}
virtual ~Animal() {} // ALWAYS virtual destructor
};
// Pure Virtual Function — derived classes MUST override
class Shape {
public:
// Pure virtual: no implementation, makes class abstract
virtual double area() = 0;
virtual double perimeter() = 0;
virtual ~Shape() {} // virtual destructor
};
class Circle : public Shape {
double radius;
public:
Circle(double r) : radius(r) {}
// MUST implement pure virtual functions
double area() override {
return 3.14159 * radius * radius;
}
double perimeter() override {
return 2 * 3.14159 * radius;
}
};| Feature | Virtual Function | Pure Virtual Function |
|---|---|---|
| Declaration | virtual returnType name() {} | virtual returnType name() = 0 |
| Has Implementation? | Yes (default behavior) | No (must be overridden) |
| Class Type | Concrete class | Abstract class (cannot be instantiated) |
| Override Required? | Optional | Mandatory |
| Use Case | Default behavior with optional override | Enforce contract on all subclasses |
# ── Friend Function (C++ concept, Python equivalent) ──
#
# In C++, a friend function is a non-member function that is granted
# access to the private and protected members of a class.
#
# class MyClass {
# private:
# int secret = 42;
# friend void revealSecret(MyClass& obj); // friend declaration
# };
#
# void revealSecret(MyClass& obj) {
# cout << obj.secret; // allowed because of friendship
# }
#
# Key points:
# - Friendship is GRANTED, not TAKEN (class declares friend, not the other way)
# - Friendship is NOT inherited (child class does not inherit friends)
# - Friendship is NOT transitive (friend of my friend is NOT my friend)
# - A class can be a friend of another class (all methods get access)
# Python does not have "friend" — use these alternatives:
class BankAccount:
def __init__(self, owner: str, balance: float):
self.__owner = owner # name-mangled "private"
self.__balance = balance
# 1. Provide a controlled accessor method
def get_balance_for(self, auditor_name: str) -> float:
if auditor_name == "TaxAuthority":
return self.__balance
raise PermissionError("Unauthorized access")
# 2. Use Python's name mangling (technical access, not recommended)
# obj._BankAccount__balance — works but breaks encapsulation
# 3. Use __getattr__ for fine-grained access control
def __getattr__(self, name):
if name == "audit_data":
return {'owner': self.__owner, 'balance': self.__balance}
raise AttributeError(f"'{'{'}self.__class__.__name__{'}'}' has no attribute '{name}'")override catches signature mismatches, finalprevents further overriding. (3) Python's MRO is C3 linearization — know ClassName.__mro__ for interview questions.Imagine you have a toy factory. The factory has a blueprint (class) for making toy cars. Each toy car you make from the blueprint is an object. Each car has its own color and size (state/attributes), and each can drive and honk (behavior/methods). Now, you can make a racing car blueprint based on the original car blueprint — it inherits the ability to drive but goes faster (inheritance). You don't need to know how the engine works inside to drive the car (abstraction). The car's engine is hidden safely inside and you control it with buttons (encapsulation). And you can press the same "go" button on a car, boat, or plane and they all move — but in their own way (polymorphism).
Abstract class: Can have both abstract and concrete methods, constructors, instance variables, and any access modifier. Supports single inheritance. Use when classes share a common identity (IS-A relationship).
Interface: Only method signatures (traditionally), no constructors, no instance variables, all methods are public. Supports multiple inheritance. Use when you want to define a contract that unrelated classes can fulfill (CAN-DO relationship).
Rule of thumb: Use an abstract class when you want to share code among closely related classes. Use an interface when you want to specify behavior for unrelated classes. In modern Java, interfaces can have default methods and static methods, narrowing the gap — but interfaces still cannot hold state.
S — Single Responsibility: A class should have one, and only one, reason to change. Example: separateEmployee into EmployeePayCalculator, EmployeeRepository, and ReportGenerator.
O — Open/Closed: Open for extension, closed for modification. Use polymorphism and interfaces to add new features without changing existing code. Example: Shape interface with area() — new shapes just implement the interface.
L — Liskov Substitution: Subtypes must be substitutable for their base types. Example: Penguinshouldn't extend Bird if Bird promises fly().
I — Interface Segregation: Many specific interfaces > one general interface. Example: Robotshouldn't implement Eatable. Split Worker into Workable, Eatable, Sleepable.
D — Dependency Inversion: Depend on abstractions, not concretions. Example: UserServicedepends on Database interface, not MySQLDatabase directly. Use dependency injection.
Polymorphism means "many forms." It allows objects of different types to be treated through a common interface.
Compile-time (static) polymorphism: Resolved at compile time. Method overloading — same method name with different parameters (add(int, int) vs add(double, double)).Operator overloading — redefining + for custom types.
Runtime (dynamic) polymorphism: Resolved at runtime. Method overriding — subclass redefines parent method. The actual method called depends on the object type, not the reference type. Animal a = new Dog(); a.speak(); calls Dog's speak() because of dynamic dispatch via virtual method tables.
Key mechanism: Virtual functions / vtable — each class with virtual methods has a vtable (array of function pointers). The compiler looks up the correct function at runtime through this table.
Classes: ParkingLot, Level, ParkingSpot,Vehicle (abstract), Car, Motorcycle, Truck,Ticket, Payment.
Abstraction: Vehicle is abstract with getSpotSize().Polymorphism: Each vehicle type returns different spot size.
Encapsulation: ParkingSpot hides availability logic.
Composition: ParkingLot has multiple Levels; each Level has many ParkingSpots.
Key methods: parkVehicle(Vehicle v) — find nearest available spot of correct size.removeVehicle(Ticket t) — free the spot and calculate fee based on duration. Use Strategy pattern for pricing (hourly, daily, flat rate). Use Observer for spot availability notifications.
// Parking Lot Design (simplified)
enum SpotSize { SMALL, MEDIUM, LARGE }
abstract class Vehicle {
public abstract SpotSize getSpotSize();
public abstract double getHourlyRate();
}
class Car extends Vehicle {
public SpotSize getSpotSize() { return SpotSize.MEDIUM; }
public double getHourlyRate() { return 2.0; }
}
class ParkingSpot {
private SpotSize size;
private boolean isFree = true;
private Vehicle vehicle;
public boolean canFit(Vehicle v) { return isFree && size == v.getSpotSize(); }
public void park(Vehicle v) { this.vehicle = v; isFree = false; }
public void remove() { this.vehicle = null; isFree = true; }
}
class ParkingLot {
private List<ParkingSpot> spots;
private Map<String, ParkingSpot> ticketToSpot;
public boolean park(Vehicle v) {
for (ParkingSpot spot : spots) {
if (spot.canFit(v)) { spot.park(v); return true; }
}
return false; // lot is full
}
}Composition over Inheritance is a design principle that favors composing objects from smaller, focused components rather than creating deep inheritance hierarchies.
Inheritance problems: Tight coupling (child depends on parent), fragility (parent changes break children), inflexibility (inheritance is static — decided at compile time), the "banana, gorilla, jungle" problem (you want a banana but get the gorilla holding it and the whole jungle).
Composition benefits: Loose coupling, flexibility (change behavior at runtime via strategy/decorator), single responsibility (each component does one thing), easier testing.
Example: Instead of FlyingCar extends Car and SwimmingCar extends Car, compose: Car has MovementBehavior which can be FlyingMovement,DrivingMovement, or SwimmingMovement — swapped at runtime.
Singleton ensures only one instance of a class exists globally. Thread safety is critical because multiple threads might try to create the instance simultaneously.
Approaches:
1. Eager initialization: Create instance at class loading time. Thread-safe but wastes memory if never used.
2. Synchronized method: synchronized getInstance(). Thread-safe but slow (locks every call).
3. Double-checked locking: Check null, synchronize, check null again. Fast after first call. Must use volatile to prevent instruction reordering.
4. Static inner class (Bill Pugh): Instance created when inner class is loaded. Thread-safe, lazy, no synchronization overhead.
5. Enum singleton: Thread-safe by JVM guarantee, prevents reflection attacks and serialization issues. Best practice in Java.
In Python: Use a module-level variable or the Borg/Monostate pattern (shared state via __dict__).
// Thread-safe Singleton (3 approaches)
// Approach 1: Eager (simple, always thread-safe)
public class EagerSingleton {
private static final EagerSingleton INSTANCE = new EagerSingleton();
private EagerSingleton() {}
public static EagerSingleton getInstance() { return INSTANCE; }
}
// Approach 2: Double-Checked Locking (lazy, fast after init)
public class DCLSingleton {
private static volatile DCLSingleton instance;
private DCLSingleton() {}
public static DCLSingleton getInstance() {
if (instance == null) {
synchronized (DCLSingleton.class) {
if (instance == null) instance = new DCLSingleton();
}
}
return instance;
}
}
// Approach 3: Enum (BEST — JVM guarantees safety)
public enum EnumSingleton {
INSTANCE;
public void doSomething() { /* ... */ }
}All three represent "Has-A" relationships but with different strengths:
Association (weakest): Two classes know about each other but are independent. Example: Teacher teaches Student. If the teacher leaves, students still exist. UML: solid line with arrow.
Aggregation (moderate): Whole-part relationship where parts CAN exist independently of the whole. Example: Department has Teachers. If the department is dissolved, teachers still exist. UML: hollow diamond on the whole.
Composition (strongest): Whole owns parts; parts CANNOT exist without the whole. Same lifecycle. Example: House has Rooms. If the house is demolished, rooms are destroyed. UML: filled diamond on the whole.
Memory tip: Association = "knows about", Aggregation = "has-a but can live apart", Composition = "owns and dies together". Always think about lifecycle: if destroying the whole also destroys the part, it's composition.