Explain the four pillars of OOP with real-world examples.
Quick Answer
The four pillars are Encapsulation (bundling data and methods, hiding internals), Abstraction (hiding complexity, showing essentials), Inheritance (reusing code from parent classes), and Polymorphism (same interface, different implementations). Each provides specific benefits: data protection, reduced complexity, code reusability, and flexibility respectively.
Detailed Answer
The four pillars of Object-Oriented Programming are: Encapsulation, Abstraction, Inheritance, and Polymorphism.
1. Encapsulation:
Bundling data (fields) and methods that operate on that data within a single unit (class), and restricting direct access to some components.
Real-world analogy: A car's engine - you don't need to know how the engine works internally, you just use the gas pedal and steering wheel (public interface).
public class BankAccount
{
// Private fields - hidden from outside
private decimal balance;
private string accountNumber;
private List<Transaction> transactions;
// Public constructor
public BankAccount(string accountNumber, decimal initialBalance)
{
this.accountNumber = accountNumber;
this.balance = initialBalance;
transactions = new List<Transaction>();
}
// Public property with validation
public decimal Balance
{
get { return balance; }
private set // Can only be set internally
{
if (value < 0)
throw new InvalidOperationException("Balance cannot be negative");
balance = value;
}
}
// Public methods - controlled interface
public void Deposit(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("Amount must be positive");
Balance += amount;
AddTransaction("Deposit", amount);
}
public bool Withdraw(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("Amount must be positive");
if (balance >= amount)
{
Balance -= amount;
AddTransaction("Withdrawal", -amount);
return true;
}
return false;
}
// Private helper method
private void AddTransaction(string type, decimal amount)
{
transactions.Add(new Transaction
{
Date = DateTime.Now,
Type = type,
Amount = amount
});
}
}
// Usage - clean public interface, implementation hidden
BankAccount account = new BankAccount("123456", 1000);
account.Deposit(500); // OK
account.Withdraw(200); // OK
// account.balance = -1000; // ERROR - can't access private field
// account.Balance = -1000; // ERROR - setter is private
Benefits:
- Data protection and validation
- Flexibility to change implementation
- Controlled access
- Maintainability
2. Abstraction:
Hiding complex implementation details and showing only essential features. Focusing on "what" rather than "how".
Real-world analogy: Using a smartphone - you tap icons to make calls, send messages, but don't need to understand the underlying cellular technology.
// Abstract class - defines contract
public abstract class PaymentProcessor
{
// Abstract methods - must be implemented
public abstract bool ProcessPayment(decimal amount);
public abstract string GetPaymentMethod();
// Concrete method - shared implementation
public void LogTransaction(decimal amount)
{
Console.WriteLine($"{GetPaymentMethod()}: ${amount} at {DateTime.Now}");
}
// Template method pattern
public bool MakePayment(decimal amount)
{
if (ValidateAmount(amount))
{
bool success = ProcessPayment(amount);
if (success)
{
LogTransaction(amount);
}
return success;
}
return false;
}
private bool ValidateAmount(decimal amount)
{
return amount > 0;
}
}
// Concrete implementations
public class CreditCardProcessor : PaymentProcessor
{
private string cardNumber;
public CreditCardProcessor(string cardNumber)
{
this.cardNumber = cardNumber;
}
public override bool ProcessPayment(decimal amount)
{
// Credit card specific logic
Console.WriteLine($"Processing credit card payment: {cardNumber}");
return true; // Simplified
}
public override string GetPaymentMethod()
{
return "Credit Card";
}
}
public class PayPalProcessor : PaymentProcessor
{
private string email;
public PayPalProcessor(string email)
{
this.email = email;
}
public override bool ProcessPayment(decimal amount)
{
// PayPal specific logic
Console.WriteLine($"Processing PayPal payment: {email}");
return true; // Simplified
}
public override string GetPaymentMethod()
{
return "PayPal";
}
}
// Usage - work with abstraction, not concrete types
public class CheckoutService
{
public void Checkout(PaymentProcessor processor, decimal amount)
{
// Don't care about implementation details
if (processor.MakePayment(amount))
{
Console.WriteLine("Payment successful!");
}
}
}
// Client code
CheckoutService checkout = new CheckoutService();
checkout.Checkout(new CreditCardProcessor("1234-5678"), 99.99m);
checkout.Checkout(new PayPalProcessor("user@email.com"), 49.99m);
Benefits:
- Reduces complexity
- Easier to understand and use
- Changes in implementation don't affect users
- Promotes code reusability
3. Inheritance:
Mechanism where a new class derives properties and behaviors from an existing class, establishing a parent-child relationship.
Real-world analogy: Biological inheritance - children inherit characteristics from parents (eye color, height), but can have their own unique traits.
// Base class
public class Vehicle
{
public string Make { get; set; }
public string Model { get; set; }
public int Year { get; set; }
protected bool isEngineRunning;
public Vehicle(string make, string model, int year)
{
Make = make;
Model = model;
Year = year;
}
public virtual void Start()
{
isEngineRunning = true;
Console.WriteLine($"{Make} {Model} engine started");
}
public virtual void Stop()
{
isEngineRunning = false;
Console.WriteLine($"{Make} {Model} engine stopped");
}
public void DisplayInfo()
{
Console.WriteLine($"{Year} {Make} {Model}");
}
}
// Derived class - inherits from Vehicle
public class Car : Vehicle
{
public int NumberOfDoors { get; set; }
public string TransmissionType { get; set; }
public Car(string make, string model, int year, int doors, string transmission)
: base(make, model, year) // Call parent constructor
{
NumberOfDoors = doors;
TransmissionType = transmission;
}
// Override parent method
public override void Start()
{
Console.WriteLine("Checking car systems...");
base.Start(); // Call parent implementation
Console.WriteLine("Car is ready to drive");
}
// New method specific to Car
public void OpenTrunk()
{
Console.WriteLine("Trunk opened");
}
}
// Another derived class
public class Motorcycle : Vehicle
{
public bool HasSidecar { get; set; }
public Motorcycle(string make, string model, int year, bool hasSidecar)
: base(make, model, year)
{
HasSidecar = hasSidecar;
}
public override void Start()
{
Console.WriteLine("Kick-starting motorcycle...");
base.Start();
}
// New method specific to Motorcycle
public void Wheelie()
{
Console.WriteLine("Performing wheelie!");
}
}
// Derived class from Car
public class ElectricCar : Car
{
public int BatteryCapacity { get; set; }
public ElectricCar(string make, string model, int year, int doors, int batteryCapacity)
: base(make, model, year, doors, "Automatic")
{
BatteryCapacity = batteryCapacity;
}
public override void Start()
{
Console.WriteLine("Electric car booting up...");
isEngineRunning = true; // Can access protected member
Console.WriteLine("Ready to drive - silent mode");
}
public void Charge()
{
Console.WriteLine("Charging battery...");
}
}
// Usage
Vehicle vehicle1 = new Car("Toyota", "Camry", 2023, 4, "Automatic");
Vehicle vehicle2 = new Motorcycle("Harley", "Street 750", 2023, false);
Vehicle vehicle3 = new ElectricCar("Tesla", "Model 3", 2023, 4, 75);
vehicle1.Start(); // Calls Car.Start()
vehicle2.Start(); // Calls Motorcycle.Start()
vehicle3.Start(); // Calls ElectricCar.Start()
Benefits:
- Code reusability (DRY principle)
- Hierarchical classification
- Extensibility
- Polymorphic behavior
4. Polymorphism:
Ability of objects to take multiple forms. Same interface, different implementations.
Real-world analogy: A person can be a student, employee, athlete simultaneously - same person, different roles/behaviors in different contexts.
Types of Polymorphism:
a) Compile-time Polymorphism (Method Overloading):
public class Calculator
{
// Same method name, different parameters
public int Add(int a, int b)
{
return a + b;
}
public double Add(double a, double b)
{
return a + b;
}
public int Add(int a, int b, int c)
{
return a + b + c;
}
public string Add(string a, string b)
{
return a + b;
}
}
// Usage - compiler decides which method to call
Calculator calc = new Calculator();
int result1 = calc.Add(5, 3); // Calls Add(int, int)
double result2 = calc.Add(5.5, 3.2); // Calls Add(double, double)
int result3 = calc.Add(5, 3, 2); // Calls Add(int, int, int)
string result4 = calc.Add("Hello", "World"); // Calls Add(string, string)
b) Runtime Polymorphism (Method Overriding):
// Base class
public abstract class Shape
{
public abstract double CalculateArea();
public abstract double CalculatePerimeter();
public virtual void Display()
{
Console.WriteLine($"Area: {CalculateArea()}");
Console.WriteLine($"Perimeter: {CalculatePerimeter()}");
}
}
// Derived classes with different implementations
public class Circle : Shape
{
public double Radius { get; set; }
public Circle(double radius)
{
Radius = radius;
}
public override double CalculateArea()
{
return Math.PI * Radius * Radius;
}
public override double CalculatePerimeter()
{
return 2 * Math.PI * Radius;
}
public override void Display()
{
Console.WriteLine($"Circle with radius {Radius}");
base.Display();
}
}
public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public Rectangle(double width, double height)
{
Width = width;
Height = height;
}
public override double CalculateArea()
{
return Width * Height;
}
public override double CalculatePerimeter()
{
return 2 * (Width + Height);
}
public override void Display()
{
Console.WriteLine($"Rectangle {Width}x{Height}");
base.Display();
}
}
public class Triangle : Shape
{
public double Base { get; set; }
public double Height { get; set; }
public double SideA { get; set; }
public double SideB { get; set; }
public double SideC { get; set; }
public Triangle(double baseLength, double height, double sideA, double sideB, double sideC)
{
Base = baseLength;
Height = height;
SideA = sideA;
SideB = sideB;
SideC = sideC;
}
public override double CalculateArea()
{
return (Base * Height) / 2;
}
public override double CalculatePerimeter()
{
return SideA + SideB + SideC;
}
}
// Polymorphic behavior - same interface, different implementations
public class ShapeProcessor
{
public void ProcessShapes(List<Shape> shapes)
{
double totalArea = 0;
foreach (Shape shape in shapes)
{
// Polymorphism in action!
// Calls appropriate CalculateArea() based on actual type
totalArea += shape.CalculateArea();
shape.Display();
Console.WriteLine("---");
}
Console.WriteLine($"Total area of all shapes: {totalArea:F2}");
}
}
// Usage
List<Shape> shapes = new List<Shape>
{
new Circle(5),
new Rectangle(4, 6),
new Triangle(3, 4, 3, 4, 5)
};
ShapeProcessor processor = new ShapeProcessor();
processor.ProcessShapes(shapes); // Each shape behaves differently!
c) Interface Polymorphism:
public interface INotificationSender
{
void Send(string message, string recipient);
}
public class EmailSender : INotificationSender
{
public void Send(string message, string recipient)
{
Console.WriteLine($"Sending email to {recipient}: {message}");
// Email sending logic
}
}
public class SmsSender : INotificationSender
{
public void Send(string message, string recipient)
{
Console.WriteLine($"Sending SMS to {recipient}: {message}");
// SMS sending logic
}
}
public class PushNotificationSender : INotificationSender
{
public void Send(string message, string recipient)
{
Console.WriteLine($"Sending push notification to {recipient}: {message}");
// Push notification logic
}
}
// Notification service using polymorphism
public class NotificationService
{
private readonly List<INotificationSender> senders;
public NotificationService()
{
senders = new List<INotificationSender>
{
new EmailSender(),
new SmsSender(),
new PushNotificationSender()
};
}
public void NotifyAll(string message, string recipient)
{
// Polymorphism - each sender implements Send() differently
foreach (var sender in senders)
{
sender.Send(message, recipient);
}
}
}
// Usage
NotificationService service = new NotificationService();
service.NotifyAll("Your order has shipped!", "user@example.com");
Benefits:
- Flexibility and extensibility
- Code reusability
- Easier maintenance
- Loose coupling
- Supports Open/Closed Principle
Summary of Four Pillars:
| Pillar | What it does | Key benefit |
|---|---|---|
| Encapsulation | Bundles data and methods, hides internals | Data protection, controlled access |
| Abstraction | Hides complexity, shows only essentials | Simplicity, reduces complexity |
| Inheritance | Reuses code from parent classes | Code reusability, hierarchy |
| Polymorphism | Same interface, different implementations | Flexibility, extensibility |
Real-world complete example combining all four:
// Encapsulation & Abstraction
public abstract class Employee
{
// Encapsulation - private fields
private string name;
private decimal baseSalary;
// Public properties
public string Name
{
get => name;
set => name = value ?? throw new ArgumentNullException(nameof(value));
}
protected decimal BaseSalary
{
get => baseSalary;
set => baseSalary = value > 0 ? value : throw new ArgumentException("Salary must be positive");
}
// Abstraction - abstract method
public abstract decimal CalculateSalary();
public virtual void DisplayInfo()
{
Console.WriteLine($"Employee: {Name}");
Console.WriteLine($"Salary: ${CalculateSalary():F2}");
}
}
// Inheritance
public class FullTimeEmployee : Employee
{
public decimal Bonus { get; set; }
public FullTimeEmployee(string name, decimal baseSalary, decimal bonus)
{
Name = name;
BaseSalary = baseSalary;
Bonus = bonus;
}
// Polymorphism - override
public override decimal CalculateSalary()
{
return BaseSalary + Bonus;
}
}
public class ContractEmployee : Employee
{
public int HoursWorked { get; set; }
public decimal HourlyRate { get; set; }
public ContractEmployee(string name, int hours, decimal rate)
{
Name = name;
HoursWorked = hours;
HourlyRate = rate;
BaseSalary = 0;
}
// Polymorphism - different implementation
public override decimal CalculateSalary()
{
return HoursWorked * HourlyRate;
}
public override void DisplayInfo()
{
base.DisplayInfo();
Console.WriteLine($"Hours: {HoursWorked}, Rate: ${HourlyRate}/hr");
}
}
// Polymorphic usage
List<Employee> employees = new List<Employee>
{
new FullTimeEmployee("John Doe", 5000, 1000),
new ContractEmployee("Jane Smith", 160, 50)
};
foreach (Employee emp in employees)
{
emp.DisplayInfo(); // Polymorphic call
Console.WriteLine("---");
}