CHAPTER 1 · FOUNDATIONS

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.

🧠 Why this matters

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.

Airplane.cs (simulator context)
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;
    }
}
Airplane.cs (booking context)
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."

🌍 Real-world analogy

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:

❌ Naive: public field
var acc = new BankAccount();
acc.Balance = -500; // perfectly legal, and now the account is broken

public class BankAccount
{
    public decimal Balance;
}
Any code, anywhere, can set Balance to anything. There's no single place to enforce "balance can't go negative" — you'd have to trust every caller forever.
✅ Encapsulated: private field + property
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;
    }
}
The only doors in are 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.

ModifierVisible toTypical use
privateOnly this classInternal state — the default choice for fields
protectedThis class + subclassesState a subclass legitimately needs to extend behaviour
internalSame assembly (project)Implementation details shared across your own codebase, hidden from consumers
publicEveryoneThe deliberate, minimal interface to the outside world
🔀 Easily confused

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:

❌ Naive: duplicate everything
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
}
Every field and every line of shared logic exists twice. Fix a bug in the annual-salary formula and you must remember to fix it in both places — and the next role (Intern, Contractor...) means writing it all a third time.
✅ With inheritance
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.
Employee + GetAnnualSalary() Manager + GetAnnualSalary() override is-a
Figure: inheritance — a solid line with an open triangle pointing at the base class.
⚠️ Watch out

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).

Shape.cs
// 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;
}
🧠 Why this matters

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:

ℹ️ From the slides

"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.

Overloading — same name, different parameters
// 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;
}
Same class, same method name, different signatures. Nothing to do with inheritance.
Overriding — same signature, subclass replaces it
// 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;
}
Base class and subclass, identical signature. This is what makes polymorphism work.
OverloadingOverriding
ResolvedCompile-timeRun-time (dynamic binding)
InvolvesOne class, multiple signaturesBase + subclass, identical signature
RequiresNothing specialvirtual (base) + override (subclass)
PurposeConvenience — same operation, different inputsPolymorphism — same call, type-specific behaviour

OO analysis, design & implementation

Building an OO system isn't one step — the slides break it into three:

Analysis

Identify the objects the system needs and give each one responsibilities — the functions it performs. The system's purpose emerges from those responsibilities collaborating.

Design

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.

Implementation

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:

⚠️ Watch out

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.

🎓 Lecture view vs reality

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.

RelationshipMeaningC# code clueLife of the "child"
AssociationTwo independent objects use each other, no ownershipA stored (but unowned) reference: private Other _other; assigned from outsideFully independent — neither controls the other's lifetime
Aggregation"Has-a", weak ownership — parent uses child, doesn't control its lifeField assigned from outside: _x = passedInObject;Independent — survives if the parent is destroyed
Composition"Has-a", strong ownership — parent controls the child's whole lifeField created inside: _x = new Child();Owned — destroyed with the parent
Inheritance"Is-a" — subclass is a specialised kind of the baseclass Sub : BaseN/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.

Hospital Department Doctor Patient composition 1..* aggregation 0..* dependency writes prescription for
Figure: composition, aggregation and dependency, side by side — filled diamond (composition), open diamond (aggregation), dashed line with open arrowhead (dependency — Doctor uses Patient only for the duration of one call, never stores it). The diamond always sits at the "owner" end.

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.

Doctor.cs
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.

Department.cs
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.");
    }
}
💡 Tip

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.

Hospital.cs
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.

🎓 Lecture view vs reality

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.

Try it: the lifetime simulator

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.)

What to notice: click the steps in order. This simulator models the design-level ownership rule, not real garbage collection — in an actual C# process, nulling out 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."

🌍 Real-world analogy

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":

❌ Naive: one subclass per combination
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.
N payment methods × M notification types = N×M classes. Every new payment method or notifier multiplies the whole hierarchy.
✅ Composition + delegation
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 «interface» IPaymentMethod «interface» INotifier composes composes CreditCard PayPal EmailNotifier SmsNotifier
Figure: 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."
⚠️ Watch out

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:

ℹ️ From the slides

"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.

InheritanceComposition
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 atCompile-time — fixed the moment you write : BaseRun-time — you can swap the composed object while the program runs
EncapsulationWeakened — "inheritance breaks encapsulation," because subclass behaviour is entangled with base implementation detailsPreserved — any object can be replaced by another of the same interface, with zero ripple effect
ResultFewer, larger, more tightly-coupled classesMore, 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:

LoggingList.cs — looks completely safe today
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);
    }
}
⚠️ The change request that breaks it

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 capturesExamples
StructuralThe static, "noun" parts of the modelclass, interface, use case, component, node
BehaviouralThe dynamic, "verb" partsinteraction (messages), state machine
GroupingOrganises everything elsepackage
AnnotationalExplanatory side-notesnote
RelationshipMeaningNotation
Dependency"Uses temporarily" — a change to one may affect the other, but there's no structural ownershipDashed line, open arrowhead
AssociationStructural "using" link (incl. aggregation/composition)Solid line, optional diamond
Generalisation"Is-a" — inheritance between classesSolid line, open triangle
Realisation"Implements" — a class fulfils an interface's contractDashed line, open triangle
Shape Circle Square
Figure: generalisation (inheritance) — solid line, open triangle at the parent.
«interface» IShape Circle Square
Figure: realisation (implements) — dashed line, open triangle at the interface.
🔀 Easily confused

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.

Customer Withdraw Cash Check Balance Deposit Funds
Figure: use case diagram — an actor and the functionality (use cases) they trigger.

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.

ATM Card Account reads 1 0..1 linked to 1 1
Figure: class diagram — classes, their associations, and multiplicities.

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.

Customer ATM Bank InsertCard() ValidatePin(pin) approved DispenseCash(amount)
Figure: sequence diagram — dashed lifelines, solid arrows for calls, dashed arrows for returns, read top-to-bottom.

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.

Idle CardInserted DispensingCash insert card PIN ok cash dispensed / card returned
Figure: statechart — states as rounded boxes, transitions labelled with the triggering event.

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.

Enter PIN PIN valid? Dispense cash Show error yes no
Figure: activity diagram — start node, actions, a decision diamond, and labelled branches.
Try it: UML relationship reader

A random pair of classes and their connecting line appears below. Read the notation and pick what relationship it represents.

Score: 0 / 0. What to notice: the diamond always sits at the owner end (filled = composition, open = aggregation); the triangle always points at the parent/interface end (solid line = inheritance, dashed line = implements); a plain arrow with neither means association.

Chapter quiz

Eight scenarios. Reason about ownership and lifetime before you answer — that's the skill this chapter is really teaching.