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.
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:
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:
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.
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()) Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
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.
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.
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()) Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
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:
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.
- shop calls add(tea, 2) on order.
- order sends «create» to item1, on a dashed line: the order creates its line item.
- shop calls checkout(card) on order.
- order calls total() on itself, drawn as an arrow that loops back to its own lifeline.
- order calls subtotal() on item1.
- item1 replies 500 to order, on a dashed line.
- total() returns 500 to order itself, on a dashed loop.
- order calls pay(500) on card.
- card replies "card ending 4242 charged 500" to order, on a dashed line.
- 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 Typeis 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.Protocolby 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.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
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.
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.
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
- Unified Modeling Language (UML) specification, version 2.5.1 (Object Management Group)
- abc: Abstract Base Classes (Python Software Foundation)
- inspect: Inspect live objects (Python Software Foundation)
- weakref: Weak references (Python Software Foundation)
- Data model: objects, values and types (reference counting in CPython) (Python Software Foundation)
- typing.Protocol (Python Software Foundation)
- The javac command: --release (Oracle)
- List.copyOf (Java SE 25 API) (Oracle)
- Effective Go: Interfaces and Embedding (The Go Authors)
- The Rust Programming Language: Characteristics of Object-Oriented Languages (The Rust Project)
- Understanding Success Criterion 1.1.1: Non-text Content (WCAG 2.2) (W3C)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress