.NET Encapsulation
Introduction
Encapsulation is one of the four fundamental principles of object-oriented programming (OOP), alongside inheritance, polymorphism, and abstraction. In .NET and C#, encapsulation refers to the bundling of data (fields) and methods that operate on that data into a single unit (class) and restricting access to some of the object's components.
Encapsulation serves several important purposes:
- Data hiding - It protects the internal state of an object by preventing direct access to its fields
- Control over data - It enables validation of data before it's modified
- Flexibility - Implementation details can be changed without affecting code that uses the class
- Reduced complexity - It simplifies the interface of a class by exposing only necessary functionality
In this article, we'll explore how to implement encapsulation in C# and .NET, and understand its practical applications.
Understanding Encapsulation in C#
Access Modifiers
The primary mechanism for implementing encapsulation in C# is through access modifiers. These keywords control the visibility and accessibility of classes, methods, properties, and fields. Here are the main access modifiers in C#:
Access Modifier | Description |
---|---|
public | Accessible from any code |
private | Accessible only within the containing class or struct |
protected | Accessible within the containing class or derived classes |
internal | Accessible within the same assembly |
protected internal | Accessible within the same assembly or derived classes |
private protected | Accessible within the containing class or derived classes within the same assembly |
Implementing Encapsulation
Example 1: Basic Encapsulation
Let's consider a simple Person
class without proper encapsulation:
public class Person
{
// Fields are directly accessible
public string Name;
public int Age;
}
Usage:
Person person = new Person();
person.Name = "John";
person.Age = -30; // Invalid age, but no validation
This approach allows anyone to modify the fields directly, potentially leading to invalid data (like a negative age).
Now, let's improve this class with encapsulation:
public class Person
{
// Private fields
private string _name;
private int _age;
// Public properties with validation
public string Name
{
get { return _name; }
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("Name cannot be empty");
_name = value;
}
}
public int Age
{
get { return _age; }
set
{
if (value < 0 || value > 120)
throw new ArgumentException("Age must be between 0 and 120");
_age = value;
}
}
}
Usage with proper validation:
Person person = new Person();
person.Name = "John"; // Valid
try
{
person.Age = -30; // Will throw an exception
}
catch (ArgumentException ex)
{
Console.WriteLine(ex.Message); // Output: "Age must be between 0 and 120"
}
Example 2: Auto-implemented Properties
C# provides a more concise syntax for simple properties using auto-implemented properties:
public class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
}
Behind the scenes, the compiler creates a private field and the necessary accessors.
Example 3: Read-Only Properties
You can create read-only properties to allow access to data without permitting modification:
public class Circle
{
private double _radius;
public Circle(double radius)
{
_radius = radius;
}
public double Radius
{
get { return _radius; }
// No setter means the property is read-only
}
public double Diameter
{
get { return _radius * 2; }
}
public double Area
{
get { return Math.PI * _radius * _radius; }
}
}
Usage:
Circle circle = new Circle(5);
Console.WriteLine($"Radius: {circle.Radius}"); // Output: Radius: 5
Console.WriteLine($"Diameter: {circle.Diameter}"); // Output: Diameter: 10
Console.WriteLine($"Area: {circle.Area}"); // Output: Area: 78.53981633974483
// This would cause a compilation error if uncommented:
// circle.Radius = 10;
Example 4: Private Setters
Sometimes you want to allow a property to be set only from within the class:
public class User
{
public string Username { get; private set; }
public string Email { get; set; }
public User(string username)
{
Username = username;
}
public void UpdateUsername(string newUsername)
{
if (string.IsNullOrWhiteSpace(newUsername))
throw new ArgumentException("Username cannot be empty");
Username = newUsername;
}
}
Usage:
User user = new User("john_doe");
Console.WriteLine(user.Username); // Output: john_doe
// This would cause a compilation error if uncommented:
// user.Username = "jane_doe";
// Must use the method to update the username
user.UpdateUsername("jane_doe");
Console.WriteLine(user.Username); // Output: jane_doe
Real-World Application: Bank Account
Let's create a more practical example of encapsulation with a bank account class:
public class BankAccount
{
private string _accountNumber;
private string _ownerName;
private decimal _balance;
private readonly List<Transaction> _transactions;
public string AccountNumber => _accountNumber; // Read-only property
public string OwnerName { get; private set; }
public decimal Balance => _balance; // Read-only property
public BankAccount(string accountNumber, string ownerName, decimal initialDeposit = 0)
{
if (string.IsNullOrWhiteSpace(accountNumber))
throw new ArgumentException("Account number cannot be empty");
if (string.IsNullOrWhiteSpace(ownerName))
throw new ArgumentException("Owner name cannot be empty");
if (initialDeposit < 0)
throw new ArgumentException("Initial deposit cannot be negative");
_accountNumber = accountNumber;
OwnerName = ownerName;
_balance = initialDeposit;
_transactions = new List<Transaction>();
if (initialDeposit > 0)
{
_transactions.Add(new Transaction("Initial deposit", initialDeposit));
}
}
public void Deposit(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("Deposit amount must be positive");
_balance += amount;
_transactions.Add(new Transaction("Deposit", amount));
}
public bool Withdraw(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("Withdrawal amount must be positive");
if (_balance < amount)
return false;
_balance -= amount;
_transactions.Add(new Transaction("Withdrawal", -amount));
return true;
}
public List<Transaction> GetTransactionHistory()
{
// Return a copy to prevent modifications to the internal list
return _transactions.ToList();
}
}
public class Transaction
{
public string Description { get; }
public decimal Amount { get; }
public DateTime Date { get; }
public Transaction(string description, decimal amount)
{
Description = description;
Amount = amount;
Date = DateTime.Now;
}
}
Usage:
// Create a new account
BankAccount account = new BankAccount("1234567890", "John Doe", 1000);
// Deposit money
account.Deposit(500);
// Withdraw money
bool withdrawSuccess = account.Withdraw(200);
Console.WriteLine($"Withdrawal successful: {withdrawSuccess}"); // Output: Withdrawal successful: True
Console.WriteLine($"Current balance: {account.Balance}"); // Output: Current balance: 1300
// Attempt to withdraw too much
withdrawSuccess = account.Withdraw(2000);
Console.WriteLine($"Withdrawal successful: {withdrawSuccess}"); // Output: Withdrawal successful: False
Console.WriteLine($"Current balance: {account.Balance}"); // Output: Current balance: 1300
// View transaction history
List<Transaction> transactions = account.GetTransactionHistory();
foreach (var transaction in transactions)
{
Console.WriteLine($"{transaction.Date}: {transaction.Description}, {transaction.Amount}");
}
Benefits of Encapsulation in this Example
- Data integrity: The balance can only be modified through defined methods (deposit and withdraw).
- Validation: All inputs are validated before making changes to the account.
- Audit trail: All changes to the balance are automatically tracked in the transaction history.
- Information hiding: The implementation details are hidden, and the class provides a clean interface to work with.
Best Practices for Encapsulation
- Keep fields private: Always declare fields as private and expose them through properties when needed.
- Validate input: Use properties to validate data before assigning values to fields.
- Immutable when possible: Make properties read-only when they should not be changed after initialization.
- Return copies of collections: When exposing collections, return copies to prevent direct modification.
- Minimize public interface: Only expose what's necessary for the class to be useful.
- Use properties over public fields: Even for simple data, prefer properties for future flexibility.
Advanced Encapsulation Techniques
Expression-bodied Members
C# 6.0 and later allows shorthand syntax for getter-only properties:
public class Rectangle
{
private double _width;
private double _height;
public Rectangle(double width, double height)
{
_width = width;
_height = height;
}
// Expression-bodied property
public double Area => _width * _height;
// Expression-bodied method
public bool IsSquare() => _width == _height;
}
Init-only Properties
C# 9.0 introduces init-only properties, which can only be set during object initialization:
public class Person
{
// Init-only properties
public string Name { get; init; }
public DateTime BirthDate { get; init; }
// Derived property
public int Age => DateTime.Now.Year - BirthDate.Year;
}
Usage:
var person = new Person
{
Name = "Jane Doe",
BirthDate = new DateTime(1990, 5, 15)
};
Console.WriteLine(person.Name); // Output: Jane Doe
Console.WriteLine(person.Age); // Output: Current year minus 1990
// This would cause a compilation error if uncommented:
// person.Name = "John Doe";
Summary
Encapsulation is a core principle of object-oriented programming that helps you write more maintainable and robust code. In C# and .NET, encapsulation is implemented primarily through:
- Access modifiers (
public
,private
,protected
, etc.) - Properties with getters and setters
- Methods that control access to data
By properly encapsulating your classes, you can ensure data integrity, provide a clean and stable interface, and make your code more flexible to future changes.
Exercises
-
Create a
Temperature
class that stores a temperature in Celsius internally but provides properties to get and set the temperature in both Celsius and Fahrenheit. -
Design a
ShoppingCart
class with encapsulated methods for adding items, removing items, and calculating the total price. Ensure the internal collection of items cannot be directly modified. -
Refactor the following class to use proper encapsulation techniques:
csharppublic class Employee
{
public string Name;
public double Salary;
public int VacationDays;
public void IncreaseSalary(double amount)
{
Salary += amount;
}
}
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)