MOO WebTech
OOPtheory-practicebeginner

L11 — OOP pt.2: Constructors & Encapsulation

We build on L10 and learn how to force every object to start in a valid state (constructors) and how to protect object data from misuse (encapsulation with private fields and property validation).

80 min17.04.2026L11

🎯Learning Objectives

  • Write default and parameterized constructors for your own classes
  • Use the this keyword to disambiguate constructor parameters and fields
  • Replace public fields with private fields + properties to control access
  • Add validation logic inside property setters
  • Understand the difference between public get; private set; and public get; set;

📖Theory

1. Recap — Where We Left Off in L10

Last lesson we built classes like Student with auto-properties and methods. Creating an object looked like this:

C#
Student s = new Student();
s.Name = "Alice";
s.Age  = 17;
s.GPA  = 3.8;

Two problems with this style:

  1. An object can exist half-filled. Between new Student() and setting Name, we have a Student with Name == null. If anyone calls s.PrintInfo() in that gap, it prints garbage.
  2. There's no way to reject bad data. s.Age = -5 is accepted. s.GPA = 99.0 is accepted. The class has no say.

Constructors fix the first problem. Encapsulation fixes the second.

2. What Is a Constructor?

A constructor is a special method that runs automatically when you write new ClassName(). Its job is to put the object into a valid starting state.

Rules:

  • Same name as the class
  • No return type (not even void)
  • Can have parameters like any method
C#
class Student
{
    public string Name { get; set; }
    public int Age { get; set; }
    public double GPA { get; set; }

    // Constructor — runs when you call `new Student(...)`
    public Student(string name, int age, double gpa)
    {
        Name = name;
        Age  = age;
        GPA  = gpa;
    }
}

Now you cannot create a half-filled Student. You must supply all three values:

C#
Student alice = new Student("Alice", 17, 3.8);  // ✅
Student bob   = new Student();                  // ❌ compile error — no such constructor

3. The Default Constructor

If you don't write any constructor, C# gives you a free parameterless constructor that does nothing. That's why new Student() worked in L10 — the compiler silently generated one for us.

The moment you write even one constructor of your own, the free one disappears. If you still want to allow new Student(), you must write it explicitly:

C#
class Student
{
    public string Name { get; set; } = "Unknown";
    public int    Age  { get; set; } = 0;
    public double GPA  { get; set; } = 0.0;

    public Student() { }  // explicit default constructor

    public Student(string name, int age, double gpa)
    {
        Name = name;
        Age  = age;
        GPA  = gpa;
    }
}

Having two constructors like this is called overloading — same name, different parameters. The compiler picks the right one based on what you pass.

4. The this Keyword

It's common to name the constructor parameters the same as the properties. To avoid a name clash, use this. — it means "the field/property of this object":

C#
public Student(string name, int age, double gpa)
{
    this.Name = name;   // this.Name = the property;  name = the parameter
    this.Age  = age;
    this.GPA  = gpa;
}

When property name and parameter name differ (e.g. property Name vs parameter studentName), this. is optional. Most developers use this. only when the names collide.

5. Why Public Fields/Properties Are Dangerous

Remember this from L10:

C#
Student s = new Student("Alice", 17, 3.8);
s.Age = -5;     // accepted!
s.GPA = 99.0;   // accepted!

With open { get; set; }, any code anywhere can push a Student into a nonsense state. The class is powerless.

Encapsulation is the OOP principle that says: a class should hide its internal data and control how that data is changed. The outside world gets a safe, checked API — not direct access to raw memory.

Analogy. Think of an ATM. You don't reach inside the machine and move cash around. You press buttons, and the ATM decides whether your request is allowed. The buttons are the public API; the cash drawer is private state.

6. Private Fields + Full Properties

The classic pattern: store the data in a private field, expose it through a public property that can add validation.

C#
class Student
{
    private int _age;   // private field — outside code can't touch it

    public int Age
    {
        get { return _age; }
        set
        {
            if (value < 0 || value > 120)
                throw new ArgumentException("Age must be between 0 and 120");
            _age = value;
        }
    }
}

