Back to LLD Concepts
Programming Paradigm

Object-Oriented Programming

A complete guide to OOP — from first principles (classes, objects) through the four pillars, to advanced topics like generics, composition, descriptors, and metaclasses. Every concept shown in Java, Python, and JavaScript.

What is OOP?

Object-Oriented Programming is a programming paradigm that organises software around objects — bundles of state (data) and behaviour (methods) — rather than around functions and logic alone. The core insight is that real-world problems are easier to model when code reflects the structure of the problem domain.

OOP rests on four traditional pillars — Encapsulation, Inheritance, Polymorphism, and Abstraction — plus a set of advanced techniques that build on top of them.

📦 Classes & Objects

The basic building blocks — blueprints and instances

🔒 Encapsulation

Bundle state + behaviour, hide implementation details

🌱 Inheritance

Reuse and specialise existing classes

🎭 Polymorphism

Same interface, different behaviour at runtime

🧩 Abstraction

Expose what, hide how — simplify interfaces

🔀 Composition

"Has-a" over "is-a" — flexible object assembly

⚡ Advanced OOP

Generics, descriptors, metaclasses, protocols

Concept 1 — Beginner

Classes & Objects

"A class is a blueprint. An object is a specific thing built from that blueprint."

The Core Idea

Before OOP, programs were collections of procedures operating on raw data. OOP bundles the data (fields/attributes) and the operations on that data (methods) together into one unit — the object.

A class defines the structure: what fields every instance will have and what methods it can perform. An object (instance) is a specific realisation of that class with its own independent state. Think of a class as the mould and objects as the individual castings from that mould — all the same shape, but each filled with its own data.

Fields (State)

Variables stored inside the object — name, balance, colour. Each instance has its own copy.

Methods (Behaviour)

Functions defined inside the class — deposit(), withdraw(). They operate on the object's own fields.

Constructor

Special method called when the object is created. Sets up initial state.

Class vs Instance: key distinctions

AspectClassInstance (Object)
What it isBlueprint / TemplateReal thing in memory
CountOne per definitionUnlimited instances
FieldsDescribes the shapeHolds actual values
StateNo runtime stateHas its own state
Created withclass keywordnew / () call

Code Example

// ── Classes & Objects ─────────────────────────────────────────

// A CLASS is a blueprint. An OBJECT is an instance built from that blueprint.
public class BankAccount {

    // ── Fields (state) ────────────────────────────────────────
    private String accountId;
    private String holderName;
    private double balance;
    private final String currency;

    // ── Constructor ───────────────────────────────────────────
    // Called when you write: new BankAccount(...)
    public BankAccount(String accountId, String holderName, String currency) {
        this.accountId   = accountId;
        this.holderName  = holderName;
        this.currency    = currency;
        this.balance     = 0.0;        // default state
    }

    // ── Methods (behaviour) ───────────────────────────────────
    public void deposit(double amount) {
        if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");
        this.balance += amount;
        System.out.printf("[%s] Deposited %.2f. Balance: %.2f %s%n",
                          accountId, amount, balance, currency);
    }

    public void withdraw(double amount) {
        if (amount <= 0)          throw new IllegalArgumentException("Amount must be positive");
        if (amount > this.balance) throw new IllegalStateException("Insufficient funds");
        this.balance -= amount;
        System.out.printf("[%s] Withdrew %.2f. Balance: %.2f %s%n",
                          accountId, amount, balance, currency);
    }

    public double getBalance()   { return balance; }
    public String getAccountId() { return accountId; }

    // ── toString — human-readable representation ──────────────
    @Override
    public String toString() {
        return String.format("BankAccount{id='%s', holder='%s', balance=%.2f %s}",
                             accountId, holderName, balance, currency);
    }

    // ── equals & hashCode — value-based identity ──────────────
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof BankAccount)) return false;
        return accountId.equals(((BankAccount) o).accountId);
    }

    @Override
    public int hashCode() { return accountId.hashCode(); }
}

// ── Creating objects (instances) ──────────────────────────────
public class Main {
    public static void main(String[] args) {
        // 'new' allocates memory and calls the constructor
        BankAccount alice = new BankAccount("ACC-001", "Alice", "INR");
        BankAccount bob   = new BankAccount("ACC-002", "Bob",   "INR");

        alice.deposit(50_000);
        alice.withdraw(12_000);
        bob.deposit(20_000);

        System.out.println(alice);  // BankAccount{id='ACC-001', ...}
        System.out.println(bob);

        // Two variables, two independent objects in memory
        System.out.println(alice.equals(bob));   // false — different accountIds
        System.out.println(alice == alice);       // true  — same reference
    }
}

