Back to blog

Classes, Objects, and Abstraction: Building Blocks of OOP

oopclassesobjectsabstractioninterfacestypescriptpythonjava
Classes, Objects, and Abstraction: Building Blocks of OOP

Introduction

Classes and objects are the fundamental building blocks of Object-Oriented Programming. Understanding how to design classes, create objects, and use abstraction effectively is essential for writing clean, maintainable code. In this post, we'll explore these concepts in depth and learn when to use abstract classes versus interfaces.

What You'll Learn

✅ Understand the difference between classes and objects
✅ Master abstraction with abstract classes and interfaces
✅ Know when to use abstract classes vs interfaces
✅ Apply constructors, static members, and access modifiers correctly
✅ Build well-designed class hierarchies

Prerequisites


Classes: The Blueprint

A class is a blueprint or template that defines the structure and behavior of objects. It specifies:

  • What data the object will hold (properties/attributes)
  • What actions the object can perform (methods/functions)

Think of a class like an architectural blueprint for a house. The blueprint describes how the house should be built, but it's not a house itself.

Anatomy of a Class

TypeScript:

class Car {
  // Properties (attributes) - the data
  brand: string;
  model: string;
  year: number;
  private mileage: number = 0;
 
  // Constructor - initialization
  constructor(brand: string, model: string, year: number) {
    this.brand = brand;
    this.model = model;
    this.year = year;
  }
 
  // Methods (behaviors) - the actions
  drive(distance: number): void {
    this.mileage += distance;
    console.log(`Driving ${distance} km. Total: ${this.mileage} km`);
  }
 
  getInfo(): string {
    return `${this.year} ${this.brand} ${this.model}`;
  }
 
  getMileage(): number {
    return this.mileage;
  }
}

Python:

class Car:
    # Constructor - initialization
    def __init__(self, brand: str, model: str, year: int):
        # Properties (attributes) - the data
        self.brand = brand
        self.model = model
        self.year = year
        self._mileage = 0  # Protected by convention
 
    # Methods (behaviors) - the actions
    def drive(self, distance: int) -> None:
        self._mileage += distance
        print(f"Driving {distance} km. Total: {self._mileage} km")
 
    def get_info(self) -> str:
        return f"{self.year} {self.brand} {self.model}"
 
    @property
    def mileage(self) -> int:
        return self._mileage

Java:

public class Car {
    // Properties (attributes) - the data
    private String brand;
    private String model;
    private int year;
    private int mileage = 0;
 
    // Constructor - initialization
    public Car(String brand, String model, int year) {
        this.brand = brand;
        this.model = model;
        this.year = year;
    }
 
    // Methods (behaviors) - the actions
    public void drive(int distance) {
        this.mileage += distance;
        System.out.println("Driving " + distance + " km. Total: " + mileage + " km");
    }
 
    public String getInfo() {
        return year + " " + brand + " " + model;
    }
 
    public int getMileage() {
        return mileage;
    }
}

Objects: Instances of Classes

An object is a specific instance created from a class. If a class is a blueprint, an object is the actual house built from that blueprint. Each object:

  • Has its own copy of instance data (properties)
  • Shares the same methods defined in the class
  • Exists in memory at runtime

Creating Objects

TypeScript:

// Creating objects (instances) from the Car class
const myCar = new Car("Toyota", "Camry", 2023);
const yourCar = new Car("Honda", "Civic", 2022);
 
// Each object has its own data
console.log(myCar.getInfo());   // "2023 Toyota Camry"
console.log(yourCar.getInfo()); // "2022 Honda Civic"
 
// Actions affect only that specific object
myCar.drive(100);
console.log(myCar.getMileage());   // 100
console.log(yourCar.getMileage()); // 0 (unaffected)

Python:

# Creating objects (instances) from the Car class
my_car = Car("Toyota", "Camry", 2023)
your_car = Car("Honda", "Civic", 2022)
 
# Each object has its own data
print(my_car.get_info())   # "2023 Toyota Camry"
print(your_car.get_info()) # "2022 Honda Civic"
 
# Actions affect only that specific object
my_car.drive(100)
print(my_car.mileage)   # 100
print(your_car.mileage) # 0 (unaffected)

Java:

// Creating objects (instances) from the Car class
Car myCar = new Car("Toyota", "Camry", 2023);
Car yourCar = new Car("Honda", "Civic", 2022);
 
// Each object has its own data
System.out.println(myCar.getInfo());   // "2023 Toyota Camry"
System.out.println(yourCar.getInfo()); // "2022 Honda Civic"
 
// Actions affect only that specific object
myCar.drive(100);
System.out.println(myCar.getMileage());   // 100
System.out.println(yourCar.getMileage()); // 0 (unaffected)

Class vs Object: Key Differences

AspectClassObject
DefinitionBlueprint/templateInstance of a class
ExistenceExists at compile timeExists at runtime
MemoryNo memory allocatedMemory allocated for data
CreationDefined onceCreated multiple times
AnalogyCookie cutterCookie

Constructors: Object Initialization

A constructor is a special method that initializes an object when it's created. It sets up the initial state of the object.

Constructor Patterns

TypeScript:

class User {
  private id: string;
  private email: string;
  private name: string;
  private createdAt: Date;
 
  // Primary constructor
  constructor(email: string, name: string) {
    this.id = this.generateId();
    this.email = email;
    this.name = name;
    this.createdAt = new Date();
  }
 
