Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Design Patterns (GoF and modern)  Module 1 – Why patterns, and when not to use them

Reading UML class and sequence diagrams

How to read the UML diagrams in pattern catalogues: class boxes, the six relationship lines, multiplicities, lifelines, calls, replies and object creation.

  • Beginner
  • 25 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7 and Node.js 24.21.0
  • By MySmartCoPilot
Free: 25 minutes

What you will learn

  • Read class-diagram notation for classes, interfaces and the six relationships between them
  • Read a sequence diagram's lifelines, synchronous calls, replies and object creation
  • Translate a small class diagram into Python and Java code, and code back into a diagram
  • Explain why a diagram needs a text description and why diagrams kept as text stay in step with the code

Before you start

What you’ll learn · Before you start

What you will learn

  • Read class-diagram notation for classes, interfaces and the six relationships between them
  • Read a sequence diagram's lifelines, synchronous calls, replies and object creation
  • Translate a small class diagram into Python and Java code, and code back into a diagram
  • Explain why a diagram needs a text description and why diagrams kept as text stay in step with the code

Before you start

On this page

A UML class diagram shows the structure of a design. Each box is a class or an interface with its attributes and operations, and each line between two boxes is a relationship whose line and ends tell you which one it is: a hollow triangle for “is a kind of”, a dashed line for “implements” or “uses”, a diamond for a whole that holds its parts. A sequence diagram shows one run of that design: the objects taking part side by side at the top, time flowing down the page, and an arrow for every call and every reply. Pattern catalogues use the first kind to show a pattern’s participants and the second to show how they work together.

UML’s current formal version, UML 2.5.1 from the Object Management Group, is a long specification, but reading patterns needs only a small part of it. This lesson covers that part, draws one checkout design both ways, writes it in Python and Java, and then lets the running code draw its own sequence diagram.

The class box

A class box has up to three compartments, from top to bottom: the name, the attributes and the operations. A member’s visibility comes before its name, + for public, - for private, # for protected and ~ for package, and its type follows a colon, as in total(): int or items: LineItem[*], where [*] is a multiplicity meaning “any number”. An abstract class shows its name in italics or adds {abstract}, an interface carries the keyword «interface» above its name, and a static member is underlined.

Catalogues often leave compartments out. A box with only a name says nothing about the class’s members; it does not say that the class has none. Types are often left out too, and a pattern’s diagram usually shows only the operations that the pattern is about.

The six relationships

Six kinds of line cover every class diagram in this track. The line can be solid or dashed, and the end can carry a hollow triangle, an arrowhead or a diamond:

Six panels, one per UML relationship, each joining two classes with the line and end that mark that relationship.Generalisation: is a kind ofRealisation: implementsAssociation: refers toAggregation: holds, sharesComposition: ownsDependency: uses brieflySavedCardCardCard«interface»PaymentMethodLineItemProductCatalogueProductOrderLineItemOrderPaymentMethodsolid line,hollow triangledashed line,hollow trianglesolid line,arrowheadhollow diamondat the wholefilled diamondat the wholedashed line,arrowhead

The six relationships of a class diagram, and the line and end of each

Text description of the diagram

Six panels stacked from top to bottom. Each joins two class boxes and names the line and the end that mark the relationship.

Generalisation, "is a kind of": SavedCard is joined to Card by a solid line with a hollow triangle at Card, the more general class.

Realisation, "implements": Card is joined to the interface PaymentMethod by a dashed line with a hollow triangle at the interface.

Association, "refers to": LineItem is joined to Product by a solid line with an arrowhead at Product, the class that LineItem can reach.

Aggregation, "holds, shares": Catalogue is joined to Product by a line with a hollow diamond at Catalogue, the whole, and an arrowhead at Product.

Composition, "owns": Order is joined to LineItem by a line with a filled diamond at Order, the whole, and an arrowhead at LineItem.

Dependency, "uses briefly": Order is joined to PaymentMethod by a dashed line with an arrowhead at PaymentMethod.

Relationship Line and end Typical code
Generalisation, “is a kind of” Solid line, hollow triangle at the general class class SavedCard(Card), extends
Realisation, “implements” Dashed line, hollow triangle at the interface a subclass of an abstract base class, implements
Association, “refers to” Solid line, an arrowhead on the end you can navigate to an attribute holding an object made elsewhere
Aggregation, “holds parts that live on their own” Hollow diamond at the whole a list of objects that were passed in
Composition, “owns parts that go away with it” Filled diamond at the whole parts created inside the whole and never handed out
Dependency, “uses for a moment” Dashed line with an arrowhead a parameter, a local variable, a return type

