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.
Examplepublic 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;
}
}ResultA BankAccount can never start with a blank owner or negative balance.
Check your understandingExplain 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.
Examplepublic 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);ResultFalse
Check your understandingExplain 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.
Examplepublic 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);Result12.566370614359172
Check your understandingExplain 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.
Examplepublic interface IMessageSender
{
void Send(string message);
}
public class ReminderService(IMessageSender sender)
{
public void Remind(string task) => sender.Send($"Reminder: {task}");
}ResultReminderService can use email, SMS, or a test sender without changing its logic.
Check your understandingExplain 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.
- 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.
- 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.
- 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.
- 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
- Write a short specification listing inputs, outputs, rules, and failure cases.
- Break the work into small functions, queries, modules, or classes appropriate to the language.
- Build the smallest working version before adding optional features.
- Test normal, boundary, empty, and invalid cases and record the results.
- Refactor names and duplication, then write a concise user guide.
Key terms
- class
- object
- property
- invariant
- record
- value equality
- inheritance
- polymorphism
- composition
- dependency injection