What's happening:

  • _age is the backing field. Convention: private fields start with underscore.
  • Age is a full propertyget returns the field, set runs code before assigning.
  • value is an automatic keyword inside set — it holds whatever the caller tried to assign.

Now this crashes loudly at the bad assignment, not silently later:

C#
Student s = new Student();
s.Age = -5;   // throws ArgumentException here, before damage spreads

7. Combining with the Constructor

Good news: the setter validation runs whether you assign from the constructor or from outside. So putting this.Age = age; in the constructor automatically validates the argument too:

C#
class Student
{
    private int _age;
    private double _gpa;

    public string Name { get; set; }

    public int Age
    {
        get => _age;
        set
        {
            if (value < 0 || value > 120)
                throw new ArgumentException("Age must be 0–120");
            _age = value;
        }
    }

    public double GPA
    {
        get => _gpa;
        set
        {
            if (value < 0.0 || value > 4.0)
                throw new ArgumentException("GPA must be 0.0–4.0");
            _gpa = value;
        }
    }

    public Student(string name, int age, double gpa)
    {
        Name = name;
        Age  = age;   // validated by the setter
        GPA  = gpa;   // validated by the setter
    }
}

Tip: get => _age; is shorthand for get { return _age; }. The => syntax is called an expression body. You'll see it everywhere in modern C#.

8. Read-Only from the Outside — private set

Sometimes a value should be readable by everyone but only changed from inside the class. For example, a BankAccount balance: anyone can read it, but it must only change via Deposit / Withdraw.

C#
class BankAccount
{
    public string Owner { get; }                  // read-only, set only in constructor
    public decimal Balance { get; private set; }  // readable outside, writable only inside

    public BankAccount(string owner, decimal initialDeposit)
    {
        Owner   = owner;
        Balance = initialDeposit;
    }

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

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

From outside the class:

C#
BankAccount acc = new BankAccount("Alice", 1000m);
Console.WriteLine(acc.Balance);   // ✅ reading is fine
acc.Balance = 1_000_000m;         // ❌ compile error — no public setter
acc.Deposit(500);                 // ✅ the proper way

This is encapsulation at its cleanest: the class owns its state and forces callers through a safe door.

💻Code Examples

Example A — Product with validated price

C#
class Product
{
    private decimal _price;

    public string Name  { get; }
    public decimal Price
    {
        get => _price;
        set
        {
            if (value < 0) throw new ArgumentException("Price cannot be negative");
            _price = value;
        }
    }

    public Product(string name, decimal price)
    {
        Name  = name;
        Price = price;
    }

    public void PrintLabel() =>
        Console.WriteLine($"{Name}{Price:C}");
}

Product p = new Product("Notebook", 12.50m);
p.PrintLabel();         // Notebook — $12.50
p.Price = 9.99m;        // ok
// p.Price = -1m;       // would throw
// p.Name  = "Pen";     // compile error — no setter

Example B — Two constructors (overloading)

C#
class Rectangle
{
    public double Width  { get; }
    public double Height { get; }

    public Rectangle(double side)                // square shortcut
    {
        Width  = side;
        Height = side;
    }

    public Rectangle(double width, double height)
    {
        Width  = width;
        Height = height;
    }

    public double Area() => Width * Height;
}

Rectangle square = new Rectangle(5);         // 5 x 5
Rectangle rect   = new Rectangle(4, 7);      // 4 x 7
Console.WriteLine(square.Area());            // 25
Console.WriteLine(rect.Area());              // 28

✏️Practice Tasks

Task 1Person with validated age
EASY — IN CLASS

Create a Person class:

  • Property Name (string, read-only from outside — set only in constructor)
  • Property Age (int) with validation: must be between 0 and 120
  • Constructor Person(string name, int age)
  • Method Greet() that prints "Hi, I'm Alice and I'm 17 years old."

In the main program, create a valid Person and call Greet(). Then try p.Age = 200; inside a try/catch and print the exception message.

💡 Hint
Use a private backing field _age and a full property with get/set. In the set, compare value against the bounds and throw new ArgumentException(...) if it's out of range. Wrap the bad assignment in try { ... } catch (ArgumentException ex) { Console.WriteLine(ex.Message); }.
Task 2BankAccount with protected balance
MEDIUM — HOMEWORK

Create a BankAccount class:

