Back to Blogs
February 5, 2026
#OOP#Programming Fundamentals#Software Design#TypeScript

Object-Oriented Programming: Building Software from Real-World Blueprints

Object-Oriented Programming: Building Software from Real-World Blueprints

Before Object-Oriented Programming (OOP), code was written as a sequence of instructions. Functions operated on data, but there was no inherent relationship between them. OOP changed everything by introducing a radical idea: model software after the real world.

Instead of thinking in procedures, we think in objects. Objects bundle data and behavior together, creating self-contained units that mirror how we naturally understand the world.


The Four Pillars of OOP

Every object-oriented language is built on four foundational concepts:

  1. Encapsulation: Bundling data and methods, hiding internal details
  2. Abstraction: Exposing only what's necessary, hiding complexity
  3. Inheritance: Creating hierarchies, sharing common behavior
  4. Polymorphism: One interface, multiple implementations

Let's explore each with concrete examples.


1. Encapsulation: The Art of Information Hiding

The Philosophy

Encapsulation means bundling related data and the methods that operate on that data into a single unit (a class), while restricting direct access to some of the object's components.

Think of a car. You don't need to understand the internal combustion engine to drive. You interact through an interface: the steering wheel, pedals, and gear shift. The complexity is hidden.

Example: A Bank Account

class BankAccount {
  private balance: number; // Hidden from outside
  private accountNumber: string;
  private transactionHistory: Transaction[] = [];

  constructor(accountNumber: string, initialDeposit: number) {
    this.accountNumber = accountNumber;
    this.balance = initialDeposit;
    this.logTransaction("Initial Deposit", initialDeposit);
  }

  // Public interface
  public deposit(amount: number): void {
    if (amount <= 0) {
      throw new Error("Deposit amount must be positive");
    }
    this.balance += amount;
    this.logTransaction("Deposit", amount);
    console.log(`Deposited $${amount}. New balance: $${this.balance}`);
  }

  public withdraw(amount: number): void {
    if (amount <= 0) {
      throw new Error("Withdrawal amount must be positive");
    }
    if (amount > this.balance) {
      throw new Error("Insufficient funds");
    }
    this.balance -= amount;
    this.logTransaction("Withdrawal", -amount);
    console.log(`Withdrew $${amount}. New balance: $${this.balance}`);
  }

  public getBalance(): number {
    return this.balance; // Controlled access
  }

  public getStatement(): Transaction[] {
    return [...this.transactionHistory]; // Return copy, not reference
  }

  // Private helper method
  private logTransaction(type: string, amount: number): void {
    this.transactionHistory.push({
      type,
      amount,
      timestamp: new Date(),
      balanceAfter: this.balance
    });
  }
}

interface Transaction {
  type: string;
  amount: number;
  timestamp: Date;
  balanceAfter: number;
}

// Usage
const myAccount = new BankAccount("ACC001", 1000);
myAccount.deposit(500);
myAccount.withdraw(200);
console.log(`Current balance: $${myAccount.getBalance()}`);

// This would cause an error:
// myAccount.balance = 999999; // Error: Property 'balance' is private

Key Benefits:

  • The balance cannot be directly modified, preventing invalid states
  • Business rules (like positive amounts) are enforced
  • Internal representation can change without breaking external code

2. Abstraction: Hiding Complexity, Exposing Simplicity

The Philosophy

Abstraction is about creating a simplified model that captures the essential features while hiding unnecessary details. It's the difference between "how it works" and "how to use it."

Example: Media Player

// Abstract base class
abstract class MediaPlayer {
  protected currentTrack: string | null = null;
  protected isPlaying: boolean = false;
  protected volume: number = 50;

  // Public interface - same for all players
  public play(track: string): void {
    this.currentTrack = track;
    this.isPlaying = true;
    console.log(`Now playing: ${track}`);
    this.startPlayback(); // Delegated to subclass
  }

  public pause(): void {
    this.isPlaying = false;
    console.log("Playback paused");
    this.stopPlayback();
  }

  public setVolume(level: number): void {
    if (level < 0 || level > 100) {
      throw new Error("Volume must be between 0 and 100");
    }
    this.volume = level;
    this.adjustVolume(level);
  }

  // Abstract methods - must be implemented by subclasses
  protected abstract startPlayback(): void;
  protected abstract stopPlayback(): void;
  protected abstract adjustVolume(level: number): void;
}

class SpotifyPlayer extends MediaPlayer {
  protected startPlayback(): void {
    console.log("Streaming from Spotify servers...");
    // Spotify-specific playback logic
  }

  protected stopPlayback(): void {
    console.log("Pausing Spotify stream");
  }

  protected adjustVolume(level: number): void {
    console.log(`Setting Spotify volume to ${level}%`);
  }
}

class LocalMusicPlayer extends MediaPlayer {
  protected startPlayback(): void {
    console.log("Playing from local file system...");
    // Local file playback logic
  }

  protected stopPlayback(): void {
    console.log("Stopping local playback");
  }

  protected adjustVolume(level: number): void {
    console.log(`Adjusting system volume to ${level}%`);
  }
}

