Lesson 1 of 45 beginner

OOP in C#: The Four Pillars, Honestly

Encapsulation, inheritance, polymorphism and abstraction, and which ones you should really use

Guess first

Pick an answer. You will see if you were right at the end of the lesson.

Why does BankAccount use a private setter for Balance?

Open interactive version (quiz + challenge)

Real-world analogy

Think of a bank vault. The customer cannot walk in and change the number on the ledger. They hand a slip to the teller, and the teller checks the rules first. Many tellers can handle the same slip in their own way. The customer does not care which one serves them. Here the vault is the object, the teller is its public method, and the rules live behind the counter.

What is it?

Object-oriented programming organises code as objects that own their data and expose behaviour. The four pillars are encapsulation (protect the rules) and inheritance (reuse by is-a). The others are polymorphism (one call, different results) and abstraction (hide details behind a contract). In practice, modern C# leans on encapsulation, interfaces and composition, and uses inheritance sparingly.

Real-world relevance

A checkout service takes an IPaymentMethod. It does not care if it is a card, a wallet or a bank transfer. Adding a new method means adding one class. In the same system, the account has a private balance. A bug in the UI can never write a negative number straight into it.

Key points

Quick check

Answer from memory — scrolling up first makes it stick less.

You want a class that has shared state and a constructor, and subclasses must implement one method. What fits?

Which is the better default when you want to reuse behaviour?

Code example

public class BankAccount
{
    public decimal Balance { get; private set; }

    public BankAccount(decimal opening) => Balance = opening;

    public void Withdraw(decimal amount)
    {
        if (amount <= 0) throw new ArgumentException("Amount must be positive.");
        if (amount > Balance) throw new InvalidOperationException("Not enough money.");
        Balance -= amount;
    }
}

public interface IPaymentMethod { string Pay(decimal amount); }

public sealed class CardPayment : IPaymentMethod
{
    public string Pay(decimal amount) => $"Charged {amount} to card";
}

public sealed class WalletPayment : IPaymentMethod
{
    public string Pay(decimal amount) => $"Took {amount} from wallet";
}

public static class Checkout
{
    public static void Run(IEnumerable<IPaymentMethod> methods)
    {
        foreach (var method in methods) Console.WriteLine(method.Pay(10m));
    }
}

Line-by-line walkthrough

  1. 1. Balance can be read by anyone but set only inside the class, because the setter is private.
  2. 2. Withdraw is the only door. It checks the amount is positive and that there is enough money before it changes the balance.
  3. 3. IPaymentMethod is a contract with one method. It says what a payment can do, not how.
  4. 4. CardPayment and WalletPayment are sealed classes that each implement the contract their own way.
  5. 5. Checkout.Run takes a list of IPaymentMethod. It calls Pay on each, and the real object decides what runs.
  6. 6. Adding a bank transfer means writing one new class. Checkout does not change at all.

Spot the bug

public class BankAccount
{
    public decimal Balance { get; set; }

    public void Withdraw(decimal amount)
    {
        if (amount > Balance) throw new InvalidOperationException("Not enough money.");
        Balance -= amount;
    }
}

// somewhere in the UI layer
account.Balance -= 5000;
Need a hint?
Who can change Balance without going through Withdraw?
Show answer
The setter is public, so any caller can skip the check in Withdraw and push the balance below zero. The rule is no longer protected. Fix: make the setter private (<code>{ get; private set; }</code>) so every change goes through Withdraw.

Explain like I'm 5

A toy has a lid, and you can only play with it through the buttons on the outside. You cannot reach in and change the inside gears, so it cannot be broken that way. Different toys can have the same buttons but do different things when you press them.

Fun fact

The C# team added records because developers kept writing the same boilerplate for equality, hashing and printing. One line, record Money(decimal Amount, string Currency), now generates all of it.

Hands-on challenge

Say it out loud in 30 seconds: "What is the difference between an abstract class and an interface, and when do you prefer composition over inheritance?" Then check: did you mention shared state and constructors, multiple interfaces, and swapping behaviour without a rigid tree?

More resources

Open interactive version (quiz + challenge) Download the whole course as PDF / Word

Lock it in

← Back to course: .NET + React Playbook