Object-Oriented Programming (OOP) is one of the most influential paradigms in software development. It provides a clear structure for programs, making code more modular, reusable, and easier to maintain. In this comprehensive guide, we'll explore the four pillars of OOP with practical examples in multiple programming languages.
What is Object-Oriented Programming?
OOP is a programming paradigm that organizes software design around objects rather than functions and logic. An object is a data structure that contains both data (attributes/properties) and code (methods/functions) that operate on that data.
# A simple class definition in Python
class Dog:
def __init__(self, name, breed):
self.name = name # attribute
self.breed = breed # attribute
def bark(self): # method
return f"{self.name} says Woof!"
# Creating an object (instance)
my_dog = Dog("Buddy", "Golden Retriever")
print(my_dog.bark()) # Output: Buddy says Woof!
The Four Pillars of OOP
1. Encapsulation
Encapsulation is the bundling of data and the methods that operate on that data within a single unit (class). It restricts direct access to some of an object's components, which is a means of preventing accidental interference and misuse.
class BankAccount {
private balance: number;
private accountNumber: string;
constructor(accountNumber: string, initialBalance: number) {
this.accountNumber = accountNumber;
this.balance = initialBalance;
}
// Public method to deposit money
public deposit(amount: number): void {
if (amount > 0) {
this.balance += amount;
console.log(`Deposited $${amount}. New balance: $${this.balance}`);
}
}
// Public method to withdraw money
public withdraw(amount: number): boolean {
if (amount > 0 && amount <= this.balance) {
this.balance -= amount;
console.log(`Withdrew $${amount}. New balance: $${this.balance}`);
return true;
}
console.log("Insufficient funds or invalid amount");
return false;
}
// Getter for balance (controlled access)
public getBalance(): number {
return this.balance;
}
}
const account = new BankAccount("123456", 1000);
account.deposit(500); // Deposited $500. New balance: $1500
account.withdraw(200); // Withdrew $200. New balance: $1300
// account.balance = 1000000; // Error! Cannot access private property
Benefits of Encapsulation:
- Protects internal state from external modification
- Reduces complexity by hiding implementation details
- Makes code more maintainable and flexible
2. Inheritance
Inheritance allows a class to inherit properties and methods from another class. The class that inherits is called the child class (or subclass), and the class being inherited from is called the parent class (or superclass).
// Parent class
public class Vehicle {
protected String brand;
protected int year;
public Vehicle(String brand, int year) {
this.brand = brand;
this.year = year;
}
public void start() {
System.out.println("Vehicle is starting...");
}
public void displayInfo() {
System.out.println("Brand: " + brand + ", Year: " + year);
}
}
// Child class inheriting from Vehicle
public class Car extends Vehicle {
private int numberOfDoors;
public Car(String brand, int year, int numberOfDoors) {
super(brand, year); // Call parent constructor
this.numberOfDoors = numberOfDoors;
}
// Override parent method
@Override
public void start() {
System.out.println("Car engine is starting with a roar!");
}
// New method specific to Car
public void honk() {
System.out.println("Beep beep!");
}
}
// Another child class
public class Motorcycle extends Vehicle {
private boolean hasSidecar;
public Motorcycle(String brand, int year, boolean hasSidecar) {
super(brand, year);
this.hasSidecar = hasSidecar;
}
@Override
public void start() {
System.out.println("Motorcycle engine revs up!");
}
}
Types of Inheritance:
- Single Inheritance: A class inherits from one parent class
- Multiple Inheritance: A class inherits from multiple parent classes (supported in Python, not directly in Java/C#)
- Multilevel Inheritance: A class inherits from a child class
- Hierarchical Inheritance: Multiple classes inherit from one parent class
3. Polymorphism
Polymorphism means "many forms." It allows objects of different classes to be treated as objects of a common parent class. There are two types: compile-time (method overloading) and runtime (method overriding) polymorphism.
from abc import ABC, abstractmethod
# Abstract base class
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14159 * self.radius ** 2
def perimeter(self):
return 2 * 3.14159 * self.radius
class Triangle(Shape):
def __init__(self, a, b, c):
self.a = a
self.b = b
self.c = c
def area(self):
# Heron's formula
s = (self.a + self.b + self.c) / 2
return (s * (s-self.a) * (s-self.b) * (s-self.c)) ** 0.5
def perimeter(self):
return self.a + self.b + self.c
# Polymorphism in action
def print_shape_info(shape: Shape):
print(f"Area: {shape.area():.2f}")
print(f"Perimeter: {shape.perimeter():.2f}")
print("---")
# Same function works with different shape types
shapes = [
Rectangle(5, 10),
Circle(7),
Triangle(3, 4, 5)
]
for shape in shapes:
print_shape_info(shape)
Output:
Area: 50.00
Perimeter: 30.00
---
Area: 153.94
Perimeter: 43.98
---
Area: 6.00
Perimeter: 12.00
---
4. Abstraction
Abstraction is the concept of hiding complex implementation details and showing only the necessary features of an object. It helps reduce programming complexity and effort.
// Abstract class - cannot be instantiated directly
abstract class Database {
protected connectionString: string;
constructor(connectionString: string) {
this.connectionString = connectionString;
}
// Abstract methods - must be implemented by child classes
abstract connect(): Promise<boolean>;
abstract disconnect(): Promise<void>;
abstract query(sql: string): Promise<any[]>;
// Concrete method - shared implementation
protected log(message: string): void {
console.log(`[${new Date().toISOString()}] ${message}`);
}
}
class MySQLDatabase extends Database {
private connection: any = null;
async connect(): Promise<boolean> {
this.log("Connecting to MySQL database...");
// MySQL-specific connection logic
this.connection = { active: true };
this.log("Connected to MySQL successfully");
return true;
}
async disconnect(): Promise<void> {
this.log("Disconnecting from MySQL...");
this.connection = null;
}
async query(sql: string): Promise<any[]> {
this.log(`Executing MySQL query: ${sql}`);
// MySQL-specific query execution
return [{ id: 1, name: "Sample" }];
}
}
class MongoDatabase extends Database {
private client: any = null;
async connect(): Promise<boolean> {
this.log("Connecting to MongoDB...");
// MongoDB-specific connection logic
this.client = { connected: true };
this.log("Connected to MongoDB successfully");
return true;
}
async disconnect(): Promise<void> {
this.log("Disconnecting from MongoDB...");
this.client = null;
}
async query(sql: string): Promise<any[]> {
this.log(`Executing MongoDB query: ${sql}`);
// MongoDB-specific query execution
return [{ _id: "abc123", data: "Sample" }];
}
}
// Usage - the caller doesn't need to know implementation details
async function fetchData(db: Database) {
await db.connect();
const results = await db.query("SELECT * FROM users");
console.log("Results:", results);
await db.disconnect();
}
SOLID Principles
OOP is often accompanied by SOLID principles for better software design:
| Principle | Description | |-----------|-------------| | Single Responsibility | A class should have only one reason to change | | Open/Closed | Open for extension, closed for modification | | Liskov Substitution | Objects should be replaceable with their subtypes | | Interface Segregation | Many specific interfaces are better than one general interface | | Dependency Inversion | Depend on abstractions, not concretions |
Best Practices
- Keep classes focused - Each class should have a single responsibility
- Favor composition over inheritance - Use "has-a" relationships when appropriate
- Program to interfaces - Depend on abstractions rather than concrete implementations
- Use meaningful names - Class and method names should clearly describe their purpose
- Keep methods small - Each method should do one thing well
Conclusion
Object-Oriented Programming provides a powerful way to structure code that mirrors real-world entities and relationships. By mastering the four pillars—Encapsulation, Inheritance, Polymorphism, and Abstraction—you'll write code that is more modular, reusable, and maintainable. Whether you're building a small application or a large enterprise system, OOP principles will serve as a solid foundation for your software architecture.