Overview
OOP in C# provides a structured approach to building maintainable, scalable applications. Understanding these concepts deeply allows you to design systems that are easy to extend, test, and maintain. These principles directly influence design patterns, SOLID principles, and architectural decisions.
Core Concepts
Encapsulation
Encapsulation bundles data and methods that operate on that data within a class, restricting direct access to some components.
public class BankAccount
{
// Private field - hidden from outside
private decimal _balance;
// Public property with controlled access
public decimal Balance => _balance;
// Constructor
public BankAccount(decimal initialBalance)
{
if (initialBalance < 0)
throw new ArgumentException("Initial balance cannot be negative");
_balance = initialBalance;
}
// Public method - controlled way to modify state
public void Deposit(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("Deposit amount must be positive");
_balance += amount;
}
public bool Withdraw(decimal amount)
{
if (amount <= 0 || amount > _balance)
return false;
_balance -= amount;
return true;
}
}
// Usage: Cannot directly modify _balance
var account = new BankAccount(100);
account.Deposit(50); // OK: uses public method
// account._balance = 1000; // Error: _balance is private
Inheritance
Inheritance allows a class to inherit members from a base class, promoting code reuse and establishing IS-A relationships.
// Base class
public class Employee
{
public string Name { get; set; }
public decimal BaseSalary { get; set; }
public virtual decimal CalculatePay()
{
return BaseSalary;
}
}
// Derived class - Manager IS-A Employee
public class Manager : Employee
{
public decimal Bonus { get; set; }
// Override base behavior
public override decimal CalculatePay()
{
return BaseSalary + Bonus;
}
}
// Derived class - Developer IS-A Employee
public class Developer : Employee
{
public int OvertimeHours { get; set; }
public decimal OvertimeRate { get; set; }
public override decimal CalculatePay()
{
return BaseSalary + (OvertimeHours * OvertimeRate);
}
}
// Polymorphic behavior
List<Employee> employees = new()
{
new Manager { Name = "Alice", BaseSalary = 5000, Bonus = 1000 },
new Developer { Name = "Bob", BaseSalary = 4000, OvertimeHours = 10, OvertimeRate = 50 }
};
foreach (var emp in employees)
{
// Each employee calculates pay differently
Console.WriteLine($"{emp.Name}: {emp.CalculatePay()}");
}
Polymorphism
Polymorphism allows objects to be treated as instances of their base type while executing derived type behavior.
// Compile-time polymorphism (method overloading)
public class Calculator
{
public int Add(int a, int b) => a + b;
public double Add(double a, double b) => a + b;
public int Add(int a, int b, int c) => a + b + c;
}
// Runtime polymorphism (method overriding)
public abstract class Shape
{
public abstract double CalculateArea();
}
public class Circle : Shape
{
public double Radius { get; set; }
public override double CalculateArea() => Math.PI * Radius * Radius;
}
public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public override double CalculateArea() => Width * Height;
}
// Usage - treating different shapes uniformly
Shape[] shapes = { new Circle { Radius = 5 }, new Rectangle { Width = 4, Height = 6 } };
foreach (var shape in shapes)
{
Console.WriteLine($"Area: {shape.CalculateArea()}");
}
Abstraction
Abstraction focuses on exposing only relevant details while hiding complexity.
// Abstract class - partial implementation
public abstract class PaymentProcessor
{
// Concrete method - shared implementation
public void ProcessPayment(decimal amount)
{
ValidateAmount(amount);
ExecutePayment(amount);
SendConfirmation();
}
private void ValidateAmount(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("Invalid amount");
}
// Abstract method - must be implemented by derived classes
protected abstract void ExecutePayment(decimal amount);
protected virtual void SendConfirmation()
{
Console.WriteLine("Payment processed");
}
}
public class CreditCardProcessor : PaymentProcessor
{
protected override void ExecutePayment(decimal amount)
{
// Credit card specific implementation
Console.WriteLine($"Charging credit card: ${amount}");
}
}
public class PayPalProcessor : PaymentProcessor
{
protected override void ExecutePayment(decimal amount)
{
// PayPal specific implementation
Console.WriteLine($"Processing PayPal: ${amount}");
}
}
Composition vs Inheritance
// Inheritance approach (tight coupling)
public class ElectricCar : Car
{
public void ChargeBattery() { }
}
// Composition approach (loose coupling) - Preferred
public class Car
{
private readonly IEngine _engine;
public Car(IEngine engine)
{
_engine = engine;
}
public void Start() => _engine.Start();
}
public interface IEngine
{
void Start();
}
public class ElectricEngine : IEngine
{
public void Start() => Console.WriteLine("Electric engine starting silently");
}
public class GasEngine : IEngine
{
public void Start() => Console.WriteLine("Gas engine starting with ignition");
}
// Usage - flexible composition
var electricCar = new Car(new ElectricEngine());
var gasCar = new Car(new GasEngine());
Interview Tips
Tip 1: Always explain OOP concepts with real-world examples. Interviewers want to see you can apply theory to practical scenarios.
Tip 2: Know the difference between "IS-A" (inheritance) and "HAS-A" (composition) relationships. Favor composition over inheritance.
Tip 3: Be ready to discuss when NOT to use inheritance - it creates tight coupling and can lead to fragile base class problems.
Common Interview Questions
-
What are the four pillars of OOP?
- Encapsulation (data hiding), Inheritance (code reuse), Polymorphism (many forms), and Abstraction (hiding complexity). Each serves a specific purpose in creating maintainable, extensible code.
-
What's the difference between a class and an object?
- A class is a blueprint/template that defines structure and behavior. An object is a specific instance of a class with its own state. Think: "Dog" is a class, "Buddy" is an object.
-
Explain the difference between method overloading and overriding.
- Overloading: Same method name with different parameters (compile-time polymorphism). Overriding: Replacing base class behavior in derived class using virtual/override (runtime polymorphism).
-
When would you use inheritance vs composition?
- Use inheritance for true IS-A relationships with shared behavior. Use composition (HAS-A) for flexibility and loose coupling. Prefer composition as it's more flexible and testable.
-
What is the difference between abstract class and interface?
- Abstract class: Can have implementation, single inheritance, can have state. Interface: No implementation (until C# 8), multiple inheritance, defines contracts. Use interfaces for capabilities, abstract classes for shared base behavior.
-
Can you instantiate an abstract class?
- No, abstract classes cannot be instantiated directly. They must be inherited by a concrete class that implements all abstract members.
-
What is encapsulation and why is it important?
- Encapsulation hides internal state and requires all interaction through well-defined methods. It protects data integrity, reduces coupling, and makes code easier to maintain and test.