OO Fundamentals & UML
Before any pattern or principle makes sense, you need a solid grip on four ideas — abstraction, encapsulation, inheritance, polymorphism — and on how objects relate to each other in code (association, aggregation, composition, delegation). This chapter also gives you UML, the diagramming language you'll use throughout the module to draw designs before you build them. Everything here is the vocabulary the rest of the course assumes you already have.
The four pillars of OOP
Object-oriented programming rests on four ideas. They aren't independent — encapsulation makes abstraction trustworthy, inheritance is one way to reuse abstractions, and polymorphism is what makes inheritance (and interfaces) actually pay off at runtime. Learn them together.
Abstraction
Abstraction is a simplified model of something real, kept only as detailed as your context needs. When you turn a real-world "airplane" into a class, you don't try to capture every bolt and wire — you capture only the attributes and behaviours that matter for the program you're writing, and you throw the rest away on purpose.
Without abstraction, every class would need to model reality with total fidelity, which is both
impossible and pointless. Abstraction is what lets you write a Customer class with five
properties instead of trying to encode a human being.
A subtlety from the slides that's easy to miss: the same real-world thing gets different
abstractions depending on why you're modelling it. An Airplane class in a flight
simulator and an Airplane class in a booking system share a name — and nothing else.
public class Airplane
{
public double AltitudeMeters { get; private set; }
public double SpeedKnots { get; private set; }
public double FuelKg { get; private set; }
public void ApplyThrust(double percent)
{
// physics: accelerate, burn fuel, climb...
FuelKg -= percent * 0.4;
SpeedKnots += percent * 2.1;
}
}
public class Airplane
{
public string TailNumber { get; }
public SeatMap Seats { get; } = new();
public Airplane(string tailNumber) => TailNumber = tailNumber;
public bool TryReserveSeat(string seatCode) =>
Seats.Reserve(seatCode);
}
Neither version is "more correct." Each is the right abstraction for its purpose — a lesson that generalises: whenever you design a class, ask "what does this program actually need to know about this thing?" before you write a single field.
Encapsulation
Encapsulation means hiding an object's internal state and exposing only the operations that are safe to call from outside. A class is a template — encapsulation is the promise that template makes: "you can only change my state through my methods, so I can guarantee I never end up in a nonsense state."
A car's accelerator pedal is an interface. You don't reach under the hood and manipulate the fuel injectors directly — the pedal is the only sanctioned way in, and it enforces limits (you can't inject negative fuel). Encapsulation is building that pedal instead of leaving the engine bay open.
Watch what happens without it — a BankAccount with a public field has no way to reject
an invalid value, because there's no code path to reject it through:
var acc = new BankAccount();
acc.Balance = -500; // perfectly legal, and now the account is broken
public class BankAccount
{
public decimal Balance;
}
Balance to anything. There's no
single place to enforce "balance can't go negative" — you'd have to trust every caller forever.public class BankAccount
{
private decimal _balance;
public decimal Balance
{
get => _balance;
private set
{
if (value < 0)
throw new ArgumentException("Balance cannot be negative.");
_balance = value;
}
}
public BankAccount(decimal openingBalance) => Balance = openingBalance;
public void Deposit(decimal amount)
{
if (amount <= 0) throw new ArgumentException("Deposit must be positive.");
Balance += amount;
}
public void Withdraw(decimal amount)
{
// The guard below is part of the invariant too: without it, Withdraw(-100)
// would INCREASE the balance — the exact bug encapsulation exists to prevent.
if (amount <= 0) throw new ArgumentException("Withdrawal amount must be positive.");
if (amount > Balance) throw new InvalidOperationException("Insufficient funds.");
Balance -= amount;
}
}
Deposit and Withdraw, and
both enforce the rules — including the easy-to-miss one: a negative amount passed to
Withdraw would otherwise increase the balance. It is now impossible to
construct an invalid BankAccount from outside the class.The slides put it simply: attributes are usually private, and only methods of the same
class can touch them; protected extends that trust to subclasses too.
| Modifier | Visible to | Typical use |
|---|---|---|
private | Only this class | Internal state — the default choice for fields |
protected | This class + subclasses | State a subclass legitimately needs to extend behaviour |
internal | Same assembly (project) | Implementation details shared across your own codebase, hidden from consumers |
public | Everyone | The deliberate, minimal interface to the outside world |
A public property is not the same as a public field, even though C# lets you write
obj.Balance = 5 for both. A property is a pair of methods (get/set)
wearing a field's syntax — you can validate, compute, or make it read-only (private set or
get-only) without changing a single line of calling code. A raw public field can never do
that. Modern C# defaults to properties for exactly this reason.
Inheritance
Inheritance lets you build a new class on top of an existing one, reusing its fields and methods instead of duplicating them. The new class (subclass / derived class) extends the existing one (superclass / base class), and can override inherited behaviour — replacing it entirely, or calling the base version and adding to it.
Compare writing two unrelated classes against extending one:
public class Employee
{
public string Name { get; }
public decimal MonthlySalary { get; }
public Employee(string name, decimal salary)
{ Name = name; MonthlySalary = salary; }
public decimal GetAnnualSalary() => MonthlySalary * 12;
}
public class Manager
{
public string Name { get; } // duplicated
public decimal MonthlySalary { get; } // duplicated
public decimal Bonus { get; }
public Manager(string name, decimal salary, decimal bonus)
{ Name = name; MonthlySalary = salary; Bonus = bonus; }
public decimal GetAnnualSalary() => MonthlySalary * 12 + Bonus; // duplicated formula
}
Intern, Contractor...) means writing it all a third time.public class Employee
{
public string Name { get; }
public decimal MonthlySalary { get; }
public Employee(string name, decimal salary)
{
Name = name;
MonthlySalary = salary;
}
public virtual decimal GetAnnualSalary() => MonthlySalary * 12;
}
public class Manager : Employee
{
public decimal AnnualBonus { get; }
public Manager(string name, decimal salary, decimal bonus)
: base(name, salary) => AnnualBonus = bonus;
// Overriding: enhances the base behaviour rather than replacing it
public override decimal GetAnnualSalary() =>
base.GetAnnualSalary() + AnnualBonus;
}
Manager gets Name and MonthlySalary
for free via : Employee, and its override calls
base.GetAnnualSalary() rather than reimplementing the multiplication — it enhances,
it doesn't duplicate.Inheritance is powerful, but it's not free — it's the tightest form of coupling two classes can have. We come back to exactly why later in Inheritance vs composition, and in full in the SOLID chapter's Composition over inheritance and Liskov Substitution Principle.
Polymorphism
Polymorphism means code can call an operation on an object without knowing its exact type —
and the object's own, specific implementation runs anyway. "Poly-morph" literally means "many
forms": a Shape reference might, at runtime, actually point to a Circle or a
Square, and calling shape.Area() runs whichever one is really there.
This is only possible because C# supports abstract classes (a base class that can
declare a method with no implementation, forcing every subclass to supply one) together with
virtual/override (the base marks a method overridable;
subclasses opt in to replacing it).
// The client only knows about Shape — never Circle or Square by name.
List<Shape> shapes = new()
{
new Circle { Radius = 2 },
new Square { Side = 3 },
};
double total = 0;
foreach (Shape s in shapes)
total += s.Area(); // runs Circle's Area() or Square's Area() — decided at RUNTIME
Console.WriteLine($"Total area: {total:F2}");
public abstract class Shape
{
// No implementation here — every concrete shape MUST provide one.
public abstract double Area();
}
public class Circle : Shape
{
public double Radius { get; init; }
public override double Area() => Math.PI * Radius * Radius;
}
public class Square : Shape
{
public double Side { get; init; }
public override double Area() => Side * Side;
}
The foreach loop above never says the words "Circle" or "Square." You can add a
Triangle : Shape next year and this loop doesn't change at all. That's the payoff: code
written against the abstraction keeps working as new concrete types arrive.
The slides call the runtime lookup of "which method body actually runs" dynamic binding, and note it's what makes objects with the same interface substitutable for each other — which is exactly polymorphism. This leads to one of the most quoted lines in OO design:
"Program to an interface, not an implementation." Depending on Shape (the abstraction)
instead of Circle or Square (concrete types) decouples your code from
details that are likely to change. This idea reappears as its own SOLID principle — see
Program to an interface and
Dependency Inversion.
Overloading vs. overriding
These two terms sound alike and get mixed up constantly — but they're unrelated mechanisms.
// The COMPILER picks which one to call, based on argument types —
// resolved before the program ever runs.
var calc = new Calculator();
calc.Add(1, 2); // calls Add(int,int)
calc.Add(1.0, 2.0, 3.0); // calls Add(double,double,double)
public class Calculator
{
public double Add(double a, double b) => a + b;
public double Add(double a, double b, double c) => a + b + c;
public int Add(int a, int b) => a + b;
}
// The RUNTIME type of the object decides which Area() runs —
// resolved while the program is executing.
Shape s = new Circle { Radius = 2 };
s.Area(); // runs Circle's version, even though 's' is typed as Shape
public class Shape
{
public virtual double Area() => 0;
}
public class Circle : Shape
{
public double Radius { get; init; }
public override double Area() => Math.PI * Radius * Radius;
}
| Overloading | Overriding | |
|---|---|---|
| Resolved | Compile-time | Run-time (dynamic binding) |
| Involves | One class, multiple signatures | Base + subclass, identical signature |
| Requires | Nothing special | virtual (base) + override (subclass) |
| Purpose | Convenience — same operation, different inputs | Polymorphism — same call, type-specific behaviour |
OO analysis, design & implementation
Building an OO system isn't one step — the slides break it into three:
Identify the objects the system needs and give each one responsibilities — the functions it performs. The system's purpose emerges from those responsibilities collaborating.
Identify how those objects relate to each other (association, aggregation, composition, inheritance...) and how they collaborate to satisfy requirements. Once the relationships are settled, the design is complete.
Realise the design in an OO language — C#, Java, Python. This is the step everyone thinks of as "programming," but it's the last of three.
Finding appropriate objects
There's no single recipe, and the slides are candid that experts disagree:
- Noun/verb parsing — write a problem statement in plain English, underline the nouns (candidate classes) and verbs (candidate methods).
- Responsibility-driven design — focus on what collaborations and responsibilities the system needs, and let classes fall out of that.
- Real-world modelling — translate the objects you find during analysis directly into design classes.
Strict real-world modelling has a trap: it captures today's reality but not tomorrow's.
Many of the most useful classes in a good design (a Strategy, a Factory, a
Repository) have no real-world counterpart at all — they exist purely to
keep the design flexible. Recognising design patterns is largely about spotting these
"less obvious" abstractions. See Chapter 3 onward.
Specifying object interfaces
Every operation a class exposes has a signature — its name, its parameter types, and its return type. The complete set of signatures a class exposes is its interface: the full menu of requests you're allowed to send that object.
For the
exam: a signature is name + parameter types + return type. In practice: C#'s
overload resolution only looks at the name and parameter types — the return type is not part of
what distinguishes one overload from another. You cannot declare int Add(int a, int b) and
double Add(int a, int b) in the same class; that's a compile error, even though the "full"
signature (including return type) technically differs. See the overloading
vs overriding comparison above for the working definition C# actually enforces.
Matching a request to the actual code that runs, at runtime, is dynamic binding — the
same idea you just saw as polymorphism. It's why interfaces matter so much in good design: a client
that only knows IShape can be handed any class that implements it, without
caring which one, or even knowing it changed. That's the essence of
Dependency Inversion and
Interface Segregation, covered fully in Chapter 2.
Class relationships
Objects rarely work alone — the interesting design decisions are usually about how two classes relate. The slides give four relationships, and the important nuance is that aggregation and composition are both special cases of association, distinguished only by ownership of lifetime.
| Relationship | Meaning | C# code clue | Life of the "child" |
|---|---|---|---|
| Association | Two independent objects use each other, no ownership | A stored (but unowned) reference: private Other _other; assigned from outside | Fully independent — neither controls the other's lifetime |
| Aggregation | "Has-a", weak ownership — parent uses child, doesn't control its life | Field assigned from outside: _x = passedInObject; | Independent — survives if the parent is destroyed |
| Composition | "Has-a", strong ownership — parent controls the child's whole life | Field created inside: _x = new Child(); | Owned — destroyed with the parent |
| Inheritance | "Is-a" — subclass is a specialised kind of the base | class Sub : Base | N/A — this is a type relationship, not an object relationship |
Note: association and aggregation often look identical in C# — a stored, externally-assigned reference. The difference is conceptual (does the design consider this ownership, even weak ownership?), not something the compiler enforces. And when the "child" is never stored at all — only ever a parameter or local variable — that's the weaker dependency relationship, not association; see the UML section below.
The whole spectrum in one running example, adapted from the Hospital/Department
exercise: a Hospital owns its Departments (composition) — close the
hospital and its departments are gone. A Department merely uses a
Doctor (aggregation) — close the department and the doctor is still a doctor, just
unemployed. And a Doctor and a Patient just interact for the duration of
one call (dependency, the weakest relationship of all — see above) — neither owns, nor even
stores a reference to, the other.
Association
Association is a "using" relationship between two independent objects — neither owns the other, and both keep their own lifetime. The slides' example: a doctor can see many patients, a patient can see many doctors, and deleting either one has zero effect on the other.
One more distinction worth making precisely: if the other object is only ever received as a
method parameter or local variable — never stored anywhere — that's technically the
even weaker dependency relationship (formalised in the UML section
below). The code below is exactly that case: Doctor never stores the Patient
it's handed. Genuine association usually means one object holds a reference to another (a
field), even without owning its lifetime — which is what makes aggregation and composition (both
held references) special cases of it.
public class Doctor
{
public string Name { get; }
public string Specialty { get; }
public Doctor(string name, string specialty)
{
Name = name;
Specialty = specialty;
}
// DEPENDENCY: Patient is just a parameter for the duration of this call.
// Doctor doesn't store it, doesn't own it, doesn't outlive or control it.
// (This is the weakest using-relationship — see the note above.)
public Prescription WritePrescription(Patient patient, string medication)
{
Console.WriteLine($"{Name} is writing a prescription for {patient.Name}...");
return new Prescription(Name, patient.Name, medication);
}
}
public class Patient
{
public string Name { get; }
public Patient(string name) => Name = name;
}
public record Prescription(string DoctorName, string PatientName, string Medication);
Association (and dependency, its weaker cousin above) can be one-directional — as above, only
Doctor knows about Patient — or bidirectional, where both classes hold a
reference to each other. The slides note this is a design choice, not a rule.
Aggregation — "has-a", independent lifetimes
Aggregation is association with ownership, but the owner does not control the child's
lifetime. A Department holds a list of Doctors, but each
Doctor was created somewhere else and handed in — the giveaway is a constructor or method
parameter being stored rather than created.
public class Department
{
public string Name { get; }
private readonly List<Doctor> _doctors = new();
public Department(string name) => Name = name;
// AGGREGATION: the Doctor object is created OUTSIDE and passed in.
// Department only keeps a reference — it never owns the Doctor's lifetime.
public void AssignDoctor(Doctor doctor)
{
_doctors.Add(doctor);
Console.WriteLine($"{doctor.Name} assigned to {Name} department.");
}
}
Ask yourself: "if I set this owning object to null right now, does the child object
still make sense on its own, somewhere else?" If yes — aggregation. A Doctor without a
Department is still meaningfully a doctor. A Department without a
Hospital... less so, which is why that relationship is composition, next.
Composition — owned lifetimes
Composition is association with ownership and lifetime control: the child cannot outlive the parent. If the parent is destroyed, every child it composed is destroyed with it. The giveaway is a field being constructed inside its owner, not received from outside.
public class Hospital
{
public string Name { get; }
private readonly List<Department> _departments = new();
public IReadOnlyList<Department> Departments => _departments;
public Hospital(string name) => Name = name;
// COMPOSITION: the Hospital instantiates the Department ITSELF.
// If this Hospital object goes away, so does every Department it made.
public void AddDepartment(string deptName)
{
var dept = new Department(deptName);
_departments.Add(dept);
Console.WriteLine($"Department '{deptName}' created within {Name}.");
}
}
The slides' own example is a house and its rooms: destroy the house, the rooms go with it. Control can be direct (the parent's constructor builds the child, as above) or transitive (the parent is handed an already-built child and simply takes over responsibility for destroying it) — either way, the defining fact is that the child's lifetime is bound to the parent's.
For the
exam: composition means "destroy the parent and its children are destroyed with it." In
practice: in C#, setting a reference to null (or letting it go out of scope) does
not deterministically destroy anything — the garbage collector reclaims the memory at some later,
unpredictable time, if and when it decides to. "Destroyed with the parent" is a description of the
design relationship (the child has no independent existence and nothing else should be holding a
reference to it), not a statement about when the .NET runtime frees memory. For resources that must be
released deterministically (files, sockets, database connections), the correct mechanism is
IDisposable and using, not composition — composition and disposal solve
different problems.
Step through the Hospital/Department/Doctor scenario, then "destroy" the Hospital and watch which objects the design considers to survive. (This simulates the UML ownership concept — it isn't tracking real garbage collection.)
hospital just makes it eligible for collection at some later, unpredictable time.
By the design's rules, the Cardiology Department is considered gone (composition —
it was created inside the Hospital, so nothing should keep using it once the Hospital is gone) but
Dr. Smith is considered still valid, ready to be assigned to a new department
(aggregation — he was created independently). Nothing here mentions Patient: pure
association doesn't even involve ownership, so there's nothing to test.Delegation
Delegation is the mechanism that makes composition as powerful for reuse as inheritance. Instead of inheriting behaviour from a parent class, an object holds a reference to a helper object and forwards ("delegates") the work to it. It's composition with a purpose: not just "has-a," but "has-a, and hands off the actual work to it."
From the original Design Patterns book: a window that draws itself using an internal
Rectangle object can become circular at runtime just by swapping that field for a
Circle — as long as both implement the same shape interface. Try doing that with
inheritance; you'd need to construct an entirely new object of a different class.
Watch what happens when you try to reuse behaviour across combinations using inheritance instead — the classic "class explosion":
public class CreditCardEmailPayment : Payment { /* ... */ }
public class CreditCardSmsPayment : Payment { /* ... */ }
public class PayPalEmailPayment : Payment { /* ... */ }
public class PayPalSmsPayment : Payment { /* ... */ }
// Business now wants Crypto as a payment method and Slack as a
// notification channel. That's 2 more methods x 3 notifiers =
// SIX more classes just to cover the new combinations.
var service = new PaymentService(new PayPal(), new SmsNotifier());
service.ProcessTransaction(250);
// Adding Crypto now costs exactly ONE new class: class Crypto : IPaymentMethod { ... }
public interface IPaymentMethod { string Authorize(decimal amount); }
public interface INotifier { void Send(string message); }
public class CreditCard : IPaymentMethod
{
public string Authorize(decimal amount) => $"Charged ${amount} to Credit Card.";
}
public class PayPal : IPaymentMethod
{
public string Authorize(decimal amount) => $"Charged ${amount} to PayPal account.";
}
public class EmailNotifier : INotifier
{
public void Send(string message) => Console.WriteLine($"EMAIL: {message}");
}
public class SmsNotifier : INotifier
{
public void Send(string message) => Console.WriteLine($"SMS: {message}");
}
public class PaymentService
{
private readonly IPaymentMethod _method;
private readonly INotifier _notifier;
// COMPOSITION: the service HAS-A payment method and a notifier.
public PaymentService(IPaymentMethod method, INotifier notifier)
{
_method = method;
_notifier = notifier;
}
public void ProcessTransaction(decimal amount)
{
// DELEGATION: the service doesn't know HOW to charge or notify —
// it forwards the request to whichever object it was handed.
string receipt = _method.Authorize(amount);
_notifier.Send(receipt);
}
}
PaymentService never needs to know how PayPal
authorizes a payment — only that whatever it's holding has an Authorize method. New
combinations are just new constructor arguments, not new classes.PaymentService composes two interfaces (filled-diamond composition, abbreviated here as a labelled arrow) and delegates the real work to whatever concrete class was plugged in. The dashed lines with open triangles below are realization — "implements this interface."Delegation isn't free either. The slides are candid: dynamic, highly composed software is harder to read than static software, because the actual behaviour is scattered across several small objects instead of one method body, and there's a small runtime cost per indirection. Use it "only when it simplifies more than it complicates" — which, for anything with more than two or three combinations, it usually does.
Inheritance vs. composition
You've now seen both mechanisms solve the "reuse behaviour" problem. Design literature has a strong, often-repeated opinion here:
"Favour object composition over class inheritance." This gets its own full treatment with more examples in Chapter 2 — Composition over inheritance. Here's the reasoning in brief.
| Inheritance | Composition | |
|---|---|---|
| Reuse style | "White-box" — the subclass can see and depends on the parent's internals | "Black-box" — objects only ever talk through each other's public interface |
| Bound at | Compile-time — fixed the moment you write : Base | Run-time — you can swap the composed object while the program runs |
| Encapsulation | Weakened — "inheritance breaks encapsulation," because subclass behaviour is entangled with base implementation details | Preserved — any object can be replaced by another of the same interface, with zero ripple effect |
| Result | Fewer, larger, more tightly-coupled classes | More, smaller, focused classes, wired together at runtime |
The phrase "inheritance breaks encapsulation" is worth unpacking with a concrete failure, because it's not obvious from the definition alone. This is the classic fragile base class problem:
public class LoggingList<T>
{
private readonly List<T> _items = new();
public virtual void Add(T item)
{
Console.WriteLine($"Adding: {item}");
_items.Add(item);
}
// AddRange is implemented IN TERMS OF Add today, so subclasses that
// override Add automatically see every item logged — even bulk ones.
public void AddRange(IEnumerable<T> items)
{
foreach (var item in items) Add(item);
}
}
public class AuditedList<T> : LoggingList<T>
{
public override void Add(T item)
{
Console.WriteLine($"AUDIT: {item} added at {DateTime.Now}");
base.Add(item);
}
}
Six months later, a teammate "optimizes" AddRange for performance, because looping and
calling a virtual method per item is slower than a bulk insert:
// "harmless" performance fix inside LoggingList<T>
public void AddRange(IEnumerable<T> items) => _items.AddRange(items);
AuditedList now silently stops auditing bulk inserts — nothing in its
own code changed, and nothing in the type system warned anyone. AuditedList was never
actually depending on LoggingList's public interface; it was secretly depending
on an implementation detail (that AddRange happens to call Add). That
hidden coupling is exactly what "inheritance breaks encapsulation" means.
Composition avoids this entirely: if AuditedList had instead wrapped a
List<T> and delegated to it (like PaymentService did above), there
would be no shared implementation to secretly depend on — only a public interface, which is exactly
the contract composition never lets you cheat on. This exact scenario — an inheritance hierarchy that
quietly breaks when the base class changes — is also the subject of the Liskov Substitution Principle;
see Chapter 2 for the "flightless bird" version of this bug.
UML
UML (Unified Modelling Language) is a standard pictorial language for specifying, visualising, and documenting a software design — a blueprint, drawn before (or alongside) the code. It isn't limited to software either; the slides note it's used to model business processes too.
Building blocks
UML defines its vocabulary in three layers: things (the nouns you draw), relationships (the lines connecting them — you've already learned four of these), and diagrams (named collections of things + relationships for a specific purpose).
| Category of "thing" | What it captures | Examples |
|---|---|---|
| Structural | The static, "noun" parts of the model | class, interface, use case, component, node |
| Behavioural | The dynamic, "verb" parts | interaction (messages), state machine |
| Grouping | Organises everything else | package |
| Annotational | Explanatory side-notes | note |
| Relationship | Meaning | Notation |
|---|---|---|
| Dependency | "Uses temporarily" — a change to one may affect the other, but there's no structural ownership | Dashed line, open arrowhead |
| Association | Structural "using" link (incl. aggregation/composition) | Solid line, optional diamond |
| Generalisation | "Is-a" — inheritance between classes | Solid line, open triangle |
| Realisation | "Implements" — a class fulfils an interface's contract | Dashed line, open triangle |
Generalisation vs. realisation look almost identical (both use the open triangle)
— the only difference is a solid line (extends a class) vs. a dashed line (implements an interface).
In C# terms: class Circle : Shape (abstract class) draws as generalisation;
class Circle : IShape (interface) draws as realisation.
The five core diagrams
UML defines over a dozen diagram types split into structural modelling (class, object, component, package, deployment...) and behavioural modelling (use case, sequence, activity, statechart...). The slides highlight five as the ones you'll draw constantly. Each one below models the same tiny ATM system, so you can see how the same story looks from five different angles.
Use case diagram
When to draw it: at the very start, for high-level requirements — capturing what the system must do, from the user's point of view, before any classes exist.
Class diagram
When to draw it: for the static design — which classes exist, what they know, and how they're related. This is the diagram every relationship figure in this chapter has already been.
Sequence diagram
When to draw it: to capture the time-ordered flow of messages between objects for one specific scenario — invaluable when the order of calls is exactly the thing you need to get right.
Statechart diagram
When to draw it: when an object's behaviour depends on which state it's currently in, and you need to enumerate the states and the events that move it between them.
Activity diagram
When to draw it: essentially a flowchart for a business process or algorithm — good for showing branching logic and parallel steps at a level above individual method calls.
A random pair of classes and their connecting line appears below. Read the notation and pick what relationship it represents.
Chapter quiz
Eight scenarios. Reason about ownership and lifetime before you answer — that's the skill this chapter is really teaching.