  private generateId(): string {
    return `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  }
 
  // Static factory method - alternative to constructor
  static createGuest(): User {
    return new User("guest@example.com", "Guest User");
  }
 
  static createAdmin(email: string): User {
    const admin = new User(email, "Admin");
    // Additional admin setup could go here
    return admin;
  }
 
  getInfo(): string {
    return `${this.name} (${this.email}) - Created: ${this.createdAt.toISOString()}`;
  }
}
 
// Usage
const user1 = new User("john@example.com", "John Doe");
const guest = User.createGuest();
const admin = User.createAdmin("admin@example.com");
 
console.log(user1.getInfo());
console.log(guest.getInfo());
console.log(admin.getInfo());

Python:

import uuid
from datetime import datetime
from typing import Optional
 
 
class User:
    def __init__(self, email: str, name: str):
        self._id = self._generate_id()
        self._email = email
        self._name = name
        self._created_at = datetime.now()
 
    def _generate_id(self) -> str:
        return f"user_{uuid.uuid4().hex[:12]}"
 
    # Class method - factory pattern
    @classmethod
    def create_guest(cls) -> "User":
        return cls("guest@example.com", "Guest User")
 
    @classmethod
    def create_admin(cls, email: str) -> "User":
        admin = cls(email, "Admin")
        # Additional admin setup could go here
        return admin
 
    def get_info(self) -> str:
        return f"{self._name} ({self._email}) - Created: {self._created_at.isoformat()}"
 
 
# Usage
user1 = User("john@example.com", "John Doe")
guest = User.create_guest()
admin = User.create_admin("admin@example.com")
 
print(user1.get_info())
print(guest.get_info())
print(admin.get_info())

Java:

import java.time.Instant;
import java.util.UUID;
 
public class User {
    private String id;
    private String email;
    private String name;
    private Instant createdAt;
 
    // Primary constructor
    public User(String email, String name) {
        this.id = generateId();
        this.email = email;
        this.name = name;
        this.createdAt = Instant.now();
    }
 
    private String generateId() {
        return "user_" + UUID.randomUUID().toString().substring(0, 12);
    }
 
    // Static factory methods - alternative to constructor
    public static User createGuest() {
        return new User("guest@example.com", "Guest User");
    }
 
    public static User createAdmin(String email) {
        User admin = new User(email, "Admin");
        // Additional admin setup could go here
        return admin;
    }
 
    public String getInfo() {
        return name + " (" + email + ") - Created: " + createdAt.toString();
    }
}
 
// Usage
User user1 = new User("john@example.com", "John Doe");
User guest = User.createGuest();
User admin = User.createAdmin("admin@example.com");

Static vs Instance Members

Understanding the difference between static and instance members is crucial for proper class design.

Instance Members

  • Belong to each object instance
  • Each object has its own copy
  • Accessed through object reference

Static Members

  • Belong to the class itself
  • Shared across all instances
  • Accessed through class name

TypeScript:

class Counter {
  // Static property - shared across all instances
  private static totalCount: number = 0;
  private static instances: Counter[] = [];
 
  // Instance property - unique to each object
  private id: number;
  private count: number = 0;
 
  constructor() {
    Counter.totalCount++;
    this.id = Counter.totalCount;
    Counter.instances.push(this);
  }
 
  // Instance method - operates on this object
  increment(): void {
    this.count++;
  }
 
  getCount(): number {
    return this.count;
  }
 
  getId(): number {
    return this.id;
  }
 
  // Static method - operates on the class level
  static getTotalCount(): number {
    return Counter.totalCount;
  }
 
  static getAllInstances(): Counter[] {
    return [...Counter.instances];
  }
 
  static resetAll(): void {
    Counter.instances.forEach(instance => {
      instance.count = 0;
    });
  }
}
 
// Usage
const counter1 = new Counter();
const counter2 = new Counter();
const counter3 = new Counter();
 
counter1.increment();
counter1.increment();
counter2.increment();
 
console.log(counter1.getCount()); // 2 (instance-specific)
console.log(counter2.getCount()); // 1 (instance-specific)
console.log(counter3.getCount()); // 0 (instance-specific)
 
console.log(Counter.getTotalCount()); // 3 (shared across all)
console.log(Counter.getAllInstances().length); // 3
 
Counter.resetAll();
console.log(counter1.getCount()); // 0 (reset)

Python:

class Counter:
    # Class variables - shared across all instances
    _total_count: int = 0
    _instances: list = []
 
    def __init__(self):
        Counter._total_count += 1
        self._id = Counter._total_count
        self._count = 0  # Instance variable
        Counter._instances.append(self)
 
    # Instance methods
    def increment(self) -> None:
        self._count += 1
 
    def get_count(self) -> int:
        return self._count
 
    def get_id(self) -> int:
        return self._id
 
    # Class methods - operate on class level
    @classmethod
    def get_total_count(cls) -> int:
        return cls._total_count
 
    @classmethod
    def get_all_instances(cls) -> list:
        return cls._instances.copy()
 
    @classmethod
    def reset_all(cls) -> None:
        for instance in cls._instances:
            instance._count = 0
 
 
# Usage
counter1 = Counter()
counter2 = Counter()
counter3 = Counter()
 
counter1.increment()
counter1.increment()
counter2.increment()
 
print(counter1.get_count())  # 2
print(counter2.get_count())  # 1
print(counter3.get_count())  # 0
 
print(Counter.get_total_count())  # 3
print(len(Counter.get_all_instances()))  # 3

Java:

import java.util.ArrayList;
import java.util.List;
 
public class Counter {
    // Static fields - shared across all instances
    private static int totalCount = 0;
    private static List<Counter> instances = new ArrayList<>();
 
    // Instance fields - unique to each object
    private int id;
    private int count = 0;
 
    public Counter() {
        totalCount++;
        this.id = totalCount;
        instances.add(this);
    }
 
    // Instance methods
    public void increment() {
        this.count++;
    }
 
    public int getCount() {
        return this.count;
    }
 
    public int getId() {
        return this.id;
    }
 
    // Static methods
    public static int getTotalCount() {
        return totalCount;
    }
 
    public static List<Counter> getAllInstances() {
        return new ArrayList<>(instances);
    }
 
    public static void resetAll() {
        for (Counter instance : instances) {
            instance.count = 0;
        }
    }
}

When to Use Static

Use CaseExample
ConstantsMath.PI, HttpStatus.OK
Utility methodsMath.max(), Arrays.sort()
Factory methodsUser.createGuest()
Counting instancesCounter.getTotalCount()
Shared configurationConfig.getEnvironment()

Abstraction: Hiding Complexity

Abstraction is about hiding complex implementation details and exposing only the essential features. It answers the question: "What does this object do?" without revealing "How does it do it?"

Two Tools for Abstraction

  1. Abstract Classes: Partial implementation with some abstract methods
  2. Interfaces: Pure contracts with no implementation

Abstract Classes

An abstract class is a class that cannot be instantiated directly. It serves as a base for other classes and can contain:

  • Abstract methods (no implementation, must be overridden)
  • Concrete methods (with implementation, inherited)
  • Properties and state

When to Use Abstract Classes

  • You want to share code among closely related classes
  • You expect subclasses to have common methods or fields
  • You need constructors or non-public members

TypeScript:

abstract class Vehicle {
  protected brand: string;
  protected model: string;
  protected year: number;
  protected isRunning: boolean = false;
 
  constructor(brand: string, model: string, year: number) {
    this.brand = brand;
    this.model = model;
    this.year = year;
  }
 
  // Concrete method - shared implementation
  startEngine(): void {
    if (this.isRunning) {
      console.log("Engine is already running");
      return;
    }
    this.isRunning = true;
    console.log(`${this.getDescription()} engine started`);
  }
 
  stopEngine(): void {
    if (!this.isRunning) {
      console.log("Engine is already stopped");
      return;
    }
    this.isRunning = false;
    console.log(`${this.getDescription()} engine stopped`);
  }
 
  getDescription(): string {
    return `${this.year} ${this.brand} ${this.model}`;
  }
 
  // Abstract methods - must be implemented by subclasses
  abstract calculateFuelEfficiency(): number;
  abstract getVehicleType(): string;
}
 
class Car extends Vehicle {
  private fuelTankCapacity: number;
  private milesPerGallon: number;
 
  constructor(
    brand: string,
    model: string,
    year: number,
    fuelTankCapacity: number,
    mpg: number
  ) {
    super(brand, model, year);
    this.fuelTankCapacity = fuelTankCapacity;
    this.milesPerGallon = mpg;
  }
 
  calculateFuelEfficiency(): number {
    return this.milesPerGallon;
  }
 
  getVehicleType(): string {
    return "Car";
  }
 
  // Car-specific method
  getRange(): number {
    return this.fuelTankCapacity * this.milesPerGallon;
  }
}
 
class Motorcycle extends Vehicle {
  private engineCC: number;
 
  constructor(brand: string, model: string, year: number, engineCC: number) {
    super(brand, model, year);
    this.engineCC = engineCC;
  }
 
  calculateFuelEfficiency(): number {
    // Motorcycles typically get better MPG
    return Math.round(50 + (1000 - this.engineCC) / 20);
  }
 
  getVehicleType(): string {
    return "Motorcycle";
  }
 
  // Motorcycle-specific method
  getEngineSize(): string {
    return `${this.engineCC}cc`;
  }
}
 
class Truck extends Vehicle {
  private cargoCapacity: number; // in tons
  private axles: number;
 
  constructor(
    brand: string,
    model: string,
    year: number,
    cargoCapacity: number,
    axles: number
  ) {
    super(brand, model, year);
    this.cargoCapacity = cargoCapacity;
    this.axles = axles;
  }
 
  calculateFuelEfficiency(): number {
    // Trucks have lower MPG, affected by cargo capacity
    return Math.round(20 - this.cargoCapacity * 2);
  }
 
  getVehicleType(): string {
    return "Truck";
  }
 
  // Truck-specific method
  getCargoInfo(): string {
    return `${this.cargoCapacity} tons, ${this.axles} axles`;
  }
}
 
// Usage
function printVehicleInfo(vehicle: Vehicle): void {
  console.log(`Type: ${vehicle.getVehicleType()}`);
  console.log(`Description: ${vehicle.getDescription()}`);
  console.log(`Fuel Efficiency: ${vehicle.calculateFuelEfficiency()} MPG`);
  vehicle.startEngine();
  vehicle.stopEngine();
  console.log("---");
}
 
const car = new Car("Toyota", "Camry", 2023, 15, 32);
const motorcycle = new Motorcycle("Harley-Davidson", "Iron 883", 2022, 883);
const truck = new Truck("Ford", "F-150", 2023, 3, 2);
 
// Polymorphism in action - same function works with different vehicles
printVehicleInfo(car);
printVehicleInfo(motorcycle);
printVehicleInfo(truck);
 
// Vehicle-specific methods
console.log(`Car range: ${car.getRange()} miles`);
console.log(`Motorcycle engine: ${motorcycle.getEngineSize()}`);
console.log(`Truck cargo: ${truck.getCargoInfo()}`);

Python:

from abc import ABC, abstractmethod
 
 
class Vehicle(ABC):
    def __init__(self, brand: str, model: str, year: int):
        self._brand = brand
        self._model = model
        self._year = year
        self._is_running = False
 
    # Concrete method - shared implementation
    def start_engine(self) -> None:
        if self._is_running:
            print("Engine is already running")
            return
        self._is_running = True
        print(f"{self.get_description()} engine started")
 
    def stop_engine(self) -> None:
        if not self._is_running:
            print("Engine is already stopped")
            return
        self._is_running = False
        print(f"{self.get_description()} engine stopped")
 
    def get_description(self) -> str:
        return f"{self._year} {self._brand} {self._model}"
 
    # Abstract methods - must be implemented by subclasses
    @abstractmethod
    def calculate_fuel_efficiency(self) -> float:
        pass
 
    @abstractmethod
    def get_vehicle_type(self) -> str:
        pass
 
 
class Car(Vehicle):
    def __init__(self, brand: str, model: str, year: int,
                 fuel_tank_capacity: float, mpg: float):
        super().__init__(brand, model, year)
        self._fuel_tank_capacity = fuel_tank_capacity
        self._mpg = mpg
 
    def calculate_fuel_efficiency(self) -> float:
        return self._mpg
 
    def get_vehicle_type(self) -> str:
        return "Car"
 
    def get_range(self) -> float:
        return self._fuel_tank_capacity * self._mpg
 
 
class Motorcycle(Vehicle):
    def __init__(self, brand: str, model: str, year: int, engine_cc: int):
        super().__init__(brand, model, year)
        self._engine_cc = engine_cc
 
    def calculate_fuel_efficiency(self) -> float:
        return round(50 + (1000 - self._engine_cc) / 20)
 
    def get_vehicle_type(self) -> str:
        return "Motorcycle"
 
    def get_engine_size(self) -> str:
        return f"{self._engine_cc}cc"
 
 
class Truck(Vehicle):
    def __init__(self, brand: str, model: str, year: int,
                 cargo_capacity: float, axles: int):
        super().__init__(brand, model, year)
        self._cargo_capacity = cargo_capacity
        self._axles = axles
 
    def calculate_fuel_efficiency(self) -> float:
        return round(20 - self._cargo_capacity * 2)
 
    def get_vehicle_type(self) -> str:
        return "Truck"
 
    def get_cargo_info(self) -> str:
        return f"{self._cargo_capacity} tons, {self._axles} axles"
 
 
# Usage
def print_vehicle_info(vehicle: Vehicle) -> None:
    print(f"Type: {vehicle.get_vehicle_type()}")
    print(f"Description: {vehicle.get_description()}")
    print(f"Fuel Efficiency: {vehicle.calculate_fuel_efficiency()} MPG")
    vehicle.start_engine()
    vehicle.stop_engine()
    print("---")
 
 
car = Car("Toyota", "Camry", 2023, 15, 32)
motorcycle = Motorcycle("Harley-Davidson", "Iron 883", 2022, 883)
truck = Truck("Ford", "F-150", 2023, 3, 2)
 
print_vehicle_info(car)
print_vehicle_info(motorcycle)
print_vehicle_info(truck)

Java:

public abstract class Vehicle {
    protected String brand;
    protected String model;
    protected int year;
    protected boolean isRunning = false;
 
    public Vehicle(String brand, String model, int year) {
        this.brand = brand;
        this.model = model;
        this.year = year;
    }
 
    // Concrete method - shared implementation
    public void startEngine() {
        if (isRunning) {
            System.out.println("Engine is already running");
            return;
        }
        isRunning = true;
        System.out.println(getDescription() + " engine started");
    }
 
    public void stopEngine() {
        if (!isRunning) {
            System.out.println("Engine is already stopped");
            return;
        }
        isRunning = false;
        System.out.println(getDescription() + " engine stopped");
    }
 
    public String getDescription() {
        return year + " " + brand + " " + model;
    }
 
    // Abstract methods - must be implemented by subclasses
    public abstract double calculateFuelEfficiency();
    public abstract String getVehicleType();
}
 
public class Car extends Vehicle {
    private double fuelTankCapacity;
    private double mpg;
 
    public Car(String brand, String model, int year,
               double fuelTankCapacity, double mpg) {
        super(brand, model, year);
        this.fuelTankCapacity = fuelTankCapacity;
        this.mpg = mpg;
    }
 
    @Override
    public double calculateFuelEfficiency() {
        return mpg;
    }
 
    @Override
    public String getVehicleType() {
        return "Car";
    }
 
    public double getRange() {
        return fuelTankCapacity * mpg;
    }
}

Interfaces: Pure Contracts

An interface defines a contract that classes must follow. It specifies what methods a class must implement, but not how. Interfaces contain:

  • Method signatures (no implementation)
  • No state (traditionally)
  • No constructors

When to Use Interfaces

  • You want to define a capability that can be added to any class
  • Unrelated classes need to implement the same behavior
  • You want to support multiple inheritance of type

TypeScript:

// Interface - defines a contract
interface ILogger {
  log(message: string): void;
  error(message: string, error?: Error): void;
  warn(message: string): void;
  debug(message: string): void;
}
 
interface IConfigurable {
  configure(options: Record<string, any>): void;
  getConfiguration(): Record<string, any>;
}
 
// A class can implement multiple interfaces
class ConsoleLogger implements ILogger, IConfigurable {
  private config: Record<string, any> = {
    prefix: "[LOG]",
    showTimestamp: true,
  };
 
  configure(options: Record<string, any>): void {
    this.config = { ...this.config, ...options };
  }
 
  getConfiguration(): Record<string, any> {
    return { ...this.config };
  }
 
  private formatMessage(level: string, message: string): string {
    const timestamp = this.config.showTimestamp
      ? `[${new Date().toISOString()}]`
      : "";
    return `${timestamp} ${this.config.prefix} [${level}] ${message}`;
  }
 
  log(message: string): void {
    console.log(this.formatMessage("INFO", message));
  }
 
  error(message: string, error?: Error): void {
    const errorMsg = error ? `${message}: ${error.message}` : message;
    console.error(this.formatMessage("ERROR", errorMsg));
  }
 
  warn(message: string): void {
    console.warn(this.formatMessage("WARN", message));
  }
 
  debug(message: string): void {
    console.debug(this.formatMessage("DEBUG", message));
  }
}
 
class FileLogger implements ILogger, IConfigurable {
  private config: Record<string, any> = {
    filePath: "/var/log/app.log",
    maxSize: 10485760, // 10MB
  };
 
  configure(options: Record<string, any>): void {
    this.config = { ...this.config, ...options };
  }
 
  getConfiguration(): Record<string, any> {
    return { ...this.config };
  }
 
  private writeToFile(level: string, message: string): void {
    const entry = `[${new Date().toISOString()}] [${level}] ${message}\n`;
    // In real implementation: fs.appendFileSync(this.config.filePath, entry);
    console.log(`Writing to ${this.config.filePath}: ${entry}`);
  }
 
  log(message: string): void {
    this.writeToFile("INFO", message);
  }
 
  error(message: string, error?: Error): void {
    const errorMsg = error ? `${message}: ${error.message}` : message;
    this.writeToFile("ERROR", errorMsg);
  }
 
  warn(message: string): void {
    this.writeToFile("WARN", message);
  }
 
  debug(message: string): void {
    this.writeToFile("DEBUG", message);
  }
}
 
// Using interfaces for dependency injection
class UserService {
  private logger: ILogger;
 
  constructor(logger: ILogger) {
    this.logger = logger;
  }
 
  createUser(email: string, name: string): void {
    this.logger.log(`Creating user: ${name}`);
    try {
      // User creation logic
      this.logger.log(`User created: ${email}`);
    } catch (error) {
      this.logger.error("Failed to create user", error as Error);
    }
  }
}
 
// Usage - easily swap implementations
const consoleLogger = new ConsoleLogger();
const fileLogger = new FileLogger();
 
// Configure loggers
consoleLogger.configure({ prefix: "[APP]", showTimestamp: true });
fileLogger.configure({ filePath: "/var/log/users.log" });
 
// Use different loggers for different environments
const devService = new UserService(consoleLogger);
const prodService = new UserService(fileLogger);
 
devService.createUser("dev@example.com", "Dev User");
prodService.createUser("prod@example.com", "Prod User");

Python:

from abc import ABC, abstractmethod
from datetime import datetime
from typing import Any, Optional
 
 
# Interface using ABC
class ILogger(ABC):
    @abstractmethod
    def log(self, message: str) -> None:
        pass
 
    @abstractmethod
    def error(self, message: str, error: Optional[Exception] = None) -> None:
        pass
 
    @abstractmethod
    def warn(self, message: str) -> None:
        pass
 
    @abstractmethod
    def debug(self, message: str) -> None:
        pass
 
 
class IConfigurable(ABC):
    @abstractmethod
    def configure(self, options: dict[str, Any]) -> None:
        pass
 
    @abstractmethod
    def get_configuration(self) -> dict[str, Any]:
        pass
 
 
# Multiple inheritance for multiple interfaces
class ConsoleLogger(ILogger, IConfigurable):
    def __init__(self):
        self._config = {
            "prefix": "[LOG]",
            "show_timestamp": True,
        }
 
    def configure(self, options: dict[str, Any]) -> None:
        self._config.update(options)
 
    def get_configuration(self) -> dict[str, Any]:
        return self._config.copy()
 
    def _format_message(self, level: str, message: str) -> str:
        timestamp = f"[{datetime.now().isoformat()}]" if self._config["show_timestamp"] else ""
        return f"{timestamp} {self._config['prefix']} [{level}] {message}"
 
    def log(self, message: str) -> None:
        print(self._format_message("INFO", message))
 
    def error(self, message: str, error: Optional[Exception] = None) -> None:
        error_msg = f"{message}: {str(error)}" if error else message
        print(self._format_message("ERROR", error_msg))
 
    def warn(self, message: str) -> None:
        print(self._format_message("WARN", message))
 
    def debug(self, message: str) -> None:
        print(self._format_message("DEBUG", message))
 
 
class FileLogger(ILogger, IConfigurable):
    def __init__(self):
        self._config = {
            "file_path": "/var/log/app.log",
            "max_size": 10485760,
        }
 
    def configure(self, options: dict[str, Any]) -> None:
        self._config.update(options)
 
    def get_configuration(self) -> dict[str, Any]:
        return self._config.copy()
 
    def _write_to_file(self, level: str, message: str) -> None:
        entry = f"[{datetime.now().isoformat()}] [{level}] {message}\n"
        print(f"Writing to {self._config['file_path']}: {entry}")
 
    def log(self, message: str) -> None:
        self._write_to_file("INFO", message)
 
    def error(self, message: str, error: Optional[Exception] = None) -> None:
        error_msg = f"{message}: {str(error)}" if error else message
        self._write_to_file("ERROR", error_msg)
 
    def warn(self, message: str) -> None:
        self._write_to_file("WARN", message)
 
    def debug(self, message: str) -> None:
        self._write_to_file("DEBUG", message)
 
 
class UserService:
    def __init__(self, logger: ILogger):
        self._logger = logger
 
    def create_user(self, email: str, name: str) -> None:
        self._logger.log(f"Creating user: {name}")
        try:
            # User creation logic
            self._logger.log(f"User created: {email}")
        except Exception as e:
            self._logger.error("Failed to create user", e)
 
 
# Usage
console_logger = ConsoleLogger()
file_logger = FileLogger()
 
dev_service = UserService(console_logger)
prod_service = UserService(file_logger)

Java:

// Interface definitions
public interface ILogger {
    void log(String message);
    void error(String message, Exception error);
    void warn(String message);
    void debug(String message);
}
 
public interface IConfigurable {
    void configure(Map<String, Object> options);
    Map<String, Object> getConfiguration();
}
 
// Implementation
public class ConsoleLogger implements ILogger, IConfigurable {
    private Map<String, Object> config = new HashMap<>();
 
    public ConsoleLogger() {
        config.put("prefix", "[LOG]");
        config.put("showTimestamp", true);
    }
 
    @Override
    public void configure(Map<String, Object> options) {
        config.putAll(options);
    }
 
    @Override
    public Map<String, Object> getConfiguration() {
        return new HashMap<>(config);
    }
 
    private String formatMessage(String level, String message) {
        String timestamp = (boolean) config.get("showTimestamp")
            ? "[" + Instant.now().toString() + "]"
            : "";
        return timestamp + " " + config.get("prefix") + " [" + level + "] " + message;
    }
 
    @Override
    public void log(String message) {
        System.out.println(formatMessage("INFO", message));
    }
 
    @Override
    public void error(String message, Exception error) {
        String errorMsg = error != null ? message + ": " + error.getMessage() : message;
        System.err.println(formatMessage("ERROR", errorMsg));
    }
 
    @Override
    public void warn(String message) {
        System.out.println(formatMessage("WARN", message));
    }
 
    @Override
    public void debug(String message) {
        System.out.println(formatMessage("DEBUG", message));
    }
}

Abstract Classes vs Interfaces: Comparison

AspectAbstract ClassInterface
ImplementationCan have concrete methodsNo implementation (traditionally)
StateCan have instance fieldsNo state (constants only)
ConstructorsCan have constructorsNo constructors
InheritanceSingle inheritance onlyMultiple implementation
Access ModifiersAny access modifierPublic by default
Relationship"is-a" relationship"can-do" capability

Decision Guide

Use Abstract Class when:

✅ Classes share a common base with shared implementation
✅ You need constructors or protected members
✅ You need non-public methods
✅ You want to provide default behavior
✅ There's a clear "is-a" hierarchy
 
Example: Vehicle → Car, Motorcycle, Truck

Use Interface when:

✅ Multiple unrelated classes need same capability
✅ You want to define a contract/API
✅ You need multiple inheritance
✅ You're defining a service boundary
✅ Different implementations needed (strategy pattern)
 
Example: ILogger → ConsoleLogger, FileLogger, RemoteLogger

Combining Both

Often, the best design combines both:

TypeScript:

// Interface defines the contract
interface IPaymentProcessor {
  processPayment(amount: number): Promise<PaymentResult>;
  refund(transactionId: string): Promise<RefundResult>;
}
 
// Abstract class provides common implementation
abstract class BasePaymentProcessor implements IPaymentProcessor {
  protected apiKey: string;
  protected environment: "sandbox" | "production";
 
  constructor(apiKey: string, environment: "sandbox" | "production") {
    this.apiKey = apiKey;
    this.environment = environment;
  }
 
  // Common logging
  protected log(message: string): void {
    console.log(`[${this.constructor.name}] ${message}`);
  }
 
  // Common validation
  protected validateAmount(amount: number): void {
    if (amount <= 0) {
      throw new Error("Amount must be positive");
    }
  }
 
  // Abstract - each provider implements differently
  abstract processPayment(amount: number): Promise<PaymentResult>;
  abstract refund(transactionId: string): Promise<RefundResult>;
}
 
// Concrete implementations
class StripeProcessor extends BasePaymentProcessor {
  async processPayment(amount: number): Promise<PaymentResult> {
    this.validateAmount(amount);
    this.log(`Processing $${amount} via Stripe`);
    // Stripe-specific logic
    return { success: true, transactionId: `stripe_${Date.now()}` };
  }
 
  async refund(transactionId: string): Promise<RefundResult> {
    this.log(`Refunding ${transactionId} via Stripe`);
    return { success: true };
  }
}
 
class PayPalProcessor extends BasePaymentProcessor {
  async processPayment(amount: number): Promise<PaymentResult> {
    this.validateAmount(amount);
    this.log(`Processing $${amount} via PayPal`);
    // PayPal-specific logic
    return { success: true, transactionId: `paypal_${Date.now()}` };
  }
 
  async refund(transactionId: string): Promise<RefundResult> {
    this.log(`Refunding ${transactionId} via PayPal`);
    return { success: true };
  }
}
 
interface PaymentResult {
  success: boolean;
  transactionId: string;
}
 
interface RefundResult {
  success: boolean;
}
 
// Usage - depends on interface, not implementation
class CheckoutService {
  private processor: IPaymentProcessor;
 
  constructor(processor: IPaymentProcessor) {
    this.processor = processor;
  }
 
  async checkout(amount: number): Promise<void> {
    const result = await this.processor.processPayment(amount);
    console.log(`Checkout result: ${result.transactionId}`);
  }
}

Access Modifiers

Access modifiers control the visibility of class members.

ModifierSame ClassSubclassOther Classes
public
protected
private

TypeScript:

class Employee {
  public name: string;           // Accessible everywhere
  protected department: string;  // Accessible in class and subclasses
  private salary: number;        // Only accessible in this class
  readonly id: string;           // Can't be changed after initialization
 
  constructor(name: string, department: string, salary: number) {
    this.id = `EMP_${Date.now()}`;
    this.name = name;
    this.department = department;
    this.salary = salary;
  }
 
  // Public method
  public getInfo(): string {
    return `${this.name} - ${this.department}`;
  }
 
  // Protected method - usable by subclasses
  protected getSalaryInfo(): string {
    return `Salary: $${this.salary}`;
  }
 
  // Private method - only this class
  private calculateBonus(): number {
    return this.salary * 0.1;
  }
}
 
class Manager extends Employee {
  private teamSize: number;
 
  constructor(name: string, department: string, salary: number, teamSize: number) {
    super(name, department, salary);
    this.teamSize = teamSize;
  }
 
  getManagerInfo(): string {
    // Can access public and protected from parent
    return `${this.name} manages ${this.teamSize} people in ${this.department}`;
    // this.salary - Error: private
    // this.calculateBonus() - Error: private
  }
 
  // Can override and use protected method
  displaySalary(): void {
    console.log(this.getSalaryInfo()); // OK - protected accessible
  }
}

Real-World Example: E-commerce Product System

Let's put it all together with a comprehensive example:

TypeScript:

// Interfaces for capabilities
interface IPriceable {
  getPrice(): number;
  applyDiscount(percentage: number): void;
}
 
interface IShippable {
  getWeight(): number;
  getShippingCost(destination: string): number;
}
 
interface IReviewable {
  addReview(rating: number, comment: string): void;
  getAverageRating(): number;
}
 
// Abstract base class for all products
abstract class Product implements IPriceable, IShippable, IReviewable {
  protected id: string;
  protected name: string;
  protected basePrice: number;
  protected discount: number = 0;
  protected weight: number;
  protected reviews: { rating: number; comment: string }[] = [];
 
  constructor(name: string, basePrice: number, weight: number) {
    this.id = `PROD_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
    this.name = name;
    this.basePrice = basePrice;
    this.weight = weight;
  }
 