class YouTubeMusicPlayer extends MediaPlayer {
  protected startPlayback(): void {
    console.log("Loading YouTube video player...");
    // YouTube API calls
  }

  protected stopPlayback(): void {
    console.log("Pausing YouTube player");
  }

  protected adjustVolume(level: number): void {
    console.log(`Setting YouTube player volume to ${level}%`);
  }
}

// Usage - same interface for all players
function usePlayer(player: MediaPlayer) {
  player.play("Song Title");
  player.setVolume(75);
  player.pause();
}

usePlayer(new SpotifyPlayer());
usePlayer(new LocalMusicPlayer());
usePlayer(new YouTubeMusicPlayer());

Key Benefits:

  • Users interact with a simple, unified interface
  • Implementation details are hidden
  • Each player can have unique internal logic

3. Inheritance: Building on Foundations

The Philosophy

Inheritance allows you to create new classes based on existing ones, inheriting their properties and methods. This creates a hierarchy that represents "is-a" relationships.

Example: Employee Hierarchy

// Base class
class Employee {
  protected name: string;
  protected employeeId: string;
  protected salary: number;

  constructor(name: string, employeeId: string, salary: number) {
    this.name = name;
    this.employeeId = employeeId;
    this.salary = salary;
  }

  public getDetails(): string {
    return `${this.name} (ID: ${this.employeeId})`;
  }

  public getSalary(): number {
    return this.salary;
  }

  public giveRaise(percentage: number): void {
    this.salary *= (1 + percentage / 100);
    console.log(`${this.name} received a ${percentage}% raise. New salary: $${this.salary}`);
  }

  // Method that can be overridden
  public getRole(): string {
    return "Employee";
  }
}

// Derived class - Developer
class Developer extends Employee {
  private programmingLanguages: string[];
  private projectsCompleted: number = 0;

  constructor(name: string, employeeId: string, salary: number, languages: string[]) {
    super(name, employeeId, salary); // Call parent constructor
    this.programmingLanguages = languages;
  }

  public completeProject(): void {
    this.projectsCompleted++;
    console.log(`${this.name} completed project #${this.projectsCompleted}`);
  }

  public getSkills(): string[] {
    return [...this.programmingLanguages];
  }

  // Override parent method
  public getRole(): string {
    return "Software Developer";
  }

  // Additional method specific to developers
  public writeCode(language: string): void {
    if (this.programmingLanguages.includes(language)) {
      console.log(`${this.name} is writing ${language} code`);
    } else {
      console.log(`${this.name} doesn't know ${language}`);
    }
  }
}

// Another derived class - Manager
class Manager extends Employee {
  private teamMembers: Employee[] = [];

  constructor(name: string, employeeId: string, salary: number) {
    super(name, employeeId, salary);
  }

  public addTeamMember(employee: Employee): void {
    this.teamMembers.push(employee);
    console.log(`${employee.getDetails()} added to ${this.name}'s team`);
  }

  public getTeamSize(): number {
    return this.teamMembers.length;
  }

  public getRole(): string {
    return "Manager";
  }

  public conductReview(employee: Employee, rating: number): void {
    console.log(`${this.name} is reviewing ${employee.getDetails()}`);
    if (rating >= 4) {
      employee.giveRaise(10);
    }
  }
}

// Usage
const dev = new Developer("Alice", "DEV001", 80000, ["TypeScript", "Python", "Go"]);
dev.writeCode("TypeScript");
dev.completeProject();
console.log(`Role: ${dev.getRole()}`);

const manager = new Manager("Bob", "MGR001", 100000);
manager.addTeamMember(dev);
manager.conductReview(dev, 5);
console.log(`Team size: ${manager.getTeamSize()}`);

Key Benefits:

  • Code reuse - common functionality in base class
  • Logical hierarchy - represents real-world relationships
  • Extensibility - new employee types can be added easily

4. Polymorphism: One Interface, Many Forms

The Philosophy

Polymorphism means "many forms." It allows objects of different classes to be treated as objects of a common superclass. The same method call can produce different behaviors depending on the object type.

Example: Shape Calculator

// Interface defining the contract
interface Shape {
  calculateArea(): number;
  calculatePerimeter(): number;
  describe(): string;
}

class Circle implements Shape {
  constructor(private radius: number) {}

  calculateArea(): number {
    return Math.PI * this.radius ** 2;
  }

  calculatePerimeter(): number {
    return 2 * Math.PI * this.radius;
  }

  describe(): string {
    return `Circle with radius ${this.radius}`;
  }
}

class Rectangle implements Shape {
  constructor(private width: number, private height: number) {}

  calculateArea(): number {
    return this.width * this.height;
  }

  calculatePerimeter(): number {
    return 2 * (this.width + this.height);
  }

  describe(): string {
    return `Rectangle with width ${this.width} and height ${this.height}`;
  }
}

class Triangle implements Shape {
  constructor(private base: number, private height: number, private side1: number, private side2: number) {}

  calculateArea(): number {
    return 0.5 * this.base * this.height;
  }

  calculatePerimeter(): number {
    return this.base + this.side1 + this.side2;
  }