  • Owner (string, read-only)
  • Balance (decimal, readable outside, writable only inside — use private set)
  • Constructor BankAccount(string owner, decimal initialDeposit) — rejects negative initial deposit
  • Deposit(decimal amount) — must be positive
  • Withdraw(decimal amount) — must be positive and ≤ balance
  • PrintStatement() — prints owner and balance

In the main program: create an account with $100, deposit $50, withdraw $30, try to withdraw $1000 (should throw), try acc.Balance = 9999m; (compile error — comment it out and explain in a comment why).

💡 Hint
For currency use decimal, not double. Use the suffix m when writing decimal literals: 100m, 30.50m. For the invalid withdrawal, use throw new InvalidOperationException("Insufficient funds").
Task 3Temperature class with two scales
HARD — HOMEWORK

Create a Temperature class that stores temperature internally in Celsius but exposes both scales:

  • Private field _celsius
  • Property Celsius (double) with validation: must be ≥ −273.15 (absolute zero)
  • Property Fahrenheit (double) — computed: get returns _celsius * 9 / 5 + 32, set converts back: _celsius = (value - 32) * 5 / 9 (still validated via the same rule)
  • Constructor Temperature(double celsius)
  • Method Print() prints "25°C / 77°F"

In the main program, create a Temperature, print it, change Fahrenheit to 100, print again, then try to set Celsius = -300 (should throw).

💡 Hint
The trick is that Fahrenheit is not stored — it's always derived from _celsius. In the Fahrenheit set, don't assign to a separate field; convert to Celsius and go through the Celsius property so you reuse its validation: Celsius = (value - 32) * 5 / 9;.

⚠️Common Mistakes

Forgetting that writing a constructor removes the default one

If you add public Student(string name, int age, double gpa) { ... } and elsewhere you wrote new Student(), you'll get a compile error. Either add a parameterless constructor explicitly, or update every new Student() call.

Infinite recursion in a property

Writing set { Age = value; } instead of set { _age = value; } calls the setter again, and again, and again — until the stack overflows. Always assign to the field (_age) inside the property, never to the property itself.

Validating only in the constructor, not in the setter

If you check the range inside the constructor but not inside the property, code can still write s.Age = -5; after construction. Put validation in the setter — the constructor will benefit automatically because it assigns through the setter.

Using double for money

double has tiny floating-point errors. 0.1 + 0.2 is not exactly 0.3. For currency always use decimal with the m suffix: 100.50m. This is a real-world bug that shows up in invoices.

Making the backing field public

The whole point of encapsulation is that outsiders can only reach the field through the property. If the field is public, they bypass the property's validation. Backing fields must be private.

🎓Instructor Notes

⚡ How to run this lesson (~80 min)

  • [5 min] Recap L10 + motivation. Project the L10 Student class. Ask: "What prevents me from writing s.Age = -5; right now?" Silence → pivot into constructors and encapsulation.
  • [15 min] Live code: constructors. Add a constructor to Student. Show the compiler error when calling new Student() after that. Re-add the default constructor. Demonstrate this..
  • [20 min] Live code: private field + validated property. Convert Age from auto-property to full property. Throw on bad input. Then run try/catch and catch the exception. This is many students' first time seeing throw, so narrate it slowly.
  • [10 min] private set and read-only properties. Build the BankAccount. Emphasise: "the class is the only one allowed to change the balance. Outside code has to go through Deposit / Withdraw."
  • [25 min] Task 1 in class. The trap is infinite recursion in the setter (Age = value; instead of _age = value;). Have the fix on the projector.
  • [5 min] Wrap up + assign homework. Preview L12: inheritance and interfaces. Assign Tasks 2 and 3.

💬 Discussion questions

  • "Why is it better for the class to throw an exception than to silently ignore a bad value?"
  • "When would you want a property to be read-only from outside but writable from inside?"
  • "In the Temperature class, why don't we store Fahrenheit as a separate field?"