  // IPriceable implementation
  getPrice(): number {
    return this.basePrice * (1 - this.discount / 100);
  }
 
  applyDiscount(percentage: number): void {
    if (percentage < 0 || percentage > 100) {
      throw new Error("Discount must be between 0 and 100");
    }
    this.discount = percentage;
  }
 
  // IShippable implementation
  getWeight(): number {
    return this.weight;
  }
 
  getShippingCost(destination: string): number {
    const baseRate = destination === "domestic" ? 5 : 20;
    return baseRate + this.weight * 0.5;
  }
 
  // IReviewable implementation
  addReview(rating: number, comment: string): void {
    if (rating < 1 || rating > 5) {
      throw new Error("Rating must be between 1 and 5");
    }
    this.reviews.push({ rating, comment });
  }
 
  getAverageRating(): number {
    if (this.reviews.length === 0) return 0;
    const sum = this.reviews.reduce((acc, r) => acc + r.rating, 0);
    return sum / this.reviews.length;
  }
 
  // Common methods
  getName(): string {
    return this.name;
  }
 
  getId(): string {
    return this.id;
  }
 
  // Abstract method - each product type displays differently
  abstract getDescription(): string;
  abstract getCategory(): string;
}
 
// Concrete product types
class Electronics extends Product {
  private warranty: number; // months
  private brand: string;
 