Real-World Examples

User in Django / Rails

User.objects.create(email=...) creates an instance from the User class. The class defines the schema; each user record is an instance with its own data.

React Component

A React class component is an OOP class. Its state is instance-level; its render() and lifecycle methods are the behaviour. Each mounted component is a separate object in memory.

Java Thread

new Thread(runnable) creates an object from the Thread class. Each Thread object manages its own execution stack, priority, and state independently.

MongoDB Document Model

Mongoose models are classes. Schema defines fields + validations (the blueprint). new User({ name: "Alice" }) creates an instance (the object).

Concept 2 — Beginner

Encapsulation

"Bundle data and the methods that operate on it together, and restrict direct access to the internal state."

The Two Parts of Encapsulation

1. Bundling

State and methods that belong together are in one class. UserAccount holds passwordHash AND the login() method that uses it — they're co-located because they're co-dependent.

2. Information Hiding

Internal details are private. Outside code accesses state only through controlled public methods. passwordHash is never exposed — only login() can compare it.

Access Modifiers Compared

LevelJavaPythonJavaScriptAccessible from
PublicpublicnamenameEveryone
Protectedprotected_name_name (convention)Class + subclasses
Privateprivate__name#nameClass only

Why not just make everything public? Public state lets external code set account.balance = -999 directly, bypassing all validation. Private fields force all changes through methods that enforce business rules. The object stays in a valid state at all times.

Code Example

// ── Encapsulation — bundle state + behaviour, hide internals ──

public class UserAccount {

    // ── Access Modifiers ──────────────────────────────────────
    // private   → only within this class
    // protected → this class + subclasses + same package
    // (default) → same package only
    // public    → everyone

    private String  email;           // private: no direct access
    private String  passwordHash;    // private: never exposed raw
    private boolean verified;
    private int     failedLoginAttempts;
    private static final int MAX_ATTEMPTS = 5;

    public UserAccount(String email, String rawPassword) {
        setEmail(email);                            // goes through validation
        this.passwordHash = hash(rawPassword);
        this.verified     = false;
        this.failedLoginAttempts = 0;
    }

    // ── Controlled getter — expose only what's needed ─────────
    public String getEmail()     { return email; }
    public boolean isVerified()  { return verified; }
    public boolean isLocked()    { return failedLoginAttempts >= MAX_ATTEMPTS; }

    // ── Controlled setter — gate changes through business rules
    public void setEmail(String email) {
        if (email == null || !email.contains("@"))
            throw new IllegalArgumentException("Invalid email: " + email);
        this.email = email.toLowerCase().trim();
    }

    // ── Behaviour that guards internal state ──────────────────
    public boolean login(String rawPassword) {
        if (isLocked()) throw new IllegalStateException("Account locked");
        if (hash(rawPassword).equals(passwordHash)) {
            failedLoginAttempts = 0;            // reset on success
            return true;
        }
        failedLoginAttempts++;
        if (isLocked())
            System.out.println("⚠️  Account locked after " + MAX_ATTEMPTS + " failed attempts");
        return false;
    }

    public void changePassword(String oldRaw, String newRaw) {
        if (!login(oldRaw)) throw new SecurityException("Current password incorrect");
        if (newRaw.length() < 8)
            throw new IllegalArgumentException("Password must be ≥ 8 characters");
        this.passwordHash = hash(newRaw);
        System.out.println("Password changed successfully.");
    }

    public void verify() { this.verified = true; }

    // passwordHash is NEVER returned — encapsulation protects it
    private String hash(String raw) {
        return Integer.toHexString(raw.hashCode()); // simplified
    }
}

// Client code — works through the public interface only
UserAccount user = new UserAccount("alice@example.com", "secret123");
user.verify();
System.out.println(user.isVerified());   // true
System.out.println(user.login("wrong")); // false — 1 attempt
// user.passwordHash  → compile error: passwordHash is private ✓

Benefits

  • Object stays in a valid state — invariants guaranteed
  • Internal representation can change without breaking callers
  • Bugs are localised — only the class can corrupt its own state
  • API is clear — only public methods are the contract

Common Pitfalls

  • Getters/setters for every field = no real encapsulation
  • Returning mutable objects from getters — callers mutate internal state
  • Making everything public "for simplicity" — loses all protection
  • Over-encapsulating simple value objects that need no protection
Concept 3 — Beginner

Inheritance

"A subclass inherits the fields and methods of its parent, then extends or overrides them to specialise behaviour."

How Inheritance Works

When class B extends A, B automatically gets all of A's non-private fields and methods. B can then: use inherited methods as-is, override them to change behaviour, extend them by calling super first, or add entirely new methods specific to B.

