Encapsulation
Encapsulation is the practice of bundling data with the methods that operate on it and restricting direct access to some of the object’s components. It protects the internal state of an object from unintended interference.
Why Encapsulation Matters
Without encapsulation, any code can modify an object’s state, leading to:
- Invalid states (e.g., negative bank balance)
- Race conditions in multi-threaded environments
- Impossible-to-trace bugs
- Tightly coupled code that’s hard to change
The Problem: Unprotected State
// BAD: Public fields allow invalid state
public class BankAccount
{
public decimal Balance; // Anyone can modify!
public string AccountNumber; // No validation!
}
// Disaster waiting to happen:
var account = new BankAccount();
account.Balance = -1000000; // Set negative balance directly!
account.AccountNumber = ""; // Empty account number!
The Solution: Proper Encapsulation
public class BankAccount
{
// Private fields - internal state is protected
private decimal _balance;
private readonly object _lockObject = new();
private readonly List<Transaction> _transactions = new();
// Public property with private setter - controlled access
public decimal Balance => _balance;
// Read-only property - set only in constructor
public string AccountNumber { get; }
// Immutable transaction history
public IReadOnlyList<Transaction> Transactions => _transactions.AsReadOnly();
// Constructor validates initial state
public BankAccount(string accountNumber, decimal initialBalance)
{
if (string.IsNullOrWhiteSpace(accountNumber))
throw new ArgumentException("Account number is required", nameof(accountNumber));
if (initialBalance < 0)
throw new ArgumentException("Initial balance cannot be negative", nameof(initialBalance));
AccountNumber = accountNumber;
_balance = initialBalance;
}
// Public method with validation and thread safety
public bool Withdraw(decimal amount)
{
lock (_lockObject) // Thread-safe operation
{
if (amount <= 0)
throw new ArgumentException("Amount must be positive", nameof(amount));
if (amount > _balance)
return false; // Insufficient funds
_balance -= amount;
_transactions.Add(new Transaction
{
Type = TransactionType.Withdrawal,
Amount = amount,
Date = DateTime.UtcNow
});
return true;
}
}
public void Deposit(decimal amount)
{
lock (_lockObject)
{
if (amount <= 0)
throw new ArgumentException("Amount must be positive", nameof(amount));
_balance += amount;
_transactions.Add(new Transaction
{
Type = TransactionType.Deposit,
Amount = amount,
Date = DateTime.UtcNow
});
}
}
}
Benefits of Encapsulation
| Benefit | Description |
|---|---|
| Data Validation | Ensures values are valid before assignment |
| State Protection | Prevents external code from corrupting state |
| Thread Safety | Enables controlled concurrent access |
| Audit Trail | Can log or track all state changes |
| Flexibility | Internal implementation can change without affecting consumers |
Access Modifiers in C#
public class Example
{
public int PublicField; // Accessible from anywhere
private int _privateField; // Only this class
protected int ProtectedField; // This class + derived classes
internal int InternalField; // Same assembly
protected internal int ProtInternal; // Same assembly OR derived classes
private protected int PrivProt; // Same assembly AND derived classes
}
When to Use Each
private- Default for fields. Always start here.public- For APIs and interfacesprotected- When derived classes need accessinternal- For assembly-internal implementationsprivate protected- Rarely used, very restrictive
Properties: The Encapsulation Tool
public class User
{
// Auto-property with private setter
public string Name { get; private set; }
// Computed property (no backing field)
public bool IsActive => LastLogin > DateTime.UtcNow.AddDays(-30);
// Property with validation
private int _age;
public int Age
{
get => _age;
set
{
if (value < 0 || value > 150)
throw new ArgumentOutOfRangeException(nameof(value));
_age = value;
}
}
// Init-only property (C# 9+)
public string Email { get; init; }
public DateTime LastLogin { get; private set; }
// Method to modify state in a controlled way
public void RecordLogin()
{
LastLogin = DateTime.UtcNow;
}
}
// Usage
var user = new User { Email = "user@example.com" };
// user.Email = "new@email.com"; // Error: init-only property
user.RecordLogin(); // Controlled state change
Immutability: Ultimate Encapsulation
For maximum protection, make objects immutable:
// Immutable class using records (C# 9+)
public record Person(string Name, int Age);
// Traditional immutable class
public sealed class ImmutablePerson
{
public string Name { get; }
public int Age { get; }
public ImmutablePerson(string name, int age)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
Age = age >= 0 ? age : throw new ArgumentOutOfRangeException(nameof(age));
}
// Create modified copy instead of mutating
public ImmutablePerson WithAge(int newAge)
{
return new ImmutablePerson(Name, newAge);
}
}
Encapsulating Collections
public class Team
{
// BAD: Exposes modifiable list
public List<Player> Players { get; set; } = new();
// GOOD: Return read-only view
private readonly List<Player> _players = new();
public IReadOnlyList<Player> Players => _players.AsReadOnly();
// Controlled modification
public void AddPlayer(Player player)
{
if (_players.Count >= 25)
throw new InvalidOperationException("Team roster is full");
_players.Add(player);
}
public bool RemovePlayer(Player player)
{
return _players.Remove(player);
}
}
Interview Tips
Common Questions:
- “What is encapsulation and why is it important?”
- “How do you achieve encapsulation in C#?”
- “What’s the difference between encapsulation and abstraction?”
Key Points:
- Protect internal state with private fields
- Expose controlled access through properties and methods
- Validate all inputs before modifying state
- Return immutable views of collections
- Use thread safety when needed
Encapsulation vs Abstraction:
- Abstraction: Hides complexity (what vs how)
- Encapsulation: Hides data (protects state)
- They work together but solve different problems