  describe(): string {
    return `Triangle with base ${this.base}`;
  }
}

// Polymorphic function - works with any Shape
function printShapeInfo(shape: Shape): void {
  console.log(shape.describe());
  console.log(`  Area: ${shape.calculateArea().toFixed(2)}`);
  console.log(`  Perimeter: ${shape.calculatePerimeter().toFixed(2)}`);
  console.log("---");
}

// Usage - same function, different behaviors
const shapes: Shape[] = [
  new Circle(5),
  new Rectangle(4, 6),
  new Triangle(5, 4, 3, 4)
];

shapes.forEach(shape => printShapeInfo(shape));

// Calculate total area of all shapes
const totalArea = shapes.reduce((sum, shape) => sum + shape.calculateArea(), 0);
console.log(`Total area of all shapes: ${totalArea.toFixed(2)}`);

Key Benefits:

  • Same interface for different types
  • Easy to add new shapes without changing existing code
  • Enables generic algorithms that work with any conforming type

Real-World Example: E-Commerce System

Let's combine all four pillars in a practical example:

// Encapsulation and Abstraction
abstract class Product {
  protected name: string;
  protected price: number;
  protected stock: number;

  constructor(name: string, price: number, stock: number) {
    this.name = name;
    this.price = price;
    this.stock = stock;
  }

  public getPrice(): number {
    return this.price;
  }

  public isInStock(): boolean {
    return this.stock > 0;
  }

  public reduceStock(quantity: number): void {
    if (quantity > this.stock) {
      throw new Error("Not enough stock");
    }
    this.stock -= quantity;
  }

  // Abstract method - each product type calculates differently
  abstract calculateShipping(): number;
  abstract getDescription(): string;
}

// Inheritance - Physical Products
class PhysicalProduct extends Product {
  private weight: number; // kg

  constructor(name: string, price: number, stock: number, weight: number) {
    super(name, price, stock);
    this.weight = weight;
  }

  calculateShipping(): number {
    // Shipping cost based on weight
    return this.weight * 2.5;
  }

  getDescription(): string {
    return `${this.name} - Physical item (${this.weight}kg)`;
  }
}

// Inheritance - Digital Products
class DigitalProduct extends Product {
  private downloadUrl: string;

  constructor(name: string, price: number, stock: number, downloadUrl: string) {
    super(name, price, stock);
    this.downloadUrl = downloadUrl;
  }

  calculateShipping(): number {
    return 0; // No shipping for digital products
  }

  getDescription(): string {
    return `${this.name} - Digital download`;
  }

  public getDownloadLink(): string {
    return this.downloadUrl;
  }
}

// Shopping Cart - Polymorphism in action
class ShoppingCart {
  private items: Map<Product, number> = new Map();

  public addItem(product: Product, quantity: number): void {
    if (!product.isInStock()) {
      throw new Error(`${product.getDescription()} is out of stock`);
    }

    const currentQuantity = this.items.get(product) || 0;
    this.items.set(product, currentQuantity + quantity);
    console.log(`Added ${quantity}x ${product.getDescription()}`);
  }

  public calculateTotal(): number {
    let total = 0;
    this.items.forEach((quantity, product) => {
      total += product.getPrice() * quantity;
      total += product.calculateShipping() * quantity; // Polymorphic call
    });
    return total;
  }

  public checkout(): void {
    console.log("\n=== Checkout ===");
    this.items.forEach((quantity, product) => {
      console.log(`${quantity}x ${product.getDescription()}`);
      console.log(`  Price: $${product.getPrice() * quantity}`);
      console.log(`  Shipping: $${product.calculateShipping() * quantity}`);
      product.reduceStock(quantity);
    });
    console.log(`\nTotal: $${this.calculateTotal().toFixed(2)}`);
  }
}

// Usage
const laptop = new PhysicalProduct("Gaming Laptop", 1200, 5, 2.5);
const ebook = new DigitalProduct("TypeScript Guide", 29.99, 999, "https://example.com/download");
const headphones = new PhysicalProduct("Wireless Headphones", 199.99, 10, 0.3);

const cart = new ShoppingCart();
cart.addItem(laptop, 1);
cart.addItem(ebook, 2);
cart.addItem(headphones, 1);
cart.checkout();

When to Use OOP

OOP excels when:

  • Modeling real-world entities: Users, products, vehicles
  • Building large systems: Multiple developers working on different parts
  • Requiring extensibility: Adding new features without breaking existing code
  • Managing state: Objects naturally encapsulate state and behavior

OOP may be overkill for:

  • Simple scripts or utilities
  • Pure data transformations (functional programming shines here)
  • Performance-critical systems where object overhead matters

The Mental Model

Think of OOP as building with LEGO blocks:

  • Classes are the blueprints for blocks
  • Objects are the actual blocks you create
  • Encapsulation keeps the internal gears hidden
  • Inheritance lets you create specialized blocks from basic ones
  • Polymorphism means different blocks can fit the same socket
  • Abstraction means you don't need to know how a block works internally

When you understand these concepts deeply, you stop writing code as a list of instructions and start designing systems as collaborating objects—just like the real world.

Dushyant singh // 2/5/2026Home