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
- Basic understanding of programming concepts
- Completed The Four Pillars of OOP
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._mileageJava:
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
| Aspect | Class | Object |
|---|---|---|
| Definition | Blueprint/template | Instance of a class |
| Existence | Exists at compile time | Exists at runtime |
| Memory | No memory allocated | Memory allocated for data |
| Creation | Defined once | Created multiple times |
| Analogy | Cookie cutter | Cookie |
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())) # 3Java:
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 Case | Example |
|---|---|
| Constants | Math.PI, HttpStatus.OK |
| Utility methods | Math.max(), Arrays.sort() |
| Factory methods | User.createGuest() |
| Counting instances | Counter.getTotalCount() |
| Shared configuration | Config.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
- Abstract Classes: Partial implementation with some abstract methods
- 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
| Aspect | Abstract Class | Interface |
|---|---|---|
| Implementation | Can have concrete methods | No implementation (traditionally) |
| State | Can have instance fields | No state (constants only) |
| Constructors | Can have constructors | No constructors |
| Inheritance | Single inheritance only | Multiple implementation |
| Access Modifiers | Any access modifier | Public 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, TruckUse 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, RemoteLoggerCombining 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.
| Modifier | Same Class | Subclass | Other 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.52Summary and Key Takeaways
Classes and Objects
| Concept | Definition |
|---|---|
| Class | Blueprint that defines structure and behavior |
| Object | Instance created from a class with its own data |
| Constructor | Special method that initializes an object |
| Static | Members that belong to the class, not instances |
| Instance | Members that belong to each object |
Abstraction Tools
| Tool | Use When |
|---|---|
| Abstract Class | Share code among related classes, need constructors/state |
| Interface | Define contracts, enable multiple inheritance, unrelated classes |
Best Practices
- Keep classes focused: One responsibility per class
- Use meaningful names: Classes are nouns, methods are verbs
- Prefer composition: Use interfaces for flexibility
- Hide implementation: Expose only what's needed
- Document public API: Make contracts clear
What's Next?
Continue your OOP journey:
- Encapsulation and Information Hiding: Deep dive into data protection
- Inheritance and Composition: Choose the right approach
- Polymorphism and Interfaces: Flexible design patterns
- 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
IPaymentMethodwithpay()andrefund() - Abstract class
BasePaymentwith common validation - Concrete classes:
CreditCard,PayPal,BankTransfer
Exercise 3: Media Player
Build a media player with:
- Interface
IPlayablewithplay(),pause(),stop() - Abstract class
MediaFilewithdurationandformat - 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.