  constructor(
    name: string,
    price: number,
    weight: number,
    brand: string,
    warranty: number
  ) {
    super(name, price, weight);
    this.brand = brand;
    this.warranty = warranty;
  }
 
  getDescription(): string {
    return `${this.brand} ${this.name} - ${this.warranty} month warranty`;
  }
 
  getCategory(): string {
    return "Electronics";
  }
 
  getWarrantyInfo(): string {
    return `Covered for ${this.warranty} months`;
  }
}
 
class Clothing extends Product {
  private size: string;
  private material: string;
 
  constructor(
    name: string,
    price: number,
    weight: number,
    size: string,
    material: string
  ) {
    super(name, price, weight);
    this.size = size;
    this.material = material;
  }
 
  getDescription(): string {
    return `${this.name} - Size ${this.size}, ${this.material}`;
  }
 
  getCategory(): string {
    return "Clothing";
  }
 
  getCareInstructions(): string {
    return this.material.includes("cotton")
      ? "Machine wash cold"
      : "Dry clean only";
  }
}
 
class Book extends Product {
  private author: string;
  private isbn: string;
  private pages: number;
 
  constructor(
    name: string,
    price: number,
    weight: number,
    author: string,
    isbn: string,
    pages: number
  ) {
    super(name, price, weight);
    this.author = author;
    this.isbn = isbn;
    this.pages = pages;
  }
 