Inheritance creates an IS-A relationship: ElectricCar IS-A Vehicle. This means anywhere you use a Vehicle, you can use an ElectricCar — the foundation of polymorphism.

What Subclasses Can Do

  • InheritUse parent methods without any change
  • OverrideReplace parent method with a new implementation
  • ExtendCall super() then add new logic
  • AddDefine entirely new methods not in parent

Java vs Python vs JS Differences

  • Java:Single inheritance only. @Override annotation. final prevents overriding.
  • Python:Multiple inheritance supported. MRO (C3 linearisation) resolves conflicts. super() is cooperative.
  • JS:Prototype chain. Only single extends. super() mandatory before this in constructors.

⚠️ Inheritance pitfall — Fragile Base Class: Changes to a parent class can unexpectedly break all subclasses. If you override a method and the parent changes its internal logic, your override may become wrong. This is why "favour composition over inheritance" is the modern advice — covered in its own section below.

Code Example

// ── Inheritance — reuse and specialise a parent class ─────────

// Base class (superclass / parent)
public class Vehicle {
    protected String make;
    protected String model;
    protected int    year;
    protected double fuelLevel;  // 0.0 – 1.0

    public Vehicle(String make, String model, int year) {
        this.make      = make;
        this.model     = model;
        this.year      = year;
        this.fuelLevel = 1.0;
    }

    public void startEngine() {
        System.out.println(make + " " + model + " engine started.");
    }

    public void refuel(double amount) {
        fuelLevel = Math.min(1.0, fuelLevel + amount);
        System.out.printf("Refuelled. Level: %.0f%%%n", fuelLevel * 100);
    }

    public String getInfo() {
        return year + " " + make + " " + model;
    }
}

// Subclass — inherits everything from Vehicle, adds its own state/behaviour
public class ElectricCar extends Vehicle {
    private double batteryLevel;  // 0.0 – 1.0
    private int    rangeKm;

    public ElectricCar(String make, String model, int year, int rangeKm) {
        super(make, model, year);  // must call parent constructor first
        this.batteryLevel = 1.0;
        this.rangeKm      = rangeKm;
    }

    // Override — redefine parent behaviour for this specialisation
    @Override
    public void startEngine() {
        // ElectricCar has no combustion engine — completely different behaviour
        System.out.println(make + " " + model + " motors activated silently.");
    }

    // Override — refuelling an EV means charging
    @Override
    public void refuel(double amount) {
        batteryLevel = Math.min(1.0, batteryLevel + amount);
        System.out.printf("Charging complete. Battery: %.0f%% | Range: ~%d km%n",
                          batteryLevel * 100, (int)(batteryLevel * rangeKm));
    }

    // New method — specific to ElectricCar, not in Vehicle
    public int estimatedRange() {
        return (int)(batteryLevel * rangeKm);
    }

    // super.getInfo() + new info — extending the parent's method
    @Override
    public String getInfo() {
        return super.getInfo() + " [Electric, " + rangeKm + "km range]";
    }
}

public class Truck extends Vehicle {
    private double payloadTons;

    public Truck(String make, String model, int year, double payloadTons) {
        super(make, model, year);
        this.payloadTons = payloadTons;
    }

    public void loadCargo(double tons) {
        if (tons > payloadTons)
            throw new IllegalArgumentException("Exceeds payload capacity");
        System.out.printf("Loaded %.1f tonnes onto %s%n", tons, getInfo());
    }
}

// ── Usage ─────────────────────────────────────────────────────
Vehicle car = new ElectricCar("Tesla", "Model 3", 2024, 500);
car.startEngine();     // "Tesla Model 3 motors activated silently."
car.refuel(0.5);       // "Charging complete. Battery: 50% | Range: ~250 km"
System.out.println(car.getInfo()); // "2024 Tesla Model 3 [Electric, 500km range]"

Truck truck = new Truck("Tata", "T16", 2023, 16.0);
truck.startEngine();   // inherits Vehicle.startEngine()
truck.loadCargo(12.0);

Real-World Examples

Java Exception Hierarchy

Throwable ← Exception ← RuntimeException ← IllegalArgumentException. Each level adds specificity. catch (Exception e) catches all subtype exceptions.

Python's io Module

IOBase ← RawIOBase ← FileIO. Each subclass specialises for a different kind of I/O while inheriting the core read/write protocol.

React Component (Class API)

class MyComponent extends React.Component. Inherits render lifecycle, setState, and all React hooks. You override render() and lifecycle methods.

Django Model Inheritance

class Customer(User) adds customer-specific fields to Django's built-in User. All User methods (authenticate, check_password) work unchanged on Customer.

