OOP Fundamentals
Core OOP Pillars
Object-Oriented Programming is built on four fundamental pillars that work together to create maintainable, scalable software systems.
Abstraction
Definition: Hide complex implementation details while exposing a simple, clear interface.
Key Principles of Abstraction
- Focus on what an object does rather than how it does it
- Define contracts through interfaces and abstract classes
- Reduce complexity by hiding unnecessary details
Example:
// Abstract interface - defines what, not how
public interface IPaymentProcessor
{
Task<PaymentResult> ProcessPayment(decimal amount);
Task<bool> RefundPayment(string transactionId);
}
// Concrete implementation - defines how
public class StripePaymentProcessor : IPaymentProcessor
{
private readonly string apiKey;
public StripePaymentProcessor(string apiKey)
{
this.apiKey = apiKey;
}
public async Task<PaymentResult> ProcessPayment(decimal amount)
{
// Complex Stripe API logic hidden from caller
var charge = await CreateStripeCharge(amount, apiKey);
return new PaymentResult { Success = true, TransactionId = charge.Id };
}
public async Task<bool> RefundPayment(string transactionId)
{
// Complex refund logic abstracted away
await ProcessStripeRefund(transactionId);
return true;
}
private async Task<StripeCharge> CreateStripeCharge(decimal amount, string key) { /* ... */ }
private async Task ProcessStripeRefund(string id) { /* ... */ }
}
// Consumer doesn't need to know implementation details
public class OrderService
{
private readonly IPaymentProcessor paymentProcessor;
public OrderService(IPaymentProcessor paymentProcessor)
{
this.paymentProcessor = paymentProcessor;
}
public async Task CompleteOrder(Order order)
{
// Simple interface - complexity is abstracted
var result = await paymentProcessor.ProcessPayment(order.Total);
if (result.Success)
{
order.MarkAsPaid(result.TransactionId);
}
}
}
Encapsulation
Definition: Bundle data and methods that operate on that data within a single unit, controlling access through visibility modifiers.
Encapsulation bundles data and methods that operate on that data within a single unit, controlling access through visibility modifiers.
Key Principles of Encapsulation
- Keep internal state private
- Expose behavior through public methods
- Use properties for controlled access to data
- Protect object invariants
Example:
public class BankAccount
{
// Private fields - internal state is hidden
private decimal balance;
private readonly List<Transaction> transactions = new();
// Public property with controlled access
public string AccountNumber { get; }
public decimal Balance => balance; // Read-only access
public BankAccount(string accountNumber, decimal initialBalance)
{
if (initialBalance < 0)
throw new ArgumentException("Initial balance cannot be negative");
AccountNumber = accountNumber;
balance = initialBalance;
}
// Public methods control how state can be modified
public void Deposit(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("Deposit amount must be positive");
balance += amount;
transactions.Add(new Transaction(TransactionType.Deposit, amount));
}
public bool Withdraw(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("Withdrawal amount must be positive");
if (amount > balance)
return false; // Insufficient funds
balance -= amount;
transactions.Add(new Transaction(TransactionType.Withdrawal, amount));
return true;
}
// Encapsulated business logic
public IReadOnlyList<Transaction> GetRecentTransactions(int count)
{
return transactions.TakeLast(count).ToList().AsReadOnly();
}
}
Inheritance
Definition: Create new classes based on existing classes, establishing βis-aβ relationships and enabling code reuse.
Key Principles of Inheritance
- Use inheritance for true "is-a" relationships
- Prefer composition over inheritance when possible
- Follow the Liskov Substitution Principle
- Avoid deep inheritance hierarchies (3+ levels)
Example:
// Base class with common functionality
public abstract class Employee
{
public string Name { get; set; }
public string EmployeeId { get; set; }
public decimal BaseSalary { get; set; }
// Virtual method can be overridden
public virtual decimal CalculateMonthlyPay()
{
return BaseSalary / 12;
}
// Abstract method must be implemented
public abstract string GetEmployeeType();
public void DisplayInfo()
{
Console.WriteLine($"{GetEmployeeType()}: {Name} ({EmployeeId})");
Console.WriteLine($"Monthly Pay: ${CalculateMonthlyPay():F2}");
}
}
// Derived class - SalariedEmployee "is-an" Employee
public class SalariedEmployee : Employee
{
public override string GetEmployeeType() => "Salaried Employee";
}
// Derived class with additional behavior
public class HourlyEmployee : Employee
{
public decimal HourlyRate { get; set; }
public int HoursWorked { get; set; }
// Override to change behavior
public override decimal CalculateMonthlyPay()
{
return HourlyRate * HoursWorked;
}
public override string GetEmployeeType() => "Hourly Employee";
}
// Derived class with bonus structure
public class CommissionEmployee : Employee
{
public decimal CommissionRate { get; set; }
public decimal Sales { get; set; }
public override decimal CalculateMonthlyPay()
{
return base.CalculateMonthlyPay() + (Sales * CommissionRate);
}
public override string GetEmployeeType() => "Commission Employee";
}
Modern Alternative: Composition Over Inheritance
// Interface defines capability
public interface IPayCalculator
{
decimal CalculatePay();
}
// Different calculation strategies
public class SalaryCalculator : IPayCalculator
{
private readonly decimal annualSalary;
public SalaryCalculator(decimal annualSalary)
{
this.annualSalary = annualSalary;
}
public decimal CalculatePay() => annualSalary / 12;
}
public class HourlyCalculator : IPayCalculator
{
private readonly decimal hourlyRate;
private readonly int hoursWorked;
public HourlyCalculator(decimal hourlyRate, int hoursWorked)
{
this.hourlyRate = hourlyRate;
this.hoursWorked = hoursWorked;
}
public decimal CalculatePay() => hourlyRate * hoursWorked;
}
// Employee uses composition instead of inheritance
public class ModernEmployee
{
public string Name { get; set; }
public string EmployeeId { get; set; }
// Composition - has-a relationship
private readonly IPayCalculator payCalculator;
public ModernEmployee(string name, string employeeId, IPayCalculator payCalculator)
{
Name = name;
EmployeeId = employeeId;
this.payCalculator = payCalculator;
}
public decimal GetMonthlyPay() => payCalculator.CalculatePay();
}
Polymorphism
Definition: Objects of different types can be treated as instances of the same type, with behavior determined at runtime.
Polymorphism allows objects of different types to be treated as instances of the same type, with behavior determined at runtime.
Key Principles of Polymorphism
- Enable flexible, extensible code
- Support method overriding (runtime polymorphism)
- Support method overloading (compile-time polymorphism)
- Work with abstractions, not concrete types
Runtime Polymorphism Example:
public interface IShape
{
double CalculateArea();
void Draw();
}
public class Circle : IShape
{
public double Radius { get; set; }
public double CalculateArea() => Math.PI * Radius * Radius;
public void Draw() => Console.WriteLine($"Drawing circle with radius {Radius}");
}
public class Rectangle : IShape
{
public double Width { get; set; }
public double Height { get; set; }
public double CalculateArea() => Width * Height;
public void Draw() => Console.WriteLine($"Drawing rectangle {Width}x{Height}");
}
public class Triangle : IShape
{
public double Base { get; set; }
public double Height { get; set; }
public double CalculateArea() => 0.5 * Base * Height;
public void Draw() => Console.WriteLine($"Drawing triangle with base {Base} and height {Height}");
}
// Polymorphic usage - treat all shapes uniformly
public class ShapeProcessor
{
public void ProcessShapes(IEnumerable<IShape> shapes)
{
foreach (var shape in shapes)
{
// Polymorphism - actual method called depends on runtime type
shape.Draw();
Console.WriteLine($"Area: {shape.CalculateArea():F2}\n");
}
}
public double CalculateTotalArea(IEnumerable<IShape> shapes)
{
return shapes.Sum(s => s.CalculateArea());
}
}
// Usage
var shapes = new List<IShape>
{
new Circle { Radius = 5 },
new Rectangle { Width = 10, Height = 20 },
new Triangle { Base = 8, Height = 6 }
};
var processor = new ShapeProcessor();
processor.ProcessShapes(shapes);
Console.WriteLine($"Total area: {processor.CalculateTotalArea(shapes):F2}");
Advanced OOP Concepts
Covariance
Definition: Use a more derived type than originally specified (out parameters, return types).
// Covariance with IEnumerable<out T>
IEnumerable<string> strings = new List<string> { "hello", "world" };
IEnumerable<object> objects = strings; // Valid - string is derived from object
// Covariance with delegates
Func<string> funcString = () => "Hello";
Func<object> funcObject = funcString; // Valid - returns more specific type
Contravariance
Definition: Use a more generic type than originally specified (in parameters).
// Contravariance with Action<in T>
Action<object> actionObject = (obj) => Console.WriteLine(obj);
Action<string> actionString = actionObject; // Valid - can accept more general parameter
// Contravariance with comparison
IComparer<object> objectComparer = Comparer<object>.Default;
IComparer<string> stringComparer = objectComparer; // Valid with contravariant interface
Invariance
Definition: Use only the originally specified type (mutable collections).
// Invariance with List<T>
List<string> strings = new List<string>();
// List<object> objects = strings; // ERROR - List is invariant
// Why? Because we could add any object to the list, breaking type safety
// objects.Add(new Employee()); // Would add Employee to List<string>!
Quick Reference
When to Use Each Pillar
| Pillar | Use When | Avoid When |
|---|---|---|
| Abstraction | Hiding complex implementation, defining contracts | Simple, straightforward code |
| Encapsulation | Protecting internal state, enforcing invariants | Data transfer objects (DTOs) |
| Inheritance | True βis-aβ relationships, minimal hierarchy | Code reuse alone (use composition) |
| Polymorphism | Working with families of related types | Single concrete implementation |
Common Anti-Patterns
Anemic Domain Model:
// BAD - Data without behavior
public class Order
{
public int Id { get; set; }
public decimal Total { get; set; }
public List<OrderItem> Items { get; set; }
}
// GOOD - Encapsulated behavior
public class Order
{
private readonly List<OrderItem> items = new();
public int Id { get; }
public decimal Total => items.Sum(i => i.Subtotal);
public IReadOnlyList<OrderItem> Items => items.AsReadOnly();
public void AddItem(OrderItem item)
{
if (item == null)
throw new ArgumentNullException(nameof(item));
items.Add(item);
}
}
Inappropriate Intimacy:
// BAD - Classes too tightly coupled
public class Customer
{
public List<Order> Orders { get; set; }
}
public class OrderProcessor
{
public void Process(Customer customer)
{
// Direct manipulation of internal collections
customer.Orders.RemoveAll(o => o.IsCancelled);
}
}
// GOOD - Proper encapsulation
public class Customer
{
private readonly List<Order> orders = new();
public void RemoveCancelledOrders()
{
orders.RemoveAll(o => o.IsCancelled);
}
}
Found this guide helpful? Share it with your team:
Share on LinkedIn