  getDescription(): string {
    return `"${this.name}" by ${this.author} - ${this.pages} pages`;
  }
 
  getCategory(): string {
    return "Books";
  }
 
  getISBN(): string {
    return this.isbn;
  }
}
 
// Shopping cart uses interfaces for flexibility
class ShoppingCart {
  private items: { product: Product; quantity: number }[] = [];
 
  addItem(product: Product, quantity: number = 1): void {
    const existing = this.items.find((item) => item.product.getId() === product.getId());
    if (existing) {
      existing.quantity += quantity;
    } else {
      this.items.push({ product, quantity });
    }
    console.log(`Added ${quantity}x ${product.getName()} to cart`);
  }
 
  getSubtotal(): number {
    return this.items.reduce(
      (total, item) => total + item.product.getPrice() * item.quantity,
      0
    );
  }
 
  getTotalWeight(): number {
    return this.items.reduce(
      (total, item) => total + item.product.getWeight() * item.quantity,
      0
    );
  }
 
  getShippingCost(destination: string): number {
    return this.items.reduce(
      (total, item) =>
        total + item.product.getShippingCost(destination) * item.quantity,
      0
    );
  }
 
  displayCart(): void {
    console.log("\n=== Shopping Cart ===");
    this.items.forEach((item) => {
      const product = item.product;
      console.log(`${item.quantity}x ${product.getName()}`);
      console.log(`   ${product.getDescription()}`);
      console.log(`   Category: ${product.getCategory()}`);
      console.log(`   Price: $${product.getPrice().toFixed(2)} each`);
      console.log(`   Rating: ${product.getAverageRating().toFixed(1)}/5`);
    });
    console.log(`\nSubtotal: $${this.getSubtotal().toFixed(2)}`);
    console.log(`Shipping (domestic): $${this.getShippingCost("domestic").toFixed(2)}`);
    console.log(`Total: $${(this.getSubtotal() + this.getShippingCost("domestic")).toFixed(2)}`);
  }
}
 