Generalisation and realisation share the hollow triangle, which always points at the more general element, and differ only in the line. UML draws the arrowheads of associations and dependencies as open, V-shaped heads; the diagrams in this track use a plain arrowhead, and the line style still tells the two apart.

Association, aggregation and composition all mean “has a”, and the diamond says how strongly. UML 2.5.1 defines composition strictly: a part belongs to at most one composite at a time, and when the composite is deleted, its parts are deleted with it. It leaves the precise meaning of shared aggregation, the hollow diamond, to the application area and the modeller, which is why hand-drawn diagrams use it inconsistently. When you are unsure, a plain association with multiplicities is the honest choice. Multiplicities sit at the ends of a line and say how many objects take part: 1, 0..1, * (the same as 0..*) or 1..*.

One design, drawn and written

A checkout: an Order owns its LineItems, each line item refers to a Product from the shop’s catalogue, and the order takes payment through a PaymentMethod interface, which a card and a wallet implement. As a class diagram:

Class diagram: Order owns LineItems that refer to Products, and depends on the PaymentMethod interface, which two classes implement.«interface»PaymentMethod+pay(amount)strCardPayment-last4str+pay(amount)strWalletPayment-wallet_idstr+pay(amount)strOrder-itemsLineItem[*]+add(product, qty)None+items()LineItem[*]+total()int+checkout(method)strLineItem+productProduct+qtyint+subtotal()intProduct+namestr+priceintuses incheckout()1 owns 0..*0..* to 1

The checkout design as a class diagram

Text description of the diagram

Six class boxes. Each box shows the class name, then its attributes, then its operations; a plus sign marks a public member and a minus sign a private one, and the type follows each name.

Order has a private attribute items, a list of LineItem, and public operations add(product, qty), items(), total() returning int and checkout(method) returning str.

A line with a filled diamond at Order and an arrowhead at LineItem, labelled "1 owns 0..*", is a composition: one order owns zero or more line items.

LineItem has public attributes product, of type Product, and qty, an int, and an operation subtotal() returning int. A solid line with an arrowhead from LineItem to Product, labelled "0..* to 1", is an association: many line items can refer to one product.

Product has public attributes name, a str, and price, an int.

A dashed line with an arrowhead from Order to PaymentMethod, labelled "uses in checkout()", is a dependency: Order uses a payment method only as a parameter of checkout.

PaymentMethod is marked «interface» and has one operation, pay(amount), returning str. CardPayment, with a private attribute last4, and WalletPayment, with a private attribute wallet_id, each have a pay(amount) operation and are joined to PaymentMethod by a dashed line with a hollow triangle at PaymentMethod: they realise the interface.

Each line becomes one construct in Python. The interface is an abstract base class whose pay is an abstract method, so Python refuses to create a CardPayment until pay is written. The filled diamond becomes an add() that takes a product and a quantity and builds the LineItem itself, a private list, and an items() that hands out a tuple, so no caller can slip a line item in behind the order’s back. The dependency is the method parameter of checkout(): the order uses it for one call and keeps no reference to it.

The checkout in Python

Python · checkout/lifetimes.py

"""The checkout design at work, and what composition means for lifetimes, measured with weak references."""
import weakref

from order import CardPayment, Order, Product, WalletPayment

catalogue = [Product("tea", 250), Product("biscuits", 120)]  # products exist before any order

order = Order()
order.add(catalogue[0], 2)
order.add(catalogue[1], 1)
print("total:", order.total())
print(order.checkout(CardPayment("4242")))
print(order.checkout(WalletPayment("w-17")))

try:
    order.items().append("a line item made outside the order")
except AttributeError as error:
    print("items() is read-only:", type(error).__name__)

item = weakref.ref(order.items()[0])
product = weakref.ref(catalogue[0])
del order  # the last reference to the order goes away
print("line item alive after the order is gone:", item() is not None)
print("product alive after the order is gone:", product() is not None)

Output

total: 620
card ending 4242 charged 620
wallet w-17 debited 620
items() is read-only: AttributeError
line item alive after the order is gone: False
product alive after the order is gone: True

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 lifetimes.py

