Object-Oriented Programming Concepts

Object-Oriented Programming (OOP) is a programming paradigm based on objects containing data and methods. This guide covers all essential OOP concepts.

Four Pillars of OOP #

1. Encapsulation #

Bundle data and methods that operate on that data within a single unit.

class BankAccount {
  #balance = 0;  // Private field

  constructor(initialBalance) {
    this.#balance = initialBalance;
  }

  deposit(amount) {
    if (amount > 0) {
      this.#balance += amount;
    }
  }

  withdraw(amount) {
    if (amount > 0 && amount <= this.#balance) {
      this.#balance -= amount;
      return true;
    }
    return false;
  }

  getBalance() {
    return this.#balance;
  }
}

const account = new BankAccount(1000);
account.deposit(500);
console.log(account.getBalance());  // 1500
// account.#balance  // Error: Private field

2. Abstraction #

Hide complex implementation details, expose simple interface.

class Coffee {
  makeCoffee() {
    this.boilWater();
    this.brewCoffee();
    this.pourInCup();
  }

  boilWater() {
    console.log("Boiling water");
  }

  brewCoffee() {
    console.log("Brewing coffee");
  }

  pourInCup() {
    console.log("Pouring in cup");
  }
}

const coffee = new Coffee();
coffee.makeCoffee();  // Simple interface

3. Inheritance #

Create new classes based on existing classes.

class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(`${this.name} makes a sound`);
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name);
    this.breed = breed;
  }

  speak() {
    console.log(`${this.name} barks`);
  }

  fetch() {
    console.log(`${this.name} fetches the ball`);
  }
}

const dog = new Dog("Max", "Golden Retriever");
dog.speak();  // "Max barks"
dog.fetch();  // "Max fetches the ball"

4. Polymorphism #

Objects of different types can be accessed through the same interface.

class Shape {
  area() {
    throw new Error("Method must be implemented");
  }
}

class Circle extends Shape {
  constructor(radius) {
    super();
    this.radius = radius;
  }

  area() {
    return Math.PI * this.radius ** 2;
  }
}

class Rectangle extends Shape {
  constructor(width, height) {
    super();
    this.width = width;
    this.height = height;
  }

  area() {
    return this.width * this.height;
  }
}

const shapes = [
  new Circle(5),
  new Rectangle(4, 6)
];

shapes.forEach(shape => {
  console.log(shape.area());
});

Classes and Objects #

Basic Class #

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  greet() {
    return `Hello, I'm ${this.name}`;
  }

  get info() {
    return `${this.name}, ${this.age} years old`;
  }

  set birthYear(year) {
    this.age = new Date().getFullYear() - year;
  }
}

const person = new Person("Alice", 25);
console.log(person.greet());
console.log(person.info);
person.birthYear = 1998;

Static Methods and Properties #

class MathUtils {
  static PI = 3.14159;

  static add(a, b) {
    return a + b;
  }

  static multiply(a, b) {
    return a * b;
  }
}

console.log(MathUtils.PI);
console.log(MathUtils.add(5, 3));

Private Fields and Methods #

class Counter {
  #count = 0;

  increment() {
    this.#count++;
    this.#log();
  }

  #log() {
    console.log(`Count: ${this.#count}`);
  }

  getCount() {
    return this.#count;
  }
}

const counter = new Counter();
counter.increment();
console.log(counter.getCount());

Composition vs Inheritance #

Inheritance (Is-A Relationship) #

class Vehicle {
  start() {
    console.log("Starting vehicle");
  }
}

class Car extends Vehicle {
  drive() {
    console.log("Driving car");
  }
}

Composition (Has-A Relationship) #

class Engine {
  start() {
    console.log("Engine starting");
  }
}

class Car {
  constructor() {
    this.engine = new Engine();
  }

  start() {
    this.engine.start();
    console.log("Car started");
  }
}

Prefer composition - More flexible, avoids deep inheritance hierarchies.

SOLID Principles #

S - Single Responsibility #

Class should have one reason to change.

// Bad
class User {
  constructor(name) {
    this.name = name;
  }

  saveToDatabase() {
    // Database logic
  }

  sendEmail() {
    // Email logic
  }
}

// Good
class User {
  constructor(name) {
    this.name = name;
  }
}

class UserRepository {
  save(user) {
    // Database logic
  }
}

class EmailService {
  send(user, message) {
    // Email logic
  }
}

O - Open/Closed #

Open for extension, closed for modification.

class Shape {
  area() {
    throw new Error("Must implement");
  }
}