// Usage
const laptop = new Electronics("MacBook Pro", 2499, 2.1, "Apple", 24);
const shirt = new Clothing("Classic Polo", 59.99, 0.3, "M", "100% cotton");
const book = new Book(
  "Clean Code",
  44.99,
  0.5,
  "Robert C. Martin",
  "978-0132350884",
  464
);
 
// Add reviews
laptop.addReview(5, "Amazing performance!");
laptop.addReview(4, "Great but expensive");
shirt.addReview(5, "Perfect fit");
book.addReview(5, "Must read for developers");
 
// Apply discount
laptop.applyDiscount(10);
 
// Create cart and add items
const cart = new ShoppingCart();
cart.addItem(laptop, 1);
cart.addItem(shirt, 2);
cart.addItem(book, 1);
 
// Display cart
cart.displayCart();
 
// Output:
// === Shopping Cart ===
// 1x MacBook Pro
//    Apple MacBook Pro - 24 month warranty
//    Category: Electronics
//    Price: $2249.10 each
//    Rating: 4.5/5
// 2x Classic Polo
//    Classic Polo - Size M, 100% cotton
//    Category: Clothing
//    Price: $59.99 each
//    Rating: 5.0/5
// 1x Clean Code
//    "Clean Code" by Robert C. Martin - 464 pages
//    Category: Books
//    Price: $44.99 each
//    Rating: 5.0/5
//
// Subtotal: $2414.07
// Shipping (domestic): $25.45
// Total: $2439.52

