Encapsulation is the bundling of data and the methods that operate on that data, while hiding the internal representation and controlling access to maintain object invariants.
Core Concept
Encapsulation serves two primary purposes:
- Information Hiding: Hide internal implementation details from external code
- Invariant Protection: Ensure objects can only be in valid states
Why Encapsulation Matters
Protecting Invariants
Without encapsulation, external code can bypass validation and create invalid object states:
// BAD: No encapsulation, invariant can be violated
public class BankAccount
{
public decimal Balance; // Public field - danger!
}
var account = new BankAccount { Balance = 1000 };
account.Balance = -500; // VIOLATION: Negative balance allowed!// GOOD: Encapsulation protects invariant
public class BankAccount
{
private decimal _balance; // Hidden representation
// INVARIANT: _balance >= 0
public BankAccount(decimal initialDeposit)
{
if (initialDeposit < 0)
throw new ArgumentException("Initial deposit must be non-negative");
_balance = initialDeposit;
}
public void Withdraw(decimal amount)
{
if (_balance - amount < 0)
throw new InvalidOperationException("Insufficient funds");
_balance -= amount; // Invariant maintained
}
public decimal GetBalance() => _balance; // Read-only access
}Enabling Evolution
Encapsulation allows internal representation changes without breaking clients:
// Version 1: Store temperature in Celsius
public class Temperature
{
private double _celsius;
public double Fahrenheit => (_celsius * 9/5) + 32;
public double Celsius => _celsius;
}
// Version 2: Changed to store in Kelvin (clients unaffected!)
public class Temperature
{
private double _kelvin; // Internal representation changed
public double Fahrenheit => ((_kelvin - 273.15) * 9/5) + 32;
public double Celsius => _kelvin - 273.15;
}Mechanisms of Encapsulation
1. Access Modifiers
Control visibility of class members:
public class Example
{
private int _internal; // Only accessible within this class
protected int _inherited; // Accessible in this class and subclasses
public int External; // Accessible everywhere
public int GetInternal() => _internal; // Controlled access
}2. Properties and Methods
Provide controlled access to internal state:
public class Person
{
private string _name;
private int _age;
// Property with validation
public string Name
{
get => _name;
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("Name cannot be empty");
_name = value;
}
}
// Read-only property (computed)
public bool IsAdult => _age >= 18;
}3. Immutability
Make objects unchangeable after construction:
public class Money
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency)
{
if (amount < 0)
throw new ArgumentException("Amount cannot be negative");
Amount = amount;
Currency = currency ?? throw new ArgumentNullException(nameof(currency));
}
// Operations return new instances
public Money Add(Money other)
{
if (Currency != other.Currency)
throw new InvalidOperationException("Currency mismatch");
return new Money(Amount + other.Amount, Currency);
}
}Common Violations
Getters and Setters Without Purpose
Don’t create getters/setters for every field:
// BAD: Pointless encapsulation
public class Person
{
private string _name;
public string GetName() => _name;
public void SetName(string value) => _name = value;
}
// This is just a public field with extra steps!Better approaches:
// GOOD: Just use a public field if no invariant
public class PersonData
{
public string Name { get; set; }
}
// GOOD: Real encapsulation with invariant
public class ValidatedPerson
{
private string _name;
// INVARIANT: Name is never null or empty
public ValidatedPerson(string name)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("Name required");
_name = name;
}
public string Name => _name; // Read-only access
}Exposing Collections
Don’t expose mutable collections directly:
// BAD: Internal list can be modified
public class Team
{
public List<Player> Players { get; set; }
}
var team = new Team();
team.Players = null; // Can break invariants!
team.Players.Add(invalidPlayer); // No validation!// GOOD: Controlled access to collection
public class Team
{
private readonly List<Player> _players = new();
public IReadOnlyCollection<Player> Players => _players.AsReadOnly();
public void AddPlayer(Player player)
{
if (player == null)
throw new ArgumentNullException(nameof(player));
if (_players.Count >= 11)
throw new InvalidOperationException("Team is full");
_players.Add(player);
}
}Exposing Internal Objects
Don’t return mutable internal objects:
// BAD: Exposes internal state
public class Document
{
private Metadata _metadata = new();
public Metadata GetMetadata() => _metadata; // Danger!
}
var doc = new Document();
doc.GetMetadata().Author = null; // Violated encapsulation!// GOOD: Return defensive copy or immutable view
public class Document
{
private Metadata _metadata = new();
public Metadata GetMetadata() => _metadata.Clone();
// Or: Return immutable interface
public IReadOnlyMetadata GetMetadata() => _metadata;
}Relationship to Other Concepts
Tell, Don’t Ask
Tell, Don’t Ask is the behavioral principle that supports encapsulation:
// BAD: Asking for data to make decisions
if (account.GetBalance() >= amount)
account.SetBalance(account.GetBalance() - amount);
// GOOD: Tell the object what to do
account.Withdraw(amount); // Object maintains its own invariantLaw of Demeter
Law of Demeter prevents breaking encapsulation through chaining:
// BAD: Reaching through multiple objects
customer.GetAddress().GetZipCode();
// GOOD: Ask the object directly
customer.GetZipCode();Single Responsibility
Encapsulation naturally leads to Single Responsibility:
- Each class protects its own invariant
- This invariant defines the class’s single reason to change
When to Relax Encapsulation
Data Transfer Objects (DTOs)
Simple data containers with no invariants:
public class UserDTO
{
public string Username { get; set; }
public string Email { get; set; }
public DateTime CreatedAt { get; set; }
}Value Objects
Immutable objects with simple validation:
public record Email(string Address)
{
public Email(string address) : this(Validate(address)) { }
private static string Validate(string email)
{
if (!email.Contains("@"))
throw new ArgumentException("Invalid email");
return email;
}
}Resources
- Accessors Are Evil - Why getters/setters aren’t always encapsulation
- The C++ Style Sweet Spot - Stroustrup on encapsulation and invariants
See Also
- Invariants - What encapsulation protects
- Tell, Don’t Ask - Behavioral principle for encapsulation
- Law of Demeter - Structural principle for encapsulation
- Object-Oriented Design
- Design Concepts
- Good Software Practices