checkout/order.py

"""The checkout design of the class diagram: an Order composed of LineItems that refer to Products, and an Order
that depends on the PaymentMethod interface, which CardPayment and WalletPayment realise."""
from abc import ABC, abstractmethod
from dataclasses import dataclass


@dataclass(frozen=True)
class Product:
    name: str
    price: int  # in the currency's smallest unit, so that totals stay exact


class LineItem:
    """A part of exactly one Order: only Order.add() creates line items."""

    def __init__(self, product, qty):
        self.product = product  # association: the product exists without this line item
        self.qty = qty

    def subtotal(self):
        return self.product.price * self.qty


class PaymentMethod(ABC):
    """The interface: anything that can take a payment of an amount."""

    @abstractmethod
    def pay(self, amount):
        """Take the payment and return a receipt line."""


class CardPayment(PaymentMethod):  # realisation
    def __init__(self, last4):
        self._last4 = last4

    def pay(self, amount):
        return f"card ending {self._last4} charged {amount}"


class WalletPayment(PaymentMethod):  # realisation
    def __init__(self, wallet_id):
        self._wallet_id = wallet_id

    def pay(self, amount):
        return f"wallet {self._wallet_id} debited {amount}"


class Order:
    """The whole: it creates its line items, keeps them private and hands out only a copy."""

    def __init__(self):
        self._items = []  # composition: these line items live and die with this order

    def add(self, product, qty):
        self._items.append(LineItem(product, qty))

    def items(self):
        return tuple(self._items)

    def total(self):
        return sum(item.subtotal() for item in self._items)

    def checkout(self, method):
        # dependency: method is a parameter only, and the order keeps no reference to it
        return method.pay(self.total())

The last two lines measure what composition means. A weak reference watches an object without keeping it alive, and CPython frees an object as soon as the last ordinary reference to it goes. Once the order is deleted, its line item is gone too, while the product lives on, because the catalogue still holds it: the order only referred to it.

Java says the same things with its own constructs: implements for realisation, a record for the product, and a LineItem nested inside Order with a private constructor, so only the order can create one. Java’s garbage collector frees objects when it chooses, so the Java version shows ownership through access instead: items() returns List.copyOf(items), an unmodifiable copy whose add throws.

The same design in Java

java/Checkout.java

import java.util.ArrayList;
import java.util.List;

/** The checkout design of the class diagram in Java: the same classes and relationships as order.py. */
public class Checkout {

    record Product(String name, int price) {}

    /** The interface: anything that can take a payment of an amount. */
    interface PaymentMethod {
        String pay(int amount);
    }

    static final class CardPayment implements PaymentMethod { // realisation
        private final String last4;

        CardPayment(String last4) {
            this.last4 = last4;
        }

        @Override
        public String pay(int amount) {
            return "card ending " + last4 + " charged " + amount;
        }
    }

    static final class WalletPayment implements PaymentMethod { // realisation
        private final String walletId;

        WalletPayment(String walletId) {
            this.walletId = walletId;
        }

        @Override
        public String pay(int amount) {
            return "wallet " + walletId + " debited " + amount;
        }
    }

    /** The whole: it creates its line items, keeps them private and hands out only a read-only copy. */
    static final class Order {

        /** A part of exactly one Order: the constructor is private, so only Order can call it. */
        static final class LineItem {
            final Product product; // association: the product exists without this line item
            final int qty;

            private LineItem(Product product, int qty) {
                this.product = product;
                this.qty = qty;
            }

            int subtotal() {
                return product.price() * qty;
            }
        }

        private final List<LineItem> items = new ArrayList<>(); // composition

        void add(Product product, int qty) {
            items.add(new LineItem(product, qty));
        }

        List<LineItem> items() {
            return List.copyOf(items);
        }

        int total() {
            return items.stream().mapToInt(LineItem::subtotal).sum();
        }

        String checkout(PaymentMethod method) { // dependency: a parameter only
            return method.pay(total());
        }
    }

    public static void main(String[] args) {
        List<Product> catalogue = List.of(new Product("tea", 250), new Product("biscuits", 120));
        Order order = new Order();
        order.add(catalogue.get(0), 2);
        order.add(catalogue.get(1), 1);
        System.out.println("total: " + order.total());
        System.out.println(order.checkout(new CardPayment("4242")));
        System.out.println(order.checkout(new WalletPayment("w-17")));
        try {
            order.items().add(null);
        } catch (UnsupportedOperationException error) {
            System.out.println("items() is read-only: " + error.getClass().getSimpleName());
        }
    }
}

java/session.py

"""Builds and runs Checkout.java as you would in a terminal, and prints the session.

javac --release 25 compiles against the Java 25 language and API, so the code uses nothing newer than Java 25
whatever the version of the JDK. The JDK here is Homebrew's OpenJDK; its version line is printed first, without the
release date that follows it.
"""
import os
import re
import subprocess

JDK_BIN = "/opt/homebrew/opt/openjdk/bin"
env = dict(os.environ, PATH=os.pathsep.join([JDK_BIN, os.environ.get("PATH", "")]))


def run(command):
    return subprocess.run(command, shell=True, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)


version = run("java -version").stdout.splitlines()
print("#", re.sub(r"\s+\d{4}-\d{2}-\d{2}", "", version[0]) if version else "java: not found")
for command in ("javac --release 25 -d classes Checkout.java", "java -cp classes Checkout"):
    print(f"$ {command}")
    result = run(command)
    print(result.stdout, end="")
    if result.returncode != 0:
        print(f"[exit status {result.returncode}]")
        raise SystemExit(1)

Output

# openjdk version "27"
$ javac --release 25 -d classes Checkout.java
$ java -cp classes Checkout
total: 620
card ending 4242 charged 620
wallet w-17 debited 620
items() is read-only: UnsupportedOperationException

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 session.py

javac --release 25 compiles against the Java 25 language and API, whatever version the JDK itself is, so the session proves that the code needs nothing newer than Java 25. Both versions print the same total and receipts, and both refuse a line item added from outside; Python raises AttributeError because a tuple has no append, Java raises UnsupportedOperationException.

Going back from code to a diagram works the same way in reverse. A base class or an implements clause is a triangle, a stored reference is an association, an object built inside and never handed out is a composition, and a parameter used for one call is a dependency. Lifetime is the part the code shows least clearly, so look for who creates the object and who else can reach it.

Sequence diagrams

A sequence diagram shows one scenario. Each participant has a lifeline: a box at the top naming the object and its class, as order: Order, with a dashed line running down the page. Time flows downwards, and each message is an arrow from the caller’s lifeline to the receiver’s:

  • A synchronous call is a solid line with a filled arrowhead, labelled with the operation and its arguments. The caller waits for it to finish.
  • A reply is a dashed line back to the caller, labelled with the value returned. UML allows an open or a filled head, and diagrams often leave replies out when nothing interesting comes back.
  • An asynchronous message is a solid line with an open arrowhead: the sender goes on without waiting.
  • A creation is a dashed line to the new object, labelled «create». In UML the new lifeline starts at that message, lower than the others.
  • A call to itself loops back to the same lifeline.

Thin rectangles on a lifeline, the activations, show while a call is running; many diagrams leave them out. Loops and conditions need frames such as loop and alt around the messages, so a plain sequence diagram always shows one path through the code, never all of them.

Drawn by hand, a sequence diagram goes out of date the first time the code changes. This program records the calls instead: it wraps the methods of the checkout classes, runs one checkout, and prints every call and reply as D2, a text format for diagrams.

Recording the calls of one checkout

Python · checkout/trace_checkout.py

"""Records the calls that adding an item and checking out make, and prints them as a D2 sequence diagram."""
import functools

import order as design
from order import CardPayment, Order, Product

lines = ["shape: sequence_diagram"]
names = {}
caller = ["shop"]  # who is making the current call: the code at the bottom of this file starts as "shop"


def name_of(value):
    return names.get(id(value), str(value))


def lifeline(obj, name):
    names[id(obj)] = name
    lines.append(f'{name}: "{name}: {type(obj).__name__}"')


def traced(cls, method):
    original = getattr(cls, method)

    @functools.wraps(original)
    def wrapper(self, *args):
        source, target = caller[-1], name_of(self)
        shown = ", ".join(name_of(a) if id(a) in names else repr(a) if isinstance(a, str) else str(a) for a in args)
        lines.append(f'{source} -> {target}: "{method}({shown})"')
        caller.append(target)
        result = original(self, *args)
        caller.pop()
        if result is not None:  # a call that returns nothing gets no reply arrow
            lines.append(f'{target} -> {source}: "{result}" {{style.stroke-dash: 4}}')
        return result

    setattr(cls, method, wrapper)


def creating(cls, prefix):
    original = cls.__init__
    count = [0]

    @functools.wraps(original)
    def wrapper(self, *args):
        original(self, *args)
        count[0] += 1
        lifeline(self, f"{prefix}{count[0]}")
        lines.append(f'{caller[-1]} -> {prefix}{count[0]}: "«create»" {{style.stroke-dash: 4}}')

    cls.__init__ = wrapper


for cls, method in [(Order, "add"), (Order, "total"), (Order, "checkout"), (design.LineItem, "subtotal"), (CardPayment, "pay")]:
    traced(cls, method)
creating(design.LineItem, "item")

lines.append("shop")
basket = Order()
lifeline(basket, "order")
tea = Product("tea", 250)
names[id(tea)] = "tea"
basket.add(tea, 2)
card = CardPayment("4242")
lifeline(card, "card")
basket.checkout(card)
print("\n".join(lines))

Output

shape: sequence_diagram
shop
order: "order: Order"
shop -> order: "add(tea, 2)"
item1: "item1: LineItem"
order -> item1: "«create»" {style.stroke-dash: 4}
card: "card: CardPayment"
shop -> order: "checkout(card)"
order -> order: "total()"
order -> item1: "subtotal()"
item1 -> order: "500" {style.stroke-dash: 4}
order -> order: "500" {style.stroke-dash: 4}
order -> card: "pay(500)"
card -> order: "card ending 4242 charged 500" {style.stroke-dash: 4}
order -> shop: "card ending 4242 charged 500" {style.stroke-dash: 4}

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 trace_checkout.py

checkout/order.py

"""The checkout design of the class diagram: an Order composed of LineItems that refer to Products, and an Order
that depends on the PaymentMethod interface, which CardPayment and WalletPayment realise."""
from abc import ABC, abstractmethod
from dataclasses import dataclass


@dataclass(frozen=True)
class Product:
    name: str
    price: int  # in the currency's smallest unit, so that totals stay exact


class LineItem:
    """A part of exactly one Order: only Order.add() creates line items."""

    def __init__(self, product, qty):
        self.product = product  # association: the product exists without this line item
        self.qty = qty

    def subtotal(self):
        return self.product.price * self.qty


class PaymentMethod(ABC):
    """The interface: anything that can take a payment of an amount."""

    @abstractmethod
    def pay(self, amount):
        """Take the payment and return a receipt line."""


class CardPayment(PaymentMethod):  # realisation
    def __init__(self, last4):
        self._last4 = last4

    def pay(self, amount):
        return f"card ending {self._last4} charged {amount}"


class WalletPayment(PaymentMethod):  # realisation
    def __init__(self, wallet_id):
        self._wallet_id = wallet_id

    def pay(self, amount):
        return f"wallet {self._wallet_id} debited {amount}"


class Order:
    """The whole: it creates its line items, keeps them private and hands out only a copy."""

    def __init__(self):
        self._items = []  # composition: these line items live and die with this order

    def add(self, product, qty):
        self._items.append(LineItem(product, qty))

    def items(self):
        return tuple(self._items)

    def total(self):
        return sum(item.subtotal() for item in self._items)

    def checkout(self, method):
        # dependency: method is a parameter only, and the order keeps no reference to it
        return method.pay(self.total())

Rendered, that output is this diagram. Its lifeline heads all sit at the top, where UML would draw item1 lower, at its creation, and its replies are dashed with filled heads:

Sequence diagram of adding tea to an order and checking out with a card, with four lifelines and ten messages.shoporder: Orderitem1: LineItemcard: CardPaymentadd(tea, 2)«create»checkout(card)total()subtotal()500500pay(500)card ending 4242 charged 500card ending 4242 charged 500

Adding an item and checking out, as recorded while the code ran

Text description of the diagram

Four lifelines from left to right: shop, order of class Order, item1 of class LineItem, and card of class CardPayment. Time runs down the page, and each message is an arrow between two lifelines.

  1. shop calls add(tea, 2) on order.
  2. order sends «create» to item1, on a dashed line: the order creates its line item.
  3. shop calls checkout(card) on order.
  4. order calls total() on itself, drawn as an arrow that loops back to its own lifeline.
  5. order calls subtotal() on item1.
  6. item1 replies 500 to order, on a dashed line.
  7. total() returns 500 to order itself, on a dashed loop.
  8. order calls pay(500) on card.
  9. card replies "card ending 4242 charged 500" to order, on a dashed line.
  10. order replies the same receipt line to shop, on a dashed line.

add() returns nothing, so it has no reply arrow.

Read it from the top. The shop adds tea, and the order creates item1; add() returns nothing, so there is no reply. Then the shop checks out with the card. The order calls its own total(), which asks the line item for its subtotal, and finally the order calls pay(500) on the card and passes the receipt back. The class diagram said that the order depends on a payment method; the sequence diagram shows when that dependency is used, and that the order asks for the total before it pays.

Languages without classes or inheritance

The boxes and lines still apply when a language has no classes in the Java sense, but a few of them change meaning:

  • Go. An interface box maps to a Go interface, and a type implements it just by having its methods, with no declaration. The realisation arrow is then a fact the compiler checks wherever the type is used as the interface. Go has no subclassing; embedding a struct in another promotes its methods, but the outer struct is not of the inner type, so embedding is drawn as composition, never as a triangle.
  • Rust. A trait is an interface box and impl Trait for Type is a realisation. Rust has no inheritance between structs, so a Rust design has no generalisation arrows between them; default methods in a trait give some of the reuse that inheritance gives elsewhere.
  • Python protocols. A class matches a typing.Protocol by having the right methods, without naming the protocol, much like Go. The diagram shows the realisation; the code does not declare it, and a type checker confirms it.

Diagrams that stay true

A diagram is a picture, so everything it says must also exist in words for anyone who cannot see it. The first success criterion of WCAG 2.2 asks for a text alternative for all non-text content. Every diagram in this track has a description under it, and a design you share at work deserves one too, if only because a sentence such as “the order owns its line items” is harder to misread than a diamond.

Diagrams kept as text, in formats such as D2, PlantUML or Mermaid, live next to the code, change in the same commit and show their changes in a diff. A generated diagram goes one step further: the sequence diagram above is the output of the code it describes, so running the program again redraws it.

Practise: from a diagram to Python

The prompt describes a music app’s class diagram in words. Write the classes so that the code says what the diagram says. The sample tests read your code the way you read a diagram, with the abc and inspect modules, and also check that a playlist’s tracks disappear with it.

Exercise · Easy · Python

Write the classes of a class diagram in Python

A music app's class diagram, in words. Write the five classes in playlist.py so that the code says what the diagram says.

- AudioOutput is marked «interface» and has one abstract operation, +play(title: str, seconds: int): str. - Speaker and Headphones each join AudioOutput with a dashed line and a hollow triangle at AudioOutput: they realise it. Speaker().play("Raga", 185) returns "speaker: Raga (3:05)", and Headphones returns the same with headphones: in front: minutes, a colon, then two digits of seconds. - Track has +title: str and +seconds: int. - Playlist joins Track with a filled diamond at Playlist and the multiplicity 1 to 0..*. Its operations are +add(title: str, seconds: int), +tracks(): tuple (the tracks in the order they were added) and +duration(): int (the total seconds). - Player joins AudioOutput with a dashed line and an arrowhead at AudioOutput, and has one operation, +play_all(playlist, output): list, which returns what output.play() returns for each track, in order.

The tests read your classes the way you read a diagram: with Python's abc and inspect modules they check that AudioOutput is abstract, which classes realise it, the parameters of every operation, that a playlist creates its own tracks and that they disappear with it, and that a Player keeps no output of its own. They also call every operation, with an output class of their own.

Starter code · playlist.py

class AudioOutput:
    """«interface»: one abstract operation, play(title, seconds) -> str."""


class Speaker:
    """Realises AudioOutput: "speaker: <title> (m:ss)"."""


class Headphones:
    """Realises AudioOutput: "headphones: <title> (m:ss)"."""


class Track:
    """A title and a length in seconds."""


class Playlist:
    """Owns its tracks (composition): add(title, seconds), tracks(), duration()."""


class Player:
    """Uses an AudioOutput only as a parameter (dependency): play_all(playlist, output)."""
The sample tests · test_playlist.py
import gc
import inspect
import weakref

from playlist import AudioOutput, Headphones, Player, Playlist, Speaker, Track


def params(function):
    return list(inspect.signature(function).parameters)


def test_audio_output_is_an_interface():
    """«interface»: an abstract class whose abstract operation is play(title, seconds)"""
    assert inspect.isabstract(AudioOutput), "AudioOutput must be an abstract class (abc.ABC with an abstract method)"
    assert set(AudioOutput.__abstractmethods__) == {"play"}
    assert params(AudioOutput.play) == ["self", "title", "seconds"]


def test_speaker_and_headphones_realise_it():
    """the dashed lines with hollow triangles: two concrete classes of the interface"""
    for cls in (Speaker, Headphones):
        assert issubclass(cls, AudioOutput), f"{cls.__name__} must realise AudioOutput"
        assert not inspect.isabstract(cls), f"{cls.__name__} must implement play"
    assert Speaker().play("Raga", 185) == "speaker: Raga (3:05)"
    assert Headphones().play("Thillana", 59) == "headphones: Thillana (0:59)"
    assert Speaker().play("Alap", 600) == "speaker: Alap (10:00)"


def test_playlist_creates_its_own_tracks():
    """the filled diamond: add() takes a title and seconds, never a Track made outside"""
    assert params(Playlist.add) == ["self", "title", "seconds"]
    playlist = Playlist()
    playlist.add("Raga", 185)
    playlist.add("Thillana", 59)
    tracks = playlist.tracks()
    assert isinstance(tracks, tuple), "tracks() returns a tuple, so callers cannot change the playlist through it"
    assert [(t.title, t.seconds) for t in tracks] == [("Raga", 185), ("Thillana", 59)]
    assert all(isinstance(t, Track) for t in tracks)
    assert playlist.duration() == 244


def test_tracks_disappear_with_their_playlist():
    """composition: when the playlist is gone, its tracks are gone too"""
    playlist = Playlist()
    playlist.add("Raga", 185)
    track = weakref.ref(playlist.tracks()[0])
    del playlist
    gc.collect()
    assert track() is None, "a track outlived its playlist: is something else keeping a reference to it?"


def test_player_only_depends_on_the_output():
    """the dashed arrow: the output is a parameter of play_all, never an attribute of the player"""
    assert params(Player.__init__) in (["self"], ["self", "args", "kwargs"]), "Player() takes no output"
    assert params(Player.play_all) == ["self", "playlist", "output"]

    class Recorder(AudioOutput):
        def play(self, title, seconds):
            return f"recorded {title}"

    playlist = Playlist()
    playlist.add("Raga", 185)
    playlist.add("Thillana", 59)
    player = Player()
    assert player.play_all(playlist, Recorder()) == ["recorded Raga", "recorded Thillana"]
    assert player.play_all(playlist, Headphones()) == ["headphones: Raga (3:05)", "headphones: Thillana (0:59)"]
    assert vars(player) == {}, "a Player keeps no output (or anything else) between calls"
A hint

Each kind of line becomes one construct. The interface is a class that inherits from abc.ABC with play marked @abstractmethod; a hollow triangle on a dashed line means a subclass of it. The filled diamond means the playlist makes its tracks itself: add(title, seconds) builds the Track and keeps it in a private list, so no Track is passed in from outside. The dashed arrow from Player means output is only a parameter of play_all, never an attribute. For the time, divmod(seconds, 60) gives the minutes and the seconds, and f"{s:02d}" pads to two digits.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Python Online Compiler Run the checkout examples and change the design in the browser.

Check yourself

9 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 9 A solid line runs from SavedCard to Card and ends in a hollow triangle at Card. What does the diagram say?

    Choose one answer.

    Show the answer to question 1

    Answer: SavedCard is a kind of Card (generalisation)

    A hollow triangle always points at the more general element. On a solid line it is generalisation, a subclass and its superclass; on a dashed line it would be realisation, a class and the interface it implements.

  2. Question 2 of 9 A line has a filled diamond at Order and reaches LineItem. Which statement must be true of the code?

    Choose one answer.

    Show the answer to question 2

    Answer: A line item belongs to one order at a time, and it goes away when its order does

    The filled diamond is composition. UML 2.5.1 requires a part to be in at most one composite at a time and deletes the parts with the composite, so in code the order creates its line items and never hands them out for keeps.

  3. Question 3 of 9 Why is a plain association often a safer choice than the hollow diamond of aggregation?

    Choose one answer.

    Show the answer to question 3

    Answer: UML leaves the precise meaning of shared aggregation to the modeller, so readers interpret it differently

    The specification says the semantics of shared aggregation vary by application area and modeller. A plain association with multiplicities says "refers to" without promising anything about lifetimes that the code may not keep.

  4. Question 4 of 9 Which relationship from Report to Writer does this code show?

    Read the code, then choose one answer.

    class Report:
        def __init__(self, text):
            self.text = text
    
        def export(self, writer):
            writer.write(self.text)
    Show the answer to question 4

    Answer: Dependency, a dashed line with an arrowhead at Writer

    The writer is only a parameter of one method: Report neither stores it nor creates it. That brief use is a dependency. Had Report kept the writer in an attribute, it would be an association.

  5. Question 5 of 9 Which relationship from Invoice to InvoiceLine does this code show?

    Read the code, then choose one answer.

    class Invoice:
        def __init__(self):
            self._lines = []
    
        def add_line(self, text, amount):
            self._lines.append(InvoiceLine(text, amount))
    
        def lines(self):
            return tuple(self._lines)
    Show the answer to question 5

    Answer: Composition, because the invoice creates its lines and hands out only a copy of the list

    add_line takes the parts' data, not the parts, and builds each InvoiceLine itself; lines() returns a tuple, so no caller can slip a line in. The lines live and die with the invoice: composition.

  6. Question 6 of 9 Which relationship from Team to the members does this code show?

    Read the code, then choose one answer.

    class Team:
        def __init__(self, members):
            self.members = list(members)  # people who may be in other teams too
    Show the answer to question 6

    Answer: Aggregation or a plain association, because the members exist on their own and can be shared

    The members were created elsewhere and passed in, and they outlive the team. A list attribute alone does not make composition; what matters is who creates the parts and whether they die with the whole.

  7. Question 7 of 9 In a sequence diagram, what is a dashed line from card back to order, labelled with a receipt line?

    Choose one answer.

    Show the answer to question 7

    Answer: The reply to the call order made on card, carrying its return value

    Replies are dashed and point back at the caller, labelled with what the call returned. Calls are solid. A dashed arrow labelled «create» would be the creation of the object it points at.

  8. Question 8 of 9 A Go struct Logger embeds a Writer struct, so Writer's methods can be called on a Logger. How do you draw it?

    Choose one answer.

    Show the answer to question 8

    Answer: As composition, because a Logger holds a Writer, and a Logger is not of the Writer type

    Embedding promotes the inner type's methods, but Go has no subclassing: a Logger cannot be passed where a Writer struct is expected. Drawing a hollow triangle would promise a substitution the language does not allow.

  9. Question 9 of 9 What does # in front of an attribute name in a class box mean?

    Choose one answer.

    Show the answer to question 9

    Answer: Protected, visible to the class and its subclasses

    The visibility markers are + public, - private, # protected and ~ package. A static member is underlined instead.

Interview questions

Fresher · What is the difference between aggregation and composition in a UML class diagram?

Both say that a whole holds parts; the difference is ownership and lifetime. Composition, a filled diamond at the whole, means the whole owns its parts: a part belongs to at most one whole at a time, and when the whole is deleted, its parts go with it. In code the whole usually creates its parts and never hands them out, as an order creates its line items. Aggregation, a hollow diamond, means the whole holds parts that exist on their own and may be shared, such as a team whose members also belong to other teams. UML 2.5.1 leaves the exact meaning of aggregation to the modeller, so many teams draw a plain association instead. To choose, ask one question: when the whole goes away, should the parts go too? If they should, it is composition.

Key takeaways

  • A class box lists a class’s name, attributes and operations, with +, -, # and ~ for visibility and «interface» or italics for interfaces and abstract classes.
  • Six lines cover the patterns: a hollow triangle on a solid line is generalisation, on a dashed line realisation; a filled diamond is composition, a hollow one aggregation; a solid line is an association and a dashed arrow a dependency.
  • Composition is about lifetime: the whole creates its parts, keeps them, and they go away with it.
  • A sequence diagram shows one scenario: lifelines across the top, time downwards, solid calls and dashed replies.
  • Keep diagrams as text next to the code, and give every diagram a text description.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.