OOP
03 / 10

Core Principles

Object-Oriented Programming: Core Principles

Object-Oriented Programming (OOP) is a programming paradigm based on the concept of "objects" that contain data and code. It organizes software design around data, or objects, rather than functions and logic.

The Four Pillars of OOP

1. Encapsulation

Encapsulation is the bundling of data and methods that operate on that data within a single unit (class). It restricts direct access to some of an object's components, which is a means of preventing accidental interference and misuse.

class BankAccount {
  private balance: number;
  private accountNumber: string;

  constructor(accountNumber: string, initialBalance: number) {
    this.accountNumber = accountNumber;
    this.balance = initialBalance;
  }

  // Public method to access private data
  public getBalance(): number {
    return this.balance;
  }

  // Public method to modify private data safely
  public deposit(amount: number): void {
    if (amount > 0) {
      this.balance += amount;
      console.log(`Deposited $${amount}. New balance: $${this.balance}`);
    } else {
      console.log("Deposit amount must be positive");
    }
  }

  public withdraw(amount: number): boolean {
    if (amount > 0 && amount <= this.balance) {
      this.balance -= amount;
      console.log(`Withdrew $${amount}. New balance: $${this.balance}`);
      return true;
    }
    console.log("Insufficient funds or invalid amount");
    return false;
  }
}

// Usage
const myAccount = new BankAccount("12345", 1000);
myAccount.deposit(500);
console.log(myAccount.getBalance()); // 1500
// myAccount.balance = 10000; // ERROR: Cannot access private property

2. Abstraction

Abstraction means hiding complex implementation details and showing only the necessary features of an object. It helps reduce programming complexity and effort.

// Abstract class defining the interface
abstract class Vehicle {
  protected brand: string;

  constructor(brand: string) {
    this.brand = brand;
  }

  // Abstract method - must be implemented by derived classes
  abstract startEngine(): void;
  abstract stopEngine(): void;

  // Concrete method - shared by all vehicles
  public displayInfo(): void {
    console.log(`This is a ${this.brand} vehicle`);
  }
}

// Concrete implementation
class Car extends Vehicle {
  startEngine(): void {
    console.log(`${this.brand} car engine started with key turn`);
  }

  stopEngine(): void {
    console.log(`${this.brand} car engine stopped`);
  }
}

class ElectricCar extends Vehicle {
  startEngine(): void {
    console.log(`${this.brand} electric car powered on silently`);
  }

  stopEngine(): void {
    console.log(`${this.brand} electric car powered off`);
  }
}

// Usage - we don't need to know implementation details
const myCar: Vehicle = new Car("Toyota");
myCar.startEngine();
myCar.displayInfo();

const myTesla: Vehicle = new ElectricCar("Tesla");
myTesla.startEngine();

3. Inheritance

Inheritance allows a class to inherit properties and methods from another class. It promotes code reusability and establishes a relationship between parent and child classes.

// Base class (Parent)
class Animal {
  protected name: string;
  protected age: number;

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

  public makeSound(): void {
    console.log("Some generic animal sound");
  }

  public eat(): void {
    console.log(`${this.name} is eating`);
  }

  public sleep(): void {
    console.log(`${this.name} is sleeping`);
  }
}

// Derived class (Child)
class Dog extends Animal {
  private breed: string;

  constructor(name: string, age: number, breed: string) {
    super(name, age); // Call parent constructor
    this.breed = breed;
  }

  // Override parent method
  public makeSound(): void {
    console.log(`${this.name} barks: Woof! Woof!`);
  }

  // Additional method specific to Dog
  public fetch(): void {
    console.log(`${this.name} is fetching the ball`);
  }
}

class Cat extends Animal {
  constructor(name: string, age: number) {
    super(name, age);
  }

  // Override parent method
  public makeSound(): void {
    console.log(`${this.name} meows: Meow!`);
  }

  // Additional method specific to Cat
  public scratch(): void {
    console.log(`${this.name} is scratching the furniture`);
  }
}

// Usage
const dog = new Dog("Buddy", 3, "Golden Retriever");
dog.makeSound(); // Buddy barks: Woof! Woof!
dog.eat();       // Buddy is eating
dog.fetch();     // Buddy is fetching the ball

const cat = new Cat("Whiskers", 2);
cat.makeSound(); // Whiskers meows: Meow!
cat.scratch();   // Whiskers is scratching the furniture

4. Polymorphism

Polymorphism allows objects of different classes to be treated as objects of a common base class. It enables a single interface to represent different underlying forms (data types).

// Interface defining common behavior
interface Shape {
  calculateArea(): number;
  calculatePerimeter(): number;
  draw(): void;
}

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

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

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

  draw(): void {
    console.log(`Drawing a 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);
  }

  draw(): void {
    console.log(`Drawing a rectangle ${this.width}x${this.height}`);
  }
}

class Triangle implements Shape {
  constructor(
    private sideA: number,
    private sideB: number,
    private sideC: number
  ) {}

  calculateArea(): number {
    // Using Heron's formula
    const s = (this.sideA + this.sideB + this.sideC) / 2;
    return Math.sqrt(s * (s - this.sideA) * (s - this.sideB) * (s - this.sideC));
  }

  calculatePerimeter(): number {
    return this.sideA + this.sideB + this.sideC;
  }

  draw(): void {
    console.log(`Drawing a triangle with sides ${this.sideA}, ${this.sideB}, ${this.sideC}`);
  }
}

// Polymorphism in action - same interface, different implementations
function printShapeInfo(shape: Shape): void {
  shape.draw();
  console.log(`Area: ${shape.calculateArea().toFixed(2)}`);
  console.log(`Perimeter: ${shape.calculatePerimeter().toFixed(2)}`);
  console.log("---");
}

// All shapes can be treated uniformly
const shapes: Shape[] = [
  new Circle(5),
  new Rectangle(4, 6),
  new Triangle(3, 4, 5)
];

shapes.forEach(printShapeInfo);

Benefits of OOP

  • Modularity: Code is organized into self-contained objects

  • Reusability: Objects can be reused across different parts of a program or in different programs

  • Maintainability: Changes to one object don't affect others, making debugging easier

  • Scalability: New features can be added with minimal impact on existing code

  • Security: Encapsulation protects data from unauthorized access

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free