โ† All C# lessons

C# / Chapter 04

Classes, Records & Object-Oriented Design

Model valid objects with classes and records, encapsulate state, use inheritance carefully, and compose focused behavior.

Chapter roadmap

Model valid objects with classes and records, encapsulate state, use inheritance carefully, and compose focused behavior. Work through the concepts in order, type every example yourself, and keep a short debugging log that records predictions, errors, fixes, and what each fix taught you.

Learning objectives

  • Design encapsulated classes
  • Use constructors and properties
  • Choose records for value-like data
  • Prefer composition over unnecessary inheritance

04.1

Classes, properties, and invariants

A class groups state with behavior that keeps that state valid. Properties expose controlled access and can calculate values without storing duplicate data. Constructors should reject impossible states so every created object starts valid.

Before running the example, trace each expression and write down the expected state or result. Then run it, compare the actual result, and change one input or rule to confirm that you understand which part controls the behavior.

Example
public class BankAccount
{
    public string Owner { get; }
    public decimal Balance { get; private set; }

    public BankAccount(string owner, decimal openingBalance)
    {
        if (string.IsNullOrWhiteSpace(owner)) throw new ArgumentException("Owner required");
        if (openingBalance < 0) throw new ArgumentOutOfRangeException(nameof(openingBalance));
        Owner = owner;
        Balance = openingBalance;
    }

    public void Deposit(decimal amount)
    {
        if (amount <= 0) throw new ArgumentOutOfRangeException(nameof(amount));
        Balance += amount;
    }
}
Result
A BankAccount can never start with a blank owner or negative balance.
Check your understanding

Explain the example in plain language, identify one boundary or failure case, and revise it so the output changes in a predictable way.

04.2

Records and value equality

A record is concise for data whose identity is its values. Records provide value-based equality and with expressions for nondestructive updates. Use classes when evolving identity and protected operations matter more than value equality.

Before running the example, trace each expression and write down the expected state or result. Then run it, compare the actual result, and change one input or rule to confirm that you understand which part controls the behavior.

Example
public record Student(int Id, string Name, string Email);

var original = new Student(12, "Ana", "ana@example.com");
var updated = original with { Email = "ana@school.edu" };
Console.WriteLine(original == updated);
Result
False
Check your understanding

Explain the example in plain language, identify one boundary or failure case, and revise it so the output changes in a predictable way.

04.3

Inheritance and polymorphism

Inheritance models a genuine is-a relationship and allows derived objects to be used through a base abstraction. virtual and override enable runtime polymorphism. Base classes should have clear contracts; avoid inheritance used only to share a few helper methods.

Before running the example, trace each expression and write down the expected state or result. Then run it, compare the actual result, and change one input or rule to confirm that you understand which part controls the behavior.

Example
public abstract class Shape
{
    public abstract double Area { get; }
}

public sealed class Circle(double radius) : Shape
{
    public override double Area => Math.PI * radius * radius;
}

Shape shape = new Circle(2);
Console.WriteLine(shape.Area);
Result
12.566370614359172
Check your understanding

Explain the example in plain language, identify one boundary or failure case, and revise it so the output changes in a predictable way.

04.4

Composition and dependency injection

Composition builds an object from collaborators with focused responsibilities. Passing collaborators through a constructor makes dependencies explicit and replaceable in tests. Depending on abstractions prevents a class from controlling infrastructure it does not own.

Before running the example, trace each expression and write down the expected state or result. Then run it, compare the actual result, and change one input or rule to confirm that you understand which part controls the behavior.

Example
public interface IMessageSender
{
    void Send(string message);
}

public class ReminderService(IMessageSender sender)
{
    public void Remind(string task) => sender.Send($"Reminder: {task}");
}
Result
ReminderService can use email, SMS, or a test sender without changing its logic.
Check your understanding

Explain the example in plain language, identify one boundary or failure case, and revise it so the output changes in a predictable way.

Chapter assignments

Complete these in order. Later assignments assume that earlier skills are working. Do not only test the sample values; design tests that challenge boundaries, missing values, incorrect types, empty collections, and other likely failures.

  1. A4.1

    Design a validated Rectangle class with calculated properties and controlled resize operations.

    Submit readable source code, a brief design note, and evidence from at least three tests including one boundary or invalid case.

  2. A4.2

    Create immutable records for address and contact data and use with expressions for updates.

    Submit readable source code, a brief design note, and evidence from at least three tests including one boundary or invalid case.

  3. A4.3

    Model two shape types behind a base abstraction and calculate total area polymorphically.

    Submit readable source code, a brief design note, and evidence from at least three tests including one boundary or invalid case.

  4. A4.4

    Refactor a class that creates its own file or message service so the dependency is passed through its constructor.

    Submit readable source code, a brief design note, and evidence from at least three tests including one boundary or invalid case.

Chapter project

Library Domain Model

Library Domain Model โ€” Create Book, Member, Loan, and Library types with valid constructors, controlled checkout and return operations, overdue calculations, and useful reports. Use records for value-like identifiers or snapshots and composition for time or notification services.

Required process

  1. Write a short specification listing inputs, outputs, rules, and failure cases.
  2. Break the work into small functions, queries, modules, or classes appropriate to the language.
  3. Build the smallest working version before adding optional features.
  4. Test normal, boundary, empty, and invalid cases and record the results.
  5. Refactor names and duplication, then write a concise user guide.

Key terms

  • class
  • object
  • property
  • invariant
  • record
  • value equality
  • inheritance
  • polymorphism
  • composition
  • dependency injection