Summary and Key Takeaways

Classes and Objects

ConceptDefinition
ClassBlueprint that defines structure and behavior
ObjectInstance created from a class with its own data
ConstructorSpecial method that initializes an object
StaticMembers that belong to the class, not instances
InstanceMembers that belong to each object

Abstraction Tools

ToolUse When
Abstract ClassShare code among related classes, need constructors/state
InterfaceDefine contracts, enable multiple inheritance, unrelated classes

Best Practices

  1. Keep classes focused: One responsibility per class
  2. Use meaningful names: Classes are nouns, methods are verbs
  3. Prefer composition: Use interfaces for flexibility
  4. Hide implementation: Expose only what's needed
  5. Document public API: Make contracts clear

What's Next?

Continue your OOP journey:

  1. Encapsulation and Information Hiding: Deep dive into data protection
  2. Inheritance and Composition: Choose the right approach
  3. Polymorphism and Interfaces: Flexible design patterns
  4. SOLID Principles: Clean OOP design

Practice Exercises

Exercise 1: Shape Hierarchy

Create an abstract Shape class with concrete Circle, Rectangle, and Triangle implementations. Include:

  • Abstract method calculateArea()
  • Abstract method calculatePerimeter()
  • Concrete method getDescription()

Exercise 2: Payment System

Design a payment system with:

  • Interface IPaymentMethod with pay() and refund()
  • Abstract class BasePayment with common validation
  • Concrete classes: CreditCard, PayPal, BankTransfer

Exercise 3: Media Player

Build a media player with:

  • Interface IPlayable with play(), pause(), stop()
  • Abstract class MediaFile with duration and format
  • Concrete classes: AudioFile, VideoFile, StreamingMedia

Questions or feedback? Feel free to reach out at contact@chanhle.dev or connect with me on X.

Happy coding! 🚀

📬 Subscribe to Newsletter

Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.

We respect your privacy. Unsubscribe at any time.

💬 Comments

Sign in to leave a comment

We'll never post without your permission.