🏗️

Encapsulation

Object-Oriented Programming Intermediate 2 min read 300 words

Protect internal state and enforce access through controlled interfaces

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 interfaces
  • protected - When derived classes need access
  • internal - For assembly-internal implementations
  • private 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:

  1. Protect internal state with private fields
  2. Expose controlled access through properties and methods
  3. Validate all inputs before modifying state
  4. Return immutable views of collections
  5. 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

📚 Related Articles