Skip to main content

.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:

  1. Data hiding - It protects the internal state of an object by preventing direct access to its fields
  2. Control over data - It enables validation of data before it's modified
  3. Flexibility - Implementation details can be changed without affecting code that uses the class
  4. 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 ModifierDescription
publicAccessible from any code
privateAccessible only within the containing class or struct
protectedAccessible within the containing class or derived classes
internalAccessible within the same assembly
protected internalAccessible within the same assembly or derived classes
private protectedAccessible 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:

csharp
public class Person
{
// Fields are directly accessible
public string Name;
public int Age;
}

Usage:

csharp
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:

csharp
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:

csharp
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:

csharp
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:

csharp
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:

csharp
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:

csharp
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:

csharp
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:

csharp
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:

csharp
// 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

  1. Data integrity: The balance can only be modified through defined methods (deposit and withdraw).
  2. Validation: All inputs are validated before making changes to the account.
  3. Audit trail: All changes to the balance are automatically tracked in the transaction history.
  4. Information hiding: The implementation details are hidden, and the class provides a clean interface to work with.

Best Practices for Encapsulation

  1. Keep fields private: Always declare fields as private and expose them through properties when needed.
  2. Validate input: Use properties to validate data before assigning values to fields.
  3. Immutable when possible: Make properties read-only when they should not be changed after initialization.
  4. Return copies of collections: When exposing collections, return copies to prevent direct modification.
  5. Minimize public interface: Only expose what's necessary for the class to be useful.
  6. 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:

csharp
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:

csharp
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:

csharp
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:

  1. Access modifiers (public, private, protected, etc.)
  2. Properties with getters and setters
  3. 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

  1. 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.

  2. 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.

  3. Refactor the following class to use proper encapsulation techniques:

    csharp
    public 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! :)