Introduction
Object-Oriented Programming (OOP) is a programming paradigm widely used in C# and .NET development. Instead of organizing an application only around functions and procedures, OOP organizes code around objects that combine data and behavior.
For beginners, the four fundamental OOP concepts are:
Encapsulation
Inheritance
Polymorphism
Abstraction
These concepts are easier to understand when they are connected to a practical application rather than treated as separate definitions.
In this article, we will use a simple banking application to understand each concept with C# examples and see how the concepts work together.
What Is Object-Oriented Programming?
Object-Oriented Programming focuses on objects that contain both state and behavior.
For example, a bank account can have:
State
Account number
Account holder
Balance
Behavior
Deposit
Withdraw
Check balance
In C#, a class can represent the structure and behavior of such an object.
public class BankAccount
{
public string AccountNumber { get; set; }
public string AccountHolder { get; set; }
public decimal Balance { get; set; }
public void Deposit(decimal amount)
{
Balance += amount;
}
}
An object can then be created from the class:
BankAccount account = new BankAccount
{
AccountNumber = "ACC1001",
AccountHolder = "Rakesh",
Balance = 5000
};
account.Deposit(1000);
Console.WriteLine(account.Balance);
Output:
6000
The class defines the structure and behavior, while the object represents an actual instance.
Why Use OOP?
OOP can help developers organize larger applications by separating responsibilities into meaningful classes and relationships.
Common benefits include:
Encapsulation: Controls access to an object's internal state.
Reusability: Allows common behavior to be reused through appropriate class relationships and composition.
Maintainability: Keeps related data and behavior together.
Extensibility: Allows applications to support new implementations without unnecessarily changing existing code.
Abstraction: Hides implementation details that callers do not need to know.
However, OOP is not simply about creating as many classes as possible. Good object-oriented design also requires choosing appropriate responsibilities and relationships between objects.
The Four Pillars of OOP
The four commonly taught pillars are:
Encapsulation
Inheritance
Polymorphism
Abstraction
Let's examine each one using C#.
Encapsulation
Encapsulation means controlling how an object's internal state is accessed and modified.
Instead of allowing every part of an application to change a bank account's balance directly, the account can expose operations such as Deposit() and Withdraw().
Real-World Analogy: ATM
When using an ATM, you do not directly manipulate the bank's internal account records.
Instead, you interact with operations such as:
Withdraw money
Deposit money
Check balance
The banking system controls what happens internally.
The same idea can be applied to a C# class.
C# Example
Instead of exposing the balance as a freely writable property:
public decimal Balance { get; set; }
we can protect it:
public class BankAccount
{
private decimal _balance;
public decimal GetBalance()
{
return _balance;
}
public void Deposit(decimal amount)
{
if (amount <= 0)
throw new ArgumentException(
"Deposit amount must be greater than zero.");
_balance += amount;
}
public void Withdraw(decimal amount)
{
if (amount <= 0)
throw new ArgumentException(
"Withdrawal amount must be greater than zero.");
if (amount > _balance)
throw new InvalidOperationException(
"Insufficient balance.");
_balance -= amount;
}
}
Now external code cannot directly modify _balance.
Instead:
BankAccount account = new BankAccount();
account.Deposit(5000);
account.Withdraw(1500);
Console.WriteLine(account.GetBalance());
Output:
3500
The class controls how the balance changes.
Why Encapsulation Matters
Without encapsulation, another part of the application could potentially do something like:
account.Balance = -50000;
That could violate the business rules of the banking system.
By keeping the field private and exposing controlled operations, the class becomes responsible for protecting its own state.
Inheritance
Inheritance allows a class to derive from another class and reuse accessible members from the base class.
For example, different types of bank accounts may share common behavior.
We can create a base class:
public class BankAccount
{
public string AccountNumber { get; set; } = string.Empty;
public decimal Balance { get; protected set; }
public void Deposit(decimal amount)
{
if (amount <= 0)
throw new ArgumentException(
"Amount must be greater than zero.");
Balance += amount;
}
}
A savings account can inherit from it:
public class SavingsAccount : BankAccount
{
public decimal InterestRate { get; set; }
}
A current account can also inherit from it:
public class CurrentAccount : BankAccount
{
public decimal OverdraftLimit { get; set; }
}
Now both derived classes can use the common Deposit() behavior.
SavingsAccount savingsAccount = new SavingsAccount();
savingsAccount.Deposit(5000);
Console.WriteLine(savingsAccount.Balance);
Output:
5000
Real-World Analogy
Think about different types of vehicles.
A car and a bike are both vehicles. They may share common behavior such as starting and stopping while having their own specialized behavior.
The same relationship can be represented in C#:
Vehicle
/ \
/ \
Car Bike
Important Design Consideration
Inheritance should represent a genuine is-a relationship.
For example:
SavingsAccount is a BankAccount
makes sense.
But:
BankAccount is a Database
does not represent an appropriate inheritance relationship. Composition would be more appropriate in such a case.
Polymorphism
Polymorphism means that the same interface or operation can have different implementations.
In C#, polymorphism commonly appears through:
Method overloading
Method overriding
Interface-based programming
Compile-Time Polymorphism: Method Overloading
Method overloading allows multiple methods to have the same name but different parameter lists.
public class PaymentService
{
public void ProcessPayment(decimal amount)
{
Console.WriteLine(
$"Processing payment of {amount}");
}
public void ProcessPayment(
decimal amount,
string currency)
{
Console.WriteLine(
$"Processing {amount} {currency}");
}
}
The compiler determines which method should be called based on the arguments.
PaymentService service = new PaymentService();
service.ProcessPayment(1000);
service.ProcessPayment(1000, "USD");
Output:
Processing payment of 1000
Processing 1000 USD
Runtime Polymorphism: Method Overriding
Runtime polymorphism allows a derived class to provide its own implementation of a base-class method.
Consider different account types calculating interest differently.
public class BankAccount
{
public decimal Balance { get; set; }
public virtual decimal CalculateInterest()
{
return 0;
}
}
A savings account can override the method:
public class SavingsAccount : BankAccount
{
public override decimal CalculateInterest()
{
return Balance * 0.04m;
}
}
A premium account can provide another implementation:
public class PremiumAccount : BankAccount
{
public override decimal CalculateInterest()
{
return Balance * 0.06m;
}
}
Now the same method call can produce different results:
Join the conversation! Your thoughts help the community grow.