class Circle extends Shape {
  constructor(radius) {
    super();
    this.radius = radius;
  }

  area() {
    return Math.PI * this.radius ** 2;
  }
}

// Add new shapes without modifying existing code
class Triangle extends Shape {
  constructor(base, height) {
    super();
    this.base = base;
    this.height = height;
  }

  area() {
    return (this.base * this.height) / 2;
  }
}

L - Liskov Substitution #

Subclasses should be substitutable for base classes.

class Bird {
  fly() {
    console.log("Flying");
  }
}

class Sparrow extends Bird {
  fly() {
    console.log("Sparrow flying");
  }
}

// Violates LSP - Penguin can't fly
class Penguin extends Bird {
  fly() {
    throw new Error("Penguins can't fly");
  }
}

// Better design
class Bird {
  move() {
    console.log("Moving");
  }
}

class FlyingBird extends Bird {
  fly() {
    console.log("Flying");
  }
}

class Penguin extends Bird {
  swim() {
    console.log("Swimming");
  }
}

I - Interface Segregation #

Many specific interfaces better than one general.

// Bad - Fat interface
class Worker {
  work() {}
  eat() {}
  sleep() {}
}

// Good - Segregated
class Workable {
  work() {}
}

class Eatable {
  eat() {}
}

class Human extends Workable {
  work() {
    console.log("Working");
  }
}

D - Dependency Inversion #

Depend on abstractions, not concretions.

// Bad
class MySQLDatabase {
  save(data) {
    console.log("Saving to MySQL");
  }
}

class UserService {
  constructor() {
    this.database = new MySQLDatabase();
  }

  saveUser(user) {
    this.database.save(user);
  }
}

// Good
class Database {
  save(data) {
    throw new Error("Must implement");
  }
}

class MySQLDatabase extends Database {
  save(data) {
    console.log("Saving to MySQL");
  }
}

class UserService {
  constructor(database) {
    this.database = database;
  }

  saveUser(user) {
    this.database.save(user);
  }
}

const db = new MySQLDatabase();
const service = new UserService(db);

Design Patterns #

Singleton #

Only one instance exists.

class Database {
  constructor() {
    if (Database.instance) {
      return Database.instance;
    }
    this.connection = null;
    Database.instance = this;
  }

  connect() {
    if (!this.connection) {
      this.connection = "Connected";
    }
    return this.connection;
  }
}

const db1 = new Database();
const db2 = new Database();
console.log(db1 === db2);  // true

Factory #

Create objects without specifying exact class.

class CarFactory {
  createCar(type) {
    switch(type) {
      case 'sedan':
        return new Sedan();
      case 'suv':
        return new SUV();
      default:
        throw new Error("Unknown car type");
    }
  }
}

class Sedan {
  constructor() {
    this.type = "Sedan";
  }
}

class SUV {
  constructor() {
    this.type = "SUV";
  }
}

const factory = new CarFactory();
const car = factory.createCar('sedan');

Observer #

Subscribe to and receive notifications.

class Subject {
  constructor() {
    this.observers = [];
  }

  subscribe(observer) {
    this.observers.push(observer);
  }

  unsubscribe(observer) {
    this.observers = this.observers.filter(obs => obs !== observer);
  }

  notify(data) {
    this.observers.forEach(observer => observer.update(data));
  }
}

class Observer {
  update(data) {
    console.log("Received:", data);
  }
}

const subject = new Subject();
const observer1 = new Observer();
const observer2 = new Observer();

subject.subscribe(observer1);
subject.subscribe(observer2);
subject.notify("Hello observers!");

OOP in Different Languages #

Python #

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

    def speak(self):
        pass

class Dog(Animal):
    def speak(self):
        return f"{self.name} barks"

dog = Dog("Max")
print(dog.speak())

Java #

public class Animal {
    private String name;

    public Animal(String name) {
        this.name = name;
    }

    public void speak() {
        System.out.println(name + " makes a sound");
    }
}

public class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }

    @Override
    public void speak() {
        System.out.println(getName() + " barks");
    }
}

Best Practices #

  1. Keep classes small - Single Responsibility
  2. Favor composition over inheritance
  3. Program to interfaces not implementations
  4. Use meaningful names - Clear and descriptive
  5. Encapsulate properly - Private by default
  6. Avoid deep hierarchies - Max 3-4 levels
  7. Test classes independently - Loose coupling
  8. Document public APIs - Help other developers

OOP helps organize complex code into manageable, reusable pieces. Master these concepts to write maintainable software.