DEV Community

mohamed Tayel
mohamed Tayel

Posted on

Mastering C# Fundamentals: Encapsulation

Meta Description: Learn how to enforce encapsulation in C# using properties. This article explores the benefits of encapsulation, provides examples of refactoring public fields into private fields with properties, and includes exercises for various skill levels to improve your understanding of OOP in C#.

Encapsulation is one of the fundamental principles of Object-Oriented Programming (OOP). It allows us to control how our class fields are accessed and modified, which ultimately helps maintain control over our data and ensure it behaves in a predictable way. In this article, we’ll explore how to use encapsulation in C#, focusing on private fields and public properties.

The Problem with Public Fields

Let’s say we have a class named Student:

public class Student
{
    public string firstName;
    public int age;
}
Enter fullscreen mode Exit fullscreen mode

The fields firstName and age are public, meaning anyone with access to a Student object can freely read or change those fields:

Student student = new Student();
student.firstName = "Alice";
student.age = 15;

// The values can be changed from anywhere
student.age = -5; // Oops! A negative age makes no sense.
Enter fullscreen mode Exit fullscreen mode

This approach is problematic because it doesn't protect the internal state of our class. Any code can directly change the values, even if they become invalid, as in the case of the negative age.

To solve this problem, we can use encapsulation to hide the internal fields and expose them in a controlled way through properties.

Applying Encapsulation Using Properties

To properly encapsulate the data in our Student class, we make the fields private and expose them using properties:

public class Student
{
    private string firstName;
    private int age;

    public string FirstName
    {
        get { return firstName; }
        set { firstName = value; }
    }

    public int Age
    {
        get { return age; }
        set
        {
            if (value > 0) // Adding validation
            {
                age = value;
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

By using properties, we can now control access to the fields. We have added validation logic to ensure age is always a positive value.

Levels of Assignment for Practice

Now, let's go over three different levels of assignments to practice encapsulation with different use cases.

Assignment Level: Easy

  • Goal: Convert fields to properties and add basic validation.
  • Scenario: Create a Book class with the following fields: title (string), author (string), and numberOfPages (int).
  • Requirements:
    • Convert the fields to private.
    • Add public properties to expose the fields.
    • Ensure numberOfPages cannot be negative.

Solution:

public class Book
{
    private string title;
    private string author;
    private int numberOfPages;

    public string Title
    {
        get { return title; }
        set { title = value; }
    }

    public string Author
    {
        get { return author; }
        set { author = value; }
    }

    public int NumberOfPages
    {
        get { return numberOfPages; }
        set
        {
            if (value > 0)
            {
                numberOfPages = value;
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Assignment Level: Medium

  • Goal: Add read-only and write-only properties.
  • Scenario: Create a BankAccount class with the following fields: accountNumber (string), balance (decimal), and owner (string).
  • Requirements:
    • The accountNumber should be read-only after it is set in the constructor.
    • The balance should only be updated through a deposit method and should never be directly set.
    • Add an owner property that can be read and updated.

Solution:

public class BankAccount
{
    private string accountNumber;
    private decimal balance;
    private string owner;

    public BankAccount(string accountNumber, string owner)
    {
        this.accountNumber = accountNumber;
        this.owner = owner;
    }

    public string AccountNumber
    {
        get { return accountNumber; }
    }

    public decimal Balance
    {
        get { return balance; }
    }

    public string Owner
    {
        get { return owner; }
        set { owner = value; }
    }

    public void Deposit(decimal amount)
    {
        if (amount > 0)
        {
            balance += amount;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example:

  • The accountNumber can be set only through the constructor and is read-only.
  • The balance is updated through a Deposit method, ensuring better control over how it changes.
  • The owner property allows for both read and write access.

Assignment Level: Difficult

  • Goal: Implement controlled access and use private setters.
  • Scenario: Create an Employee class with the following fields: name (string), hourlyRate (decimal), and hoursWorked (int).
  • Requirements:
    • Add a public property for name that allows both getting and setting.
    • The hourlyRate should be updated only if it is positive, and should have a private setter.
    • The hoursWorked should have a public getter but a private setter. Add a method called AddHours to add to the hoursWorked.
    • Calculate the total pay using a CalculatePay() method, which uses the hourlyRate and hoursWorked.

Solution:

public class Employee
{
    private string name;
    private decimal hourlyRate;
    private int hoursWorked;

    public string Name
    {
        get { return name; }
        set { name = value; }
    }

    public decimal HourlyRate
    {
        get { return hourlyRate; }
        private set
        {
            if (value > 0)
            {
                hourlyRate = value;
            }
        }
    }

    public int HoursWorked
    {
        get { return hoursWorked; }
        private set { hoursWorked = value; }
    }

    public Employee(string name, decimal hourlyRate)
    {
        this.name = name;
        HourlyRate = hourlyRate; // Using the property to leverage validation
        hoursWorked = 0; // Initializing hours worked to 0
    }

    public void AddHours(int hours)
    {
        if (hours > 0)
        {
            hoursWorked += hours;
        }
    }

    public decimal CalculatePay()
    {
        return hourlyRate * hoursWorked;
    }
}
Enter fullscreen mode Exit fullscreen mode

In this advanced example:

  • The name property allows read and write access.
  • The hourlyRate has a private setter, so it can only be modified internally, ensuring it is never set to an invalid value externally.
  • The hoursWorked can be read publicly but is only modified via the AddHours method, which ensures that only valid hours are added.
  • The CalculatePay() method allows us to compute the total payment based on the hourlyRate and hoursWorked.

Summary

In this article, we explored how to apply encapsulation in C# by changing public fields to private fields and using properties to control access to the internal data. Encapsulation helps ensure that data is handled in a controlled way, preventing invalid values from being set and hiding internal details.

We also provided three assignments of varying difficulty levels to practice the concept:

  • Easy: Adding basic validation to fields using properties.
  • Medium: Using read-only and write-only properties to enforce controlled access.
  • Difficult: Combining properties with private setters and methods to encapsulate data fully.

Properties are powerful tools that help us create robust and maintainable classes by enforcing controlled access to the data while hiding the implementation details. Encapsulation is crucial for building reliable applications, ensuring that data can only be modified in safe and predictable ways.

Top comments (0)