C# Encapsulation
Introduction
Encapsulation is one of the four fundamental principles of object-oriented programming (OOP), alongside inheritance, polymorphism, and abstraction. In C#, encapsulation refers to the bundling of data (fields) and methods that operate on that data into a single unit (class), while restricting direct access to some of the object's components.
Think of encapsulation as a protective wrapper that prevents code outside the wrapper from directly accessing or modifying the data inside. This concept is critical for:
- Data hiding - Protecting internal implementation details
- Controlled access - Providing regulated ways to access and modify data
- Code maintainability - Allowing implementation changes without affecting other parts of the program
- Reducing complexity - Simplifying the interface by which objects interact
Basic Encapsulation in C#
Access Modifiers
C# implements encapsulation through access modifiers, which control the visibility and accessibility of classes, methods, properties, and fields:
Access Modifier | Description |
---|---|
public | Accessible from anywhere |
private | Accessible only within the containing class |
protected | Accessible within the containing class and derived classes |
internal | Accessible within the same assembly |
protected internal | Accessible within the same assembly or derived classes |
private protected | Accessible only within containing class or derived classes in the same assembly |
Example: Poor Encapsulation
Here's an example of a class with poor encapsulation:
public class BankAccount
{
public string AccountNumber;
public decimal Balance;
public void Deposit(decimal amount)
{
Balance += amount;
}
public void Withdraw(decimal amount)
{
Balance -= amount; // No validation!
}
}
Usage:
BankAccount account = new BankAccount();
account.AccountNumber = "123456789";
account.Balance = 1000000; // Directly changing the balance!
account.Withdraw(2000000); // Can withdraw more than the balance!
Console.WriteLine($"Balance: {account.Balance}"); // Balance: -1000000 (Negative balance!)
The problem here is that anyone can directly modify the Balance
field, bypassing business rules and validation.
Example: Good Encapsulation
Here's a better implementation using encapsulation:
public class BankAccount
{
private string _accountNumber;
private decimal _balance;
public string AccountNumber
{
get { return _accountNumber; }
private set { _accountNumber = value; } // Can only be set within the class
}
public decimal Balance
{
get { return _balance; }
private set { _balance = value; } // Can only be set within the class
}
public BankAccount(string accountNumber, decimal initialDeposit)
{
if (string.IsNullOrEmpty(accountNumber))
throw new ArgumentException("Account number cannot be empty");
if (initialDeposit < 0)
throw new ArgumentException("Initial deposit cannot be negative");
_accountNumber = accountNumber;
_balance = initialDeposit;
}
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)
throw new ArgumentException("Withdrawal amount must be positive");
if (amount > _balance)
return false; // Insufficient funds
_balance -= amount;
return true;
}
}
Usage:
try
{
BankAccount account = new BankAccount("123456789", 1000);
// account.Balance = 1000000; // This would cause a compile-time error
account.Deposit(500);
Console.WriteLine($"Balance after deposit: {account.Balance}"); // Balance: 1500
bool withdrawalSuccess = account.Withdraw(2000);
if (withdrawalSuccess)
Console.WriteLine("Withdrawal successful");
else
Console.WriteLine("Insufficient funds"); // This will be printed
Console.WriteLine($"Final balance: {account.Balance}"); // Balance: 1500 (Unchanged)
}
catch (ArgumentException ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
Properties in C#
Properties are a special type of class member that provide a flexible mechanism to read, write, or compute the value of a private field. They act as public getters and setters for private fields.
Auto-implemented Properties
C# provides a shorthand notation for creating properties:
public class Person
{
// Auto-implemented property
public string Name { get; set; }
// Auto-implemented property with private setter
public int Age { get; private set; }
// Read-only auto-implemented property
public string Id { get; } = Guid.NewGuid().ToString();
public Person(string name, int age)
{
Name = name;
Age = age;
}
public void HaveBirthday()
{
Age++; // We can modify Age here because we're inside the class
}
}
Usage:
Person person = new Person("John", 30);
Console.WriteLine($"{person.Name} is {person.Age} years old, ID: {person.Id}");
person.Name = "John Smith"; // OK - public getter and setter
// person.Age = 31; // Compile error - cannot access private setter
// person.Id = "NewId"; // Compile error - no setter available
person.HaveBirthday();
Console.WriteLine($"{person.Name} is now {person.Age} years old");
// Output:
// John is 30 years old, ID: 3f2504e0-4f89-41d3-9a0c-0305e82c3301
// John Smith is now 31 years old
Full Property Implementation
When you need more control over property access, you can implement full properties:
public class Temperature
{
private double _celsius;
public double Celsius
{
get { return _celsius; }
set { _celsius = value; }
}
public double Fahrenheit
{
get { return _celsius * 9 / 5 + 32; }
set { _celsius = (value - 32) * 5 / 9; }
}
}
Usage:
Temperature temp = new Temperature();
temp.Celsius = 25;
Console.WriteLine($"{temp.Celsius}°C is {temp.Fahrenheit}°F"); // 25°C is 77°F
temp.Fahrenheit = 68;
Console.WriteLine($"{temp.Fahrenheit}°F is {temp.Celsius}°C"); // 68°F is 20°C
Real-World Applications
Example: E-commerce Product Class
Here's a more complex example demonstrating encapsulation in an e-commerce scenario:
public class Product
{
// Private fields
private string _name;
private decimal _price;
private int _stockQuantity;
private List<string> _categories = new List<string>();
// Public properties with appropriate validation
public string Id { get; } = Guid.NewGuid().ToString();
public string Name
{
get => _name;
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("Product name cannot be empty");
_name = value;
}
}
public decimal Price
{
get => _price;
set
{
if (value < 0)
throw new ArgumentException("Price cannot be negative");
_price = value;
}
}
// Read-only property calculated from a private field
public bool InStock => _stockQuantity > 0;
// Read-only access to categories as IEnumerable prevents direct modification
public IEnumerable<string> Categories => _categories.AsReadOnly();
// Constructor ensures required fields are initialized
public Product(string name, decimal price, int initialStock = 0)
{
Name = name; // Uses the property setter with validation
Price = price; // Uses the property setter with validation
_stockQuantity = Math.Max(0, initialStock); // Ensure non-negative
}
// Methods to manipulate private data
public void AddToStock(int quantity)
{
if (quantity <= 0)
throw new ArgumentException("Quantity must be positive");
_stockQuantity += quantity;
}
public bool RemoveFromStock(int quantity)
{
if (quantity <= 0)
throw new ArgumentException("Quantity must be positive");
if (quantity > _stockQuantity)
return false;
_stockQuantity -= quantity;
return true;
}
public void AddCategory(string category)
{
if (string.IsNullOrWhiteSpace(category))
throw new ArgumentException("Category cannot be empty");
if (!_categories.Contains(category))
_categories.Add(category);
}
public bool RemoveCategory(string category)
{
return _categories.Remove(category);
}
// A method that exposes some internal state without revealing implementation details
public string GetInventoryStatus()
{
return _stockQuantity > 10 ? "Well Stocked" :
_stockQuantity > 0 ? "Low Stock" : "Out of Stock";
}
}
Usage:
try
{
// Create a new product
Product laptop = new Product("Laptop XPS", 1299.99m, 5);
// Add categories
laptop.AddCategory("Electronics");
laptop.AddCategory("Computers");
// Display product information
Console.WriteLine($"Product: {laptop.Name} (ID: {laptop.Id})");
Console.WriteLine($"Price: ${laptop.Price}");
Console.WriteLine($"In stock: {laptop.InStock}");
Console.WriteLine($"Inventory status: {laptop.GetInventoryStatus()}");
// Show categories
Console.WriteLine("Categories:");
foreach (string category in laptop.Categories)
{
Console.WriteLine($"- {category}");
}
// Try to add more stock
laptop.AddToStock(10);
Console.WriteLine($"Updated inventory status: {laptop.GetInventoryStatus()}");
// Process an order
bool orderProcessed = laptop.RemoveFromStock(3);
Console.WriteLine(orderProcessed
? "Order processed successfully."
: "Not enough stock to process order.");
Console.WriteLine($"Final inventory status: {laptop.GetInventoryStatus()}");
}
catch (ArgumentException ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
Expected output:
Product: Laptop XPS (ID: 7b2f4a8d-1e40-4bf5-9a2c-db85df42e8f1)
Price: $1299.99
In stock: True
Inventory status: Low Stock
Categories:
- Electronics
- Computers
Updated inventory status: Well Stocked
Order processed successfully.
Final inventory status: Well Stocked
Benefits of Encapsulation
- Data Protection: Fields can be made read-only or write-only as needed
- Flexibility: Implementation details can change without affecting the public interface
- Validation: Input can be validated before changing an object's state
- Debugging: Easier to debug as data access is centralized through properties/methods
- Maintainability: Code is better organized and easier to maintain
Common Encapsulation Patterns
Immutable Objects
When an object's state cannot be changed after creation:
public class Point
{
// Read-only properties - can only be set during initialization
public double X { get; }
public double Y { get; }
public Point(double x, double y)
{
X = x;
Y = y;
}
// Instead of modifying this object, create a new one
public Point Translate(double deltaX, double deltaY)
{
return new Point(X + deltaX, Y + deltaY);
}
}
Lazy Initialization
Delaying the creation of an object until it's needed:
public class DocumentProcessor
{
private string _filePath;
private string? _fileContent;
public DocumentProcessor(string filePath)
{
_filePath = filePath;
}
public string FileContent
{
get
{
// Lazy initialization - only load content when accessed
if (_fileContent == null)
{
try
{
_fileContent = File.ReadAllText(_filePath);
}
catch (Exception ex)
{
_fileContent = $"Error loading file: {ex.Message}";
}
}
return _fileContent;
}
}
}
Summary
Encapsulation is a powerful concept in C# and object-oriented programming that enables you to:
- Hide implementation details while exposing a clean, well-defined interface
- Control access to data through properties and methods
- Validate data before it changes an object's state
- Update internal implementation without breaking code that uses your class
- Build more secure, maintainable, and flexible applications
By using access modifiers, properties, and thoughtful method design, you can create classes that are both powerful and safe to use, preventing unintended side effects and making your code more robust.
Exercises
-
Create a
Student
class with encapsulated properties for name, age, and grades. Include methods to add a grade and calculate the average grade. -
Implement a
BankAccount
class with proper encapsulation that supports deposits, withdrawals, and transfers between accounts. Ensure that an account balance cannot go below zero. -
Design a
ShoppingCart
class that encapsulates a collection of products. Include methods to add and remove products, and calculate the total cost. -
Create an immutable
Address
class with properties for street, city, state, and ZIP code. Include a method that returns a newAddress
object with updated values. -
Implement a
Logger
class that encapsulates file I/O operations for logging messages. Use lazy initialization for the file stream.
Additional Resources
- Microsoft Documentation on Properties
- C# Access Modifiers
- Object-Oriented Programming in C#
- Book: "C# in Depth" by Jon Skeet
- Book: "Clean Code" by Robert C. Martin
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)