Concept 4 — Intermediate

Polymorphism

"One interface, many implementations — the same method call produces different behaviour depending on the actual object type at runtime."

Types of Polymorphism

Subtype (Runtime)

Dynamic dispatch, method overriding

A variable of type Shape holds a Circle. Calling shape.area() dispatches to Circle.area() at runtime. The compiler doesn't know which implementation runs.

Shape s = new Circle(); s.area()
Ad-hoc (Compile-time)

Method overloading

Same method name, different parameter types. The compiler picks the right version based on argument types at compile time. (Java/C++ only — Python and JS don't have true overloading.)

add(int,int) vs add(double,double)
Parametric

Generics / templates

A single generic class works with any type. List<Integer>, List<String> — same implementation, different type parameters.

List<T>, Result<T>, Optional<T>

Duck Typing (Python & JavaScript)

Python and JavaScript use structural polymorphism — if an object has the right method, it works, regardless of its class hierarchy. This is called duck typing: "if it walks like a duck and quacks like a duck, it's a duck."

# Any object with a describe() method works — no base class needed
def print_description(thing):
    thing.describe()   # polymorphic — resolved at runtime

print_description(Circle("red", 5))   # calls Circle.describe()
print_description(Star())             # Star doesn't extend Shape — doesn't matter!

Code Example

// ── Polymorphism — many forms, one interface ──────────────────

// Polymorphism = the same method call produces different behaviour
// depending on the actual runtime type of the object.

// ─── Shape hierarchy ──────────────────────────────────────────
abstract class Shape {
    protected String colour;

    public Shape(String colour) { this.colour = colour; }

    // Abstract method — every shape MUST provide its own implementation
    public abstract double area();
    public abstract double perimeter();

    // Non-abstract — shared by all shapes, calls the abstract methods
    public void describe() {
        System.out.printf("%s %s — area=%.2f, perimeter=%.2f%n",
                colour, getClass().getSimpleName(), area(), perimeter());
    }
}

class Circle extends Shape {
    private final double radius;
    public Circle(String colour, double radius) {
        super(colour); this.radius = radius;
    }
    @Override public double area()      { return Math.PI * radius * radius; }
    @Override public double perimeter() { return 2 * Math.PI * radius; }
}

class Rectangle extends Shape {
    private final double width, height;
    public Rectangle(String colour, double width, double height) {
        super(colour); this.width = width; this.height = height;
    }
    @Override public double area()      { return width * height; }
    @Override public double perimeter() { return 2 * (width + height); }
}

class Triangle extends Shape {
    private final double a, b, c;
    public Triangle(String colour, double a, double b, double c) {
        super(colour); this.a = a; this.b = b; this.c = c;
    }
    @Override public double area() {
        double s = (a + b + c) / 2;
        return Math.sqrt(s * (s-a) * (s-b) * (s-c)); // Heron's formula
    }
    @Override public double perimeter() { return a + b + c; }
}

// ─── Runtime polymorphism — single loop handles all shapes ────
public class DrawingApp {
    public static void main(String[] args) {
        List<Shape> canvas = List.of(
            new Circle("Red",    5),
            new Rectangle("Blue", 4, 6),
            new Triangle("Green", 3, 4, 5)
        );

        // SAME call: shape.describe() — DIFFERENT behaviour per type
        double totalArea = 0;
        for (Shape shape : canvas) {
            shape.describe();        // ← polymorphic dispatch
            totalArea += shape.area(); // ← polymorphic dispatch
        }
        System.out.printf("Total canvas area: %.2f%n", totalArea);
    }
}

// ─── Method overloading — compile-time (ad-hoc) polymorphism ──
class Calculator {
    public int    add(int a, int b)       { return a + b; }
    public double add(double a, double b) { return a + b; }
    public String add(String a, String b) { return a + b; }
    // Same name, different signatures → resolved at compile time
}

Why Polymorphism Matters

  • Write once, run for any type — no if-else per type
  • New types can be added without changing existing code (OCP)
  • Code is shorter, more expressive, and easier to maintain
  • The calling code doesn't need to know concrete types

Real-World Examples

  • Collections.sort(): Works for any List of Comparable — String, Integer, LocalDate
  • React render(): Every component has render — React calls it polymorphically for the whole tree
  • Python print(): Calls __str__ on anything — your class gets formatting for free
  • SQL drivers: execute() works on MySQL, Postgres, SQLite connections identically
Concept 5 — Intermediate

Abstraction

"Show only what's necessary. Hide the complexity behind a clean, simple interface."

Abstraction vs Encapsulation

These are closely related but distinct: Encapsulation is about how you hide implementation details — using access modifiers, private fields, controlled getters. Abstraction is about what you hide — the entire implementation, replaced by a simpler interface the caller programs against.

Encapsulation (HOW)

Hides internals of a class. You can still see the class exists and call its methods. The hiding is about state and implementation details within a class.

Abstraction (WHAT)

Hides entire implementations behind an interface. You only know the contract — PaymentGateway.processPayment(). Whether it's Stripe or Razorpay underneath is invisible.

Levels of Abstraction

Methodlist.sort() — you don't know it's Timsort
ClassFile — you call read(), not syscall open/mmap
InterfacePaymentGateway — Stripe/Razorpay hidden behind contract
ModuleHTTP client — TCP/IP entirely hidden behind requests.get()
ServiceS3 API — distributed storage hidden behind put_object()

The Template Method Pattern: Abstract classes naturally implement the Template Method pattern — the base class defines the algorithm skeleton (the abstract structure), and subclasses fill in the specific steps. This is a direct expression of abstraction in code.

Code Example

// ── Abstraction — hide complexity, expose essential interface ───

// Abstraction = showing WHAT an object does, hiding HOW it does it.
// In Java: abstract classes and interfaces are the two tools.

// Abstract class — can have both abstract AND concrete methods
public abstract class PaymentGateway {

    // Template method — defines the WHAT (the workflow)
    // Subclasses fill in the HOW for each step
    public final PaymentResult processPayment(PaymentRequest req) {
        validateRequest(req);            // concrete — shared logic
        PaymentResult result = charge(req);  // abstract — different per gateway
        audit(req, result);              // concrete — shared logic
        return result;
    }

    // Abstract — subclasses MUST implement
    protected abstract PaymentResult charge(PaymentRequest req);

    // Concrete — shared across all gateways
    private void validateRequest(PaymentRequest req) {
        if (req.getAmount() <= 0)
            throw new IllegalArgumentException("Amount must be positive");
        if (req.getCurrency() == null)
            throw new IllegalArgumentException("Currency required");
    }

    private void audit(PaymentRequest req, PaymentResult result) {
        System.out.printf("[AUDIT] %s | %.2f %s | status=%s%n",
                req.getGateway(), req.getAmount(),
                req.getCurrency(), result.getStatus());
    }
}

// Concrete implementation — provides the HOW
public class StripeGateway extends PaymentGateway {
    @Override
    protected PaymentResult charge(PaymentRequest req) {
        // Stripe-specific HTTP calls, SDK calls, error handling...
        System.out.println("[Stripe] Charging card via Stripe API...");
        return new PaymentResult("SUCCESS", "ch_stripe_" + System.nanoTime());
    }
}

public class RazorpayGateway extends PaymentGateway {
    @Override
    protected PaymentResult charge(PaymentRequest req) {
        System.out.println("[Razorpay] Initiating Razorpay payment...");
        return new PaymentResult("SUCCESS", "pay_rzp_" + System.nanoTime());
    }
}

// ── Client uses the abstraction — doesn't know (or care) which gateway ─
PaymentGateway gateway = new StripeGateway();  // or RazorpayGateway
PaymentResult  result  = gateway.processPayment(
    new PaymentRequest(999.00, "INR", "Stripe"));
System.out.println("Result: " + result.getStatus());
// No Stripe-specific code in the client. Pure abstraction.

Real-World Examples

JDBC (Java)

Connection, Statement, ResultSet abstract away every database vendor. Code written against JDBC runs unchanged on MySQL, Postgres, or SQLite.

Python's ABC module

ABCMeta and @abstractmethod enforce contracts. Subclasses that miss an abstract method raise TypeError at import time — caught before runtime.

Express.js Request/Response

req and res abstract raw Node.js IncomingMessage and ServerResponse. You call res.json() without knowing the HTTP wire format underneath.

Cloud Storage SDK

AWS S3, GCS, Azure Blob all expose a similar put/get/delete abstraction. Infrastructure changes; your code doesn't.

Concept 6 — Intermediate

Interfaces vs Abstract Classes

"Interfaces define what a type can DO. Abstract classes define what a type IS, with some of the HOW already filled in."

When to Use Which

Use an Interface / Protocol when:

  • Defining a capability a class can have (Serialisable, Comparable, Printable)
  • Multiple unrelated classes need to share a contract
  • A class should satisfy multiple roles simultaneously
  • You want pure duck typing (Python Protocols)
  • No shared implementation to provide

Use an Abstract Class when:

  • Sharing concrete code among closely related classes
  • You need a constructor or instance state in the base
  • Template Method pattern — the algorithm skeleton belongs here
  • Strong IS-A relationship (Report, PaymentGateway, Animal)
  • Some steps are genuinely common to all subclasses

Feature Comparison

FeatureInterface / ProtocolAbstract Class
Multiple inheritance✓ implement many✗ extend one (Java/JS)
Constructor
Instance fields✗ (only constants)
Concrete methodsdefault only (Java 8+)✓ fully
Relationship"can do" (CAN-DO)"is a kind of" (IS-A)
Instantiate directly

Code Example

// ── Interface vs Abstract Class — when to use which ───────────

// ─── INTERFACE ─────────────────────────────────────────────────
// • Pure contract — only method signatures (Java 8+: default methods too)
// • A class can implement MULTIPLE interfaces
// • No constructor, no instance state
// • Use when: defining a capability/role a class can play

public interface Serialisable {
    byte[] serialise();
    static <T> T deserialise(byte[] data, Class<T> type) { /* ... */ return null; }
    default String toBase64() {
        return Base64.getEncoder().encodeToString(serialise());
    }
}

public interface Auditable {
    String getAuditId();
    LocalDateTime getCreatedAt();
    LocalDateTime getUpdatedAt();
}

public interface Publishable {
    void publish(String topic);
}

// A class can wear many interface "hats"
public class OrderEvent implements Serialisable, Auditable, Publishable {
    private final String orderId;
    private final LocalDateTime createdAt = LocalDateTime.now();

    public OrderEvent(String orderId) { this.orderId = orderId; }

    @Override public byte[]         serialise()    { return orderId.getBytes(); }
    @Override public String         getAuditId()   { return orderId; }
    @Override public LocalDateTime  getCreatedAt() { return createdAt; }
    @Override public LocalDateTime  getUpdatedAt() { return createdAt; }
    @Override public void           publish(String topic) {
        System.out.println("Published " + orderId + " to " + topic);
    }
}

// ─── ABSTRACT CLASS ────────────────────────────────────────────
// • Partial implementation — mix of abstract + concrete methods
// • Can have constructor, fields, protected helpers
// • A class can extend only ONE abstract class (single inheritance)
// • Use when: sharing code + enforcing a contract across related classes

public abstract class Report {
    protected final String  title;
    protected final LocalDate date;

    protected Report(String title) {
        this.title = title;
        this.date  = LocalDate.now();
    }

    // Template method — locked workflow
    public final String generate() {
        return buildHeader() + "\n" + buildBody() + "\n" + buildFooter();
    }

    private String buildHeader() {
        return "=== " + title.toUpperCase() + " | " + date + " ===";
    }

    private String buildFooter() {
        return "=== End of Report ===";
    }

    // Subclasses fill in the body
    protected abstract String buildBody();
}

public class SalesReport extends Report {
    private final List<Sale> sales;
    public SalesReport(List<Sale> sales) { super("Sales Report"); this.sales = sales; }
    @Override protected String buildBody() {
        return sales.stream()
                    .map(s -> s.item() + ": ₹" + s.amount())
                    .collect(Collectors.joining("\n"));
    }
}

// ─── Comparison Table ──────────────────────────────────────────
//  Feature              │ Interface          │ Abstract Class
// ─────────────────────┼────────────────────┼──────────────────
//  Multiple inheritance │ ✓ (implement many) │ ✗ (extend one)
//  Constructors         │ ✗                  │ ✓
//  Instance fields      │ ✗ (only constants) │ ✓
//  Partial impl         │ default methods    │ ✓ fully
//  IS-A relationship    │ "can do"           │ "is a kind of"
//  Use when             │ defining a role    │ sharing code
Concept 7 — Advanced

Composition vs Inheritance

"Favour object composition over class inheritance." — Gang of Four (Design Patterns, 1994)

The Problem with Deep Inheritance

Inheritance creates a compile-time binding between parent and child. It exposes parent internals to subclasses (violating encapsulation), creates a fragile dependency (the Fragile Base Class problem), and leads to deep hierarchies that are hard to reason about.

Worse, inheritance forces an all-or-nothing choice — a Penguin must inherit all FlyingAnimal behaviour even though it can't fly. You end up with throw-based "implementations" that violate Liskov.

❌ Inheritance problems

  • Fragile Base Class — parent changes cascade to all children
  • Can't mix capabilities (fly + swim) in single-inheritance languages
  • Behaviour locked at compile time — can't swap at runtime
  • Deep hierarchies become hard to understand
  • Forces IS-A even when the relationship is really HAS-A

✅ Composition advantages

  • Mix any capabilities freely — duck swims AND flies
  • Swap behaviours at runtime (Strategy pattern)
  • No coupling to parent internals
  • Each capability object is independently testable
  • Hierarchies stay shallow and clear

The rule of thumb: Use inheritance only for genuine IS-A relationships where the subtype truly is a more specific version of the parent AND honours its full contract (Liskov). For everything else — capabilities, behaviours, shared utilities — use composition. When in doubt, compose.

Code Example

// ── Composition vs Inheritance ────────────────────────────────

// ─── ❌ Inheritance overuse — "is-a" forced where it doesn't fit ─
class Animal {
    public void eat()   { System.out.println("Eating..."); }
    public void sleep() { System.out.println("Sleeping..."); }
}

class FlyingAnimal extends Animal {
    public void fly() { System.out.println("Flying..."); }
}

class SwimmingAnimal extends Animal {
    public void swim() { System.out.println("Swimming..."); }
}

// What about a Duck that flies AND swims?
// FlyingSwimmingAnimal? Java has single inheritance — stuck!
// What about a Penguin (swims but can't fly)?

// ─── ✅ Composition — "has-a" capability objects ──────────────
// Define capabilities as independent pieces
@FunctionalInterface
interface FlyBehaviour   { void fly(); }
@FunctionalInterface
interface SwimBehaviour  { void swim(); }
@FunctionalInterface
interface SoundBehaviour { void makeSound(); }

// Concrete behaviour implementations
class WingFlight  implements FlyBehaviour  { public void fly()  { System.out.println("Flying with wings!"); } }
class RocketPack  implements FlyBehaviour  { public void fly()  { System.out.println("ROCKET PROPULSION!"); } }
class NoFlight    implements FlyBehaviour  { public void fly()  { System.out.println("Can't fly."); } }

class BreaststrokeSwim implements SwimBehaviour { public void swim() { System.out.println("Swimming gracefully."); } }
class NoSwim           implements SwimBehaviour { public void swim() { System.out.println("Can't swim."); } }

// Compose behaviours into animals
class Animal {
    private final String      name;
    private final FlyBehaviour  flyBehaviour;
    private final SwimBehaviour swimBehaviour;
    private final SoundBehaviour soundBehaviour;

    public Animal(String name, FlyBehaviour fly, SwimBehaviour swim, SoundBehaviour sound) {
        this.name          = name;
        this.flyBehaviour  = fly;
        this.swimBehaviour = swim;
        this.soundBehaviour = sound;
    }

    public void performActions() {
        System.out.print(name + ": ");
        flyBehaviour.fly();
        System.out.print(name + ": ");
        swimBehaviour.swim();
        System.out.print(name + ": ");
        soundBehaviour.makeSound();
    }

    // Behaviours can be swapped at RUNTIME — inheritance can't do this
    public Animal withFlyBehaviour(FlyBehaviour newFly) {
        return new Animal(name, newFly, swimBehaviour, soundBehaviour);
    }
}

// Compose freely
Animal duck    = new Animal("Duck",    new WingFlight(),  new BreaststrokeSwim(), () -> System.out.println("Quack!"));
Animal penguin = new Animal("Penguin", new NoFlight(),    new BreaststrokeSwim(), () -> System.out.println("Honk!"));
Animal eagle   = new Animal("Eagle",   new WingFlight(),  new NoSwim(),           () -> System.out.println("Screech!"));
Animal jetpack = new Animal("Human",   new RocketPack(),  new NoSwim(),           () -> System.out.println("Woohoo!"));

duck.performActions();
penguin.performActions();
eagle.performActions();
Concept 8 — Advanced

Advanced OOP

"Generics, descriptors, metaclasses, proxies, and language-level object protocols — the mechanics that power every framework."

Generics / Type Parameters

Write one class that works safely with any type. Result<T> wraps any success value. List<T> holds any element type. The type parameter is checked at compile time — no runtime ClassCastException.

Result<User>, Optional<String>, Map<String, Integer>

Builder Pattern

Fluent construction of complex objects step by step. Especially useful when a constructor would need too many parameters. Creates immutable objects with readable construction code.

HttpRequest.builder("POST", url).header(...).body(...).build()

Descriptors (Python)

__get__, __set__, __set_name__ — intercept attribute access at the class level. Power @property, @classmethod, @staticmethod, and ORMs like Django's Field classes.

class Validated: def __set__(self, obj, val): validate(val)

Metaclasses (Python)

Classes that control class creation. type is the metaclass of all classes. Custom metaclasses enforce patterns at definition time — Singleton, ORMs, ABC enforcement.

class SingletonMeta(type): def __call__(cls, *a, **kw): ...

Proxy Object (JS)

Intercept fundamental object operations (get, set, has, delete) on any object. Powers Vue 3's reactivity, validation layers, and observability without modifying the target.

new Proxy(target, { set(obj, prop, val) { validate(val); ... } })

Iterator Protocol

Any object that implements [Symbol.iterator] (JS) or __iter__/__next__ (Python) can be iterated with for...of / for...in loops and spread operators.

class Range { [Symbol.iterator]() { ... } }; [...new Range(1,10)]

Code Example

// ── Advanced OOP — Generics, Covariance, Builder, Fluent API ──

import java.util.*;
import java.util.function.*;

// ─── 1. Generics — type-safe, reusable classes ────────────────
public class Result<T> {
    private final T     value;
    private final String error;
    private final boolean success;

    private Result(T value, String error, boolean success) {
        this.value = value; this.error = error; this.success = success;
    }

    public static <T> Result<T> ok(T value)      { return new Result<>(value, null, true); }
    public static <T> Result<T> fail(String err) { return new Result<>(null, err, false); }

    public boolean isSuccess()  { return success; }
    public T       getValue()   { if (!success) throw new NoSuchElementException(); return value; }
    public String  getError()   { return error; }

    // Functor — transform the value if present
    public <U> Result<U> map(Function<T, U> mapper) {
        return success ? Result.ok(mapper.apply(value)) : Result.fail(error);
    }

    // Monad — chain Result-returning operations
    public <U> Result<U> flatMap(Function<T, Result<U>> mapper) {
        return success ? mapper.apply(value) : Result.fail(error);
    }
}

// Usage — railway-oriented error handling
Result<Integer> result = Result.ok("42")
    .map(Integer::parseInt)
    .flatMap(n -> n > 0 ? Result.ok(n * 2) : Result.fail("Must be positive"));

// ─── 2. Builder Pattern — fluent construction ─────────────────
public class HttpRequest {
    private final String method;
    private final String url;
    private final Map<String, String> headers;
    private final String body;
    private final int timeoutMs;

    private HttpRequest(Builder b) {
        this.method    = b.method;
        this.url       = b.url;
        this.headers   = Collections.unmodifiableMap(b.headers);
        this.body      = b.body;
        this.timeoutMs = b.timeoutMs;
    }

    public static Builder builder(String method, String url) {
        return new Builder(method, url);
    }

    public static class Builder {
        private final String method, url;
        private final Map<String, String> headers = new LinkedHashMap<>();
        private String body = "";
        private int timeoutMs = 5000;

        private Builder(String method, String url) {
            this.method = method; this.url = url;
        }

        public Builder header(String k, String v) { headers.put(k, v); return this; }
        public Builder body(String body)           { this.body = body;  return this; }
        public Builder timeout(int ms)             { timeoutMs = ms;    return this; }
        public HttpRequest build()                 { return new HttpRequest(this); }
    }
}

HttpRequest req = HttpRequest.builder("POST", "https://api.example.com/orders")
    .header("Content-Type", "application/json")
    .header("Authorization", "Bearer token123")
    .body("{"item":"laptop","qty":1}")
    .timeout(3000)
    .build();

// ─── 3. Bounded generics — constrain the type parameter ───────
public <T extends Comparable<T>> T findMax(List<T> list) {
    return list.stream().max(Comparator.naturalOrder())
               .orElseThrow(() -> new NoSuchElementException("Empty list"));
}

Integer max = findMax(List.of(3, 1, 4, 1, 5, 9, 2, 6)); // 9

Where Advanced OOP Powers Real Frameworks

Django ORM Fields

Django's CharField, IntegerField are Python Descriptors. They intercept __set__ on model instances to validate data before it reaches the database.

Vue 3 Reactivity

Vue's reactive() wraps objects in a JavaScript Proxy. When you set state.count = 5, the Proxy's set trap fires, triggering DOM updates automatically.

Java Generics in Collections

ArrayList<User> uses generics to give you compile-time type safety. The JVM erases types at runtime (type erasure), but javac catches ClassCastException at compile time.

Python's @dataclass

A metaclass-like decorator that inspects class annotations at definition time and generates __init__, __repr__, __eq__ automatically — metaprogramming applied to OOP.

OOP Concepts at a Glance

ConceptCore Question
Classes & Objects"What blueprint does this data and behaviour belong to?"
Encapsulation"Who is allowed to change this state?"
Inheritance"Is this a more specific version of the parent?"
Polymorphism"Can one call work for all types without if-else?"
Abstraction"Can the caller ignore the implementation details?"
Composition"Should I use HAS-A instead of IS-A here?"
Advanced OOP"Can a language feature handle this pattern automatically?"

Take OOP Further with Design Patterns

Design patterns are proven OOP solutions to recurring design problems. Each pattern applies one or more OOP concepts to solve a specific structural challenge: