C# Object Oriented Programming Encapsulation

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 (functions) that operate on that data into a single unit called a class. It also involves restricting direct access to some of an object's components, which is a means of preventing unintended interference and misuse of the data.

1. Purpose of Encapsulation

Encapsulation serves several key purposes in software development:

- Data Hiding: Protects the internal state of an object from external modification. Only the class's own methods can directly modify its fields.

- Controlled Access: Provides a controlled way of accessing and modifying the object's data through methods or properties.

- Modularity: Enhances modularity by keeping related data and behaviors together.

- Maintainability: Makes the code easier to maintain and evolve by localizing changes within the class.

- Flexibility and Reusability: Allows classes to be reused and extended without affecting other parts of the program.

2. Access Modifiers in C#

C# provides several access modifiers to control the visibility of class members:

- public: Accessible from any other code.

- private: Accessible only within the same class.

- protected: Accessible within the same class and by derived class instances.

- internal: Accessible only within the same assembly.

- protected internal: Accessible within the same assembly and from derived classes.

- private protected: Accessible within the same class and derived classes in the same assembly.

3. Implementing Encapsulation in C#

Encapsulation in C# is typically achieved using access modifiers and properties. Here's how you can implement it:

a. Using Access Modifiers By declaring class fields as private, you prevent external classes from accessing them directly. Instead, you provide public methods or properties to interact with these fields.

b. Using Properties Properties in C# provide a flexible mechanism to read, write, or compute the values of private fields. They can include logic to enforce validation rules or other constraints.

4. Detailed Example of Encapsulation

Let's delve into a detailed example that demonstrates encapsulation in C#.

Example: BankAccount Class
using System;

class BankAccount
{
    // Private fields to hold account data
    private string accountNumber;
    private string accountHolder;
    private decimal balance;

    // Public property to get the account number (read-only)
    public string AccountNumber
    {
        get { return accountNumber; }
    }

    // Public property to get and set the account holder's name
    public string AccountHolder
    {
        get { return accountHolder; }
        set
        {
            if (string.IsNullOrWhiteSpace(value))
                throw new ArgumentException("Account holder name cannot be empty.");
            accountHolder = value;
        }
    }

    // Public property to get the balance (read-only)
    public decimal Balance
    {
        get { return balance; }
    }

    // Constructor to initialize the bank account
    public BankAccount(string accountNumber, string accountHolder, decimal initialBalance)
    {
        if (string.IsNullOrWhiteSpace(accountNumber))
            throw new ArgumentException("Account number cannot be empty.");
        if (string.IsNullOrWhiteSpace(accountHolder))
            throw new ArgumentException("Account holder name cannot be empty.");
        if (initialBalance < 0)
            throw new ArgumentException("Initial balance cannot be negative.");

        this.accountNumber = accountNumber;
        this.accountHolder = accountHolder;
        this.balance = initialBalance;
    }

    // Public method to deposit money
    public void Deposit(decimal amount)
    {
        if (amount <= 0)
            throw new ArgumentException("Deposit amount must be positive.");
        balance += amount;
        Console.WriteLine($"Deposited {amount:C}. New Balance: {balance:C}");
    }

    // Public method to withdraw money
    public void Withdraw(decimal amount)
    {
        if (amount <= 0)
            throw new ArgumentException("Withdrawal amount must be positive.");
        if (amount > balance)
            throw new InvalidOperationException("Insufficient funds.");
        balance -= amount;
        Console.WriteLine($"Withdrew {amount:C}. New Balance: {balance:C}");
    }

    // Public method to display account information
    public void DisplayAccountInfo()
    {
        Console.WriteLine($"Account Number: {accountNumber}");
        Console.WriteLine($"Account Holder: {accountHolder}");
        Console.WriteLine($"Balance: {balance:C}");
    }
}

class Program
{
    static void Main()
    {
        // Creating a new bank account
        BankAccount account = new BankAccount("123456789", "John Doe", 1000m);
        account.DisplayAccountInfo();
        // Output:
        // Account Number: 123456789
        // Account Holder: John Doe
        // Balance: $1,000.00

        // Depositing money
        account.Deposit(500m);
        // Output: Deposited $500.00. New Balance: $1,500.00

        // Withdrawing money
        account.Withdraw(200m);
        // Output: Withdrew $200.00. New Balance: $1,300.00

        // Attempting invalid operations
        try
        {
            account.Withdraw(2000m);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error: {ex.Message}");
            // Output: Error: Insufficient funds.
        }
    }
}

Output:
Account Number: 123456789
Account Holder: John Doe
Balance: $1,000.00
Deposited $500.00. New Balance: $1,500.00
Withdrew $200.00. New Balance: $1,300.00
Error: Insufficient funds.



Explanation: 1. Private Fields: The accountNumber, accountHolder, and balance fields are declared as private, ensuring they cannot be accessed directly from outside the BankAccount class.

2. Public Properties:
- AccountNumber: Read-only property exposing the account number.
- AccountHolder: Read-write property with validation to ensure the name is not empty.
- Balance: Read-only property exposing the current balance.

3. Constructor: Initializes the bank account with validation to prevent invalid data.

4. Public Methods:
- Deposit: Allows adding funds to the account with validation.
- Withdraw: Allows withdrawing funds with validation to prevent overdrafts.
- DisplayAccountInfo: Displays the account details.

5. Error Handling: The Main method demonstrates how the class handles valid and invalid operations, showcasing encapsulation by preventing invalid states.

5. Benefits of Encapsulation

- Enhanced Security: Sensitive data is protected from unauthorized access and modification.
- Controlled Access: Through properties and methods, you can enforce rules and validations.
- Improved Maintainability: Changes to the internal implementation do not affect external code that interacts with the class.
- Modularity: Encapsulated classes are self-contained, making the codebase easier to manage.
- Ease of Debugging: Isolated classes make it easier to identify and fix issues.

6. Advanced Encapsulation Techniques

a. Read-Only Properties You can create properties that are read-only from outside the class but can be modified within the class.
public string AccountNumber { get; }

b. Computed Properties Properties can compute values on the fly without storing them in a field.
public decimal Interest
{
    get { return balance * 0.05m; } // 5% interest
}

c. Private Setters Allow the property to be read publicly but set privately within the class.
public string AccountHolder { get; private set; }

d. Expression-Bodied Members Simplify property definitions using expression-bodied members.
public decimal Balance => balance;

7. Encapsulation vs. Other OOP Principles

- Encapsulation vs. Inheritance: Encapsulation focuses on hiding data and providing controlled access, while inheritance allows classes to inherit behaviors and properties from other classes.

- Encapsulation vs. Polymorphism: Encapsulation manages how data is accessed and modified, whereas polymorphism allows methods to operate differently based on the object’s actual type.

- Encapsulation vs. Abstraction: Both aim to reduce complexity, but encapsulation hides the internal state and requires interaction through methods, while abstraction focuses on exposing only the relevant features of an object.

8. Best Practices for Encapsulation

- Keep Fields Private: Always declare class fields as private to enforce data hiding.

- Use Properties Instead of Public Fields: Properties provide a controlled way to access and modify data.

- Validate Data in Setters: Ensure that any data being set meets the required criteria to maintain object integrity.

- Limit Exposure of Internal Data: Expose only what is necessary through methods and properties.

- Immutable Objects: Where possible, design classes to be immutable by providing read-only properties and initializing fields through constructors.

9. Common Encapsulation Mistakes

- Using Public Fields: Exposing fields as public can lead to uncontrolled access and modification.

- Lack of Validation: Failing to validate data in setters can result in invalid object states.

- Overexposing Properties: Providing unnecessary setters or getters can compromise data integrity.

- Breaking Encapsulation with Reflection: Although advanced, using reflection to access private members can undermine encapsulation principles.

10. Real-World Example: Encapsulation in a User Management System

Consider a user management system where user details need to be managed securely.

using System;

class User
{
    // Private fields
    private string username;
    private string password; // Sensitive data

    // Public property for Username with validation
    public string Username
    {
        get { return username; }
        set
        {
            if (string.IsNullOrWhiteSpace(value))
                throw new ArgumentException("Username cannot be empty.");
            username = value;
        }
    }

    // Public method to set the password with validation
    public void SetPassword(string newPassword)
    {
        if (newPassword.Length < 6)
            throw new ArgumentException("Password must be at least 6 characters long.");
        password = newPassword;
    }

    // Public method to verify the password
    public bool VerifyPassword(string inputPassword)
    {
        return password == inputPassword;
    }

    // Constructor
    public User(string username, string password)
    {
        Username = username;
        SetPassword(password);
    }
}

class Program
{
    static void Main()
    {
        User user = new User("johndoe", "securePass");
        Console.WriteLine($"Username: {user.Username}"); // Output: Username: johndoe

        // Verifying password
        Console.WriteLine("Password verification (correct): " + user.VerifyPassword("securePass")); // Output: True
        Console.WriteLine("Password verification (incorrect): " + user.VerifyPassword("wrongPass")); // Output: False

        // Attempting to set an invalid password
        try
        {
            user.SetPassword("123");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error: {ex.Message}");
            // Output: Error: Password must be at least 6 characters long.
        }
    }
}

Output:
Username: johndoe
Password verification (correct): True
Password verification (incorrect): False
Error: Password must be at least 6 characters long.


Explanation:
1. Private Fields: username and password are private, ensuring that they cannot be accessed directly from outside the User class.

2. Public Property (Username): Allows getting and setting the username with validation to prevent empty values.

3. Public Methods (SetPassword and VerifyPassword):
- SetPassword: Allows setting the password with validation to enforce security policies (e.g., minimum length).
- VerifyPassword: Allows checking if an input password matches the stored password without exposing the actual password.

4. Constructor: Initializes the User object with validated username and password.

5. Main Method: Demonstrates creating a User, verifying passwords, and handling invalid password attempts.

11. Encapsulation in Interfaces

While interfaces themselves do not encapsulate data, they can define contracts that ensure implementing classes adhere to encapsulation principles. By specifying methods and properties without exposing internal implementation, interfaces promote encapsulated designs.

using System;

// Interface defining the contract
interface IAccount
{
    string AccountNumber { get; }
    decimal Balance { get; }
    void Deposit(decimal amount);
    void Withdraw(decimal amount);
}

// Implementing the interface with encapsulation
class BankAccount : IAccount
{
    private string accountNumber;
    private decimal balance;

    public string AccountNumber
    {
        get { return accountNumber; }
    }

    public decimal Balance
    {
        get { return balance; }
    }

    public BankAccount(string accountNumber, decimal initialBalance)
    {
        if (initialBalance < 0)
            throw new ArgumentException("Initial balance cannot be negative.");
        this.accountNumber = accountNumber;
        this.balance = initialBalance;
    }

    public void Deposit(decimal amount)
    {
        if (amount <= 0)
            throw new ArgumentException("Deposit amount must be positive.");
        balance += amount;
    }

    public void Withdraw(decimal amount)
    {
        if (amount <= 0)
            throw new ArgumentException("Withdrawal amount must be positive.");
        if (amount > balance)
            throw new InvalidOperationException("Insufficient funds.");
        balance -= amount;
    }
}

class Program
{
    static void Main()
    {
        IAccount account = new BankAccount("987654321", 500m);
        account.Deposit(150m);
        account.Withdraw(100m);
        Console.WriteLine($"Account Number: {account.AccountNumber}, Balance: {account.Balance:C}");
        // Output: Account Number: 987654321, Balance: $550.00
    }
}

Output:
Account Number: 987654321, Balance: $550.00


Explanation:
1. Interface (IAccount): Defines the contract for account operations without exposing how data is stored or managed.

2. Implementing Class (BankAccount): Encapsulates the accountNumber and balance fields, providing controlled access through properties and methods.

3. Main Method: Interacts with the BankAccount through the IAccount interface, ensuring encapsulation is maintained.

12. Encapsulation in Inheritance

Encapsulation works seamlessly with inheritance. Derived classes can inherit encapsulated members from base classes while maintaining their own encapsulation boundaries.


using System;

class Vehicle
{
    // Private field
    private string vin;

    // Public property to get VIN (read-only)
    public string VIN
    {
        get { return vin; }
    }

    // Protected method to set VIN (accessible by derived classes)
    protected void SetVIN(string vin)
    {
        this.vin = vin;
    }

    // Constructor
    public Vehicle(string vin)
    {
        SetVIN(vin);
    }

    public void DisplayInfo()
    {
        Console.WriteLine($"VIN: {vin}");
    }
}

class Car : Vehicle
{
    // Private field
    private string model;

    // Public property for Model
    public string Model
    {
        get { return model; }
        set
        {
            if (string.IsNullOrWhiteSpace(value))
                throw new ArgumentException("Model cannot be empty.");
            model = value;
        }
    }

    // Constructor
    public Car(string vin, string model) : base(vin)
    {
        Model = model;
    }

    public void DisplayCarInfo()
    {
        DisplayInfo();
        Console.WriteLine($"Model: {model}");
    }
}

class Program
{
    static void Main()
    {
        Car car = new Car("1HGCM82633A004352", "Accord");
        car.DisplayCarInfo();
        // Output:
        // VIN: 1HGCM82633A004352
        // Model: Accord
    }
}

Output:
VIN: 1HGCM82633A004352
Model: Accord


Explanation:
1. Base Class (Vehicle):
- Private Field (vin): Encapsulates the Vehicle Identification Number.

- Public Property (VIN): Provides read-only access to the VIN.

- Protected Method (SetVIN): Allows derived classes to set the VIN while keeping it hidden from external classes.

- Constructor: Initializes the VIN using the protected setter.

2. Derived Class (Car):
- Private Field (model): Encapsulates the car model.

- Public Property (Model): Provides controlled access to the model with validation.

- Constructor: Initializes both VIN (through the base class) and model.

- Method (DisplayCarInfo): Displays encapsulated information.

3. Main Method: Creates a Car object and displays its information, demonstrating how encapsulation is maintained across inheritance.

13. Summary

Encapsulation is a core principle of Object-Oriented Programming in C# that promotes the protection, modularity, and maintainability of code. By bundling data and methods within classes and controlling access through access modifiers and properties, encapsulation ensures that the internal state of objects is shielded from unintended interference and misuse. This leads to more robust, flexible, and maintainable software designs.

Understanding and effectively applying encapsulation allows developers to create classes that are both secure and easy to use, laying a strong foundation for building complex and scalable applications.

Previous: C# Object Oriented Programming - Class and Interface | Next: C# Object Oriented Programming - Inheritance and Polymorphism

<
>