Build programs from several cooperating classes with inheritance, overriding and overloading, abstract classes and interfaces, composition and aggregation, and the singleton, factory and observer design patterns, in Python and Java.
Each is in the Computer Science glossary, with every other term in the subject.
A class that cannot be instantiated, used as a superclass to share code and to declare methods that its subclasses must provide.
A method declared without a body in an abstract class or interface, which every concrete subclass must override with an implementation.
The principle of exposing what an object can do through a set of method declarations while hiding how each class carries out those methods.
A weak has-a relationship in which the containing object holds references to component objects that are created outside it, can be shared or moved, and continue to exist when it is destroyed.
The tree of classes formed by inheritance, with the most general superclass at the root and increasingly specialised subclasses below it.
A strong has-a relationship in which the containing object creates and owns its component objects, which cannot exist independently of it and are destroyed with it.
A class that implements all of its methods, including every abstract method it inherits, and so can be instantiated.
With a free account, Augment remembers where you stopped and what you have practiced.
The degree to which one class depends on the internal details of another, so that a change to one forces changes to the other.
The level of access Java gives a member declared with no access modifier, which lets it be used only by classes in the same package.
A general, reusable description of how to arrange classes and objects to solve a problem that recurs in software design.
Polymorphism in which the method to run is chosen while the program runs, from the actual class of the object that receives the call, through method overriding.
A design pattern in which a method or class creates and returns objects, choosing the concrete class from its input, so the code that asks for an object depends only on a superclass or interface and never names the concrete classes.
A relationship in which an object of one class contains or holds a reference to one or more objects of another class as part of its state.
A named set of method declarations, with no instance variables, that a class promises to implement in full, so objects of unrelated classes can be used through the same methods.
The relationship between a subclass and its superclass, in which every object of the subclass is also an object of the superclass.
Defining several methods (or constructors) with the same name in one class but different parameter lists, so the version called depends on the arguments supplied.
Defining a method in a subclass with the same signature as a method it inherits, so that objects of the subclass run the new version instead of the inherited one.
The name of a method together with the number, types and order of its parameters, which a compiler uses to tell methods apart.
A design pattern in which one object, the subject, keeps a list of observer objects that share a common interface, and calls a method on each of them whenever its state changes, so observers can be added or removed without changing the subject.
Giving an operator such as +, < or == a meaning for objects of a user-defined class, by defining the method the language calls for that operator.
A level of access that lets a member be used by its own class, by every subclass of that class, and (in Java) by every class in the same package.
A design pattern that ensures a class has only one instance and gives global access to it, through a private constructor and a static method that creates the instance on first use and returns it on every call.
Polymorphism in which the version of a method to run is chosen before the program runs, by the compiler, from the number and types of the arguments, through method overloading.
A class that inherits the attributes and methods of a superclass and may add new members or replace inherited methods with its own versions.
A class whose attributes and methods are inherited by one or more other classes.
A statement inside a subclass constructor that runs the superclass's constructor, so that the inherited attributes are initialised before the subclass adds its own.
Explain how inheritance creates a parent–child relationship between classes and promotes code reuse (HL only).
Apply inheritance in code: write subclasses that call the superclass constructor, add attributes and methods, and respect private, protected, default and public access (HL only).
Construct code that models dynamic polymorphism through method overriding (HL only).
Construct code that models static polymorphism through method overloading in Java, and its Python equivalents (HL only).
Explain the concept of abstraction in OOP, using abstract classes and interfaces to set a common interface for subclasses (HL only).
Explain the role of composition and aggregation in class relationships, and show both in a UML class diagram (HL only).
Explain the singleton, factory and observer design patterns: the problem each solves, its structure and its drawbacks (HL only).
Real programs are rarely one class. A vehicle-hire system needs cars, vans and e-bikes that share most of their behaviour but differ in a few rules, depots that hold vehicles, a single price list that every part of the program reads, and screens that update when a vehicle comes back. Object-oriented programming gives a small set of tools for relating classes to each other: one class can extend another, many classes can answer the same method call in their own way, a class can promise behaviour it leaves to others, and objects can be built out of other objects. Recurring arrangements of these tools have names, called design patterns, so developers can describe a whole design in a word.
Subtopic B3.1, Fundamentals of OOP for a single class, introduced classes, objects, encapsulation, and the ideas of inheritance and polymorphism in outline. Writing those ideas as code needs the tools for relating classes. A hypothetical vehicle-hire company serves as the running example: each new idea adds a class or a relationship to its program, written in both Python and Java.
Many classes in a program describe things that are special cases of something more general. A car, a van and an e-bike are all vehicles: each has an identifier, a daily rate and an availability flag, and each can work out the cost of a hire. Writing those attributes and methods three times would triple the code to test and to fix. Inheritance lets one general class hold the shared members and lets each special class add only what makes it different.
The general class is written once, and each special class names it as its parent. In Java the keyword is extends (class Car extends Vehicle); in Python the parent goes in brackets after the class name (class Car(Vehicle):). Every object of the child class then has the parent's attributes and can call the parent's methods as if they were its own.
A subclass is a more specific kind of its superclass, so an object of the subclass can be used wherever an object of the superclass is expected. A Car can be stored in a list of vehicles and passed to any method that takes a Vehicle. The reverse is not true: a general vehicle is not necessarily a car, so it cannot be used where a car is needed.
The "is a" sentence is the test for whether inheritance is the right relationship at all. If the sentence sounds wrong, the classes are related in some other way, usually because one object contains or uses the other.
The first version of the hire system has a superclass Vehicle and a subclass Car. Vehicle stores the identifier, the daily rate and whether the vehicle is available, and works out the cost of a hire. Car adds the number of seats and a method fits that only cars need. The program creates one car and calls methods it inherited as well as the ones it added.
class Vehicle:
def __init__(self, vehicle_id, daily_rate):
self.__vehicle_id = vehicle_id # private: only Vehicle's own methods use it
self._daily_rate = daily_rate # protected: Vehicle and its subclasses
self.__available = True
def get_vehicle_id(self):
return self.__vehicle_id
def is_available(self):
return self.__available
def set_available(self, available):
self.__available = available
def ():
._daily_rate * days
():
():
().__init__(vehicle_id, daily_rate)
.__seats = seats
():
.__seats
():
passengers <= .__seats
car = Car(, , )
(car.get_vehicle_id())
(car.cost_for())
(car.fits())
car.set_available()
(car.is_available())
((car, Car), (car, Vehicle))class Vehicle {
private String vehicleId; // only Vehicle's own methods use it
protected double dailyRate; // Vehicle, its subclasses and its package
private boolean available;
public Vehicle(String vehicleId, double dailyRate) {
this.vehicleId = vehicleId;
this.dailyRate = dailyRate;
this.available = true;
}
public String getVehicleId() {
return vehicleId;
}
{
available;
}
{
.available = available;
}
{
dailyRate * days;
}
}
{
seats;
{
(vehicleId, dailyRate);
.seats = seats;
}
{
seats;
}
{
passengers <= seats;
}
}
{
{
(, , );
System.out.println(car.getVehicleId());
System.out.println(car.costFor());
System.out.println(car.fits());
car.setAvailable();
System.out.println(car.isAvailable());
System.out.println((car Car) + + (car Vehicle));
}
}Both versions print the same results (Python writes True and False with capitals, Java true and false).
C-101
135.0
False
False
True TrueThe car object answers get_vehicle_id, cost_for, set_available and is_available although Car defines none of them: the methods come from Vehicle. The last line confirms the is-a relationship, because car is an instance of Car and also of Vehicle. Car declares only three members of its own (seats, get_seats and fits); everything else a car can do is reused from Vehicle.
A subclass object contains the superclass's attributes as well as its own, so all of them must be given values when it is created. The subclass constructor sets its own attributes, but the superclass's attributes belong to the superclass, and some of them are private. The safe way to set them is to ask the superclass's own constructor to do it.
The two languages differ in what happens when the call is left out. Java inserts an invisible super(); with no arguments at the start of every constructor that does not call super(...) or this(...) itself. If the superclass has no constructor without parameters, the program does not compile. Python inserts nothing: the superclass's __init__ simply never runs, the inherited attributes are never created, and the error appears later, when a method tries to read one of them.
class Car extends Vehicle {
private int seats;
public Car(String vehicleId, int seats) {
this.seats = seats; // no super(...) call
}
}With Car in its own file, Car.java, the compiler rejects the constructor before the program can run. The hidden super(); asks for a Vehicle constructor with no parameters, and the only one available needs a String and a double.
Car.java:4: error: constructor Vehicle in class Vehicle cannot be applied to given types;
public Car(String vehicleId, int seats) {
^
required: String,double
found: no arguments
reason: actual and formal argument lists differ in lengthThe Python equivalent runs without complaint until get_vehicle_id reads self.__vehicle_id, an attribute that was never created.
class Car(Vehicle):
def __init__(self, vehicle_id, seats):
self.__seats = seats # no super().__init__(...) call
car = Car("C-101", 5)
print(car.get_vehicle_id())The program fails only when that line runs.
AttributeError: 'Car' object has no attribute '_Vehicle__vehicle_id'Constructors themselves are not inherited in Java. A subclass that declares no constructor receives a default one with no parameters, whose only statement is the hidden super();, and that fails to compile whenever the superclass has no constructor without parameters. This is why EBike in later listings has a constructor that does nothing but call super(vehicleId, dailyRate). A Python subclass with no __init__ uses its superclass's __init__ unchanged, so the Python EBike needs none.
A subclass can itself have subclasses. An electric car is a car, and a car is a vehicle, so ElectricCar can extend Car, inheriting everything Car and Vehicle define. The classes form a tree with the most general class at the top.
A UML class diagram shows inheritance as a solid line from the subclass to the superclass, ending in a hollow triangle that touches the superclass. Several subclasses usually share one triangle. The hierarchy diagram of the hire system shows Vehicle with its three subclasses; the visibility symbols are + public, - private and # protected, and the static constants are underlined.

The subclass boxes list only what is new or replaced. Car does not repeat costFor, because it uses the inherited version; Van and EBike list costFor because they replace it with pricing rules of their own.
YouTubeA subclass inherits every attribute of its superclass, but it cannot necessarily use every one by name. Subtopic B3.1 introduced the access modifiers private and public. Inheritance brings in two more levels, which only make sense once there are subclasses and groups of classes.
A Java package is a named group of related classes, usually one folder of source files, such as java.util. If a member has no access modifier at all, Java gives it the package level.
The four levels nest inside each other, from the most restricted to the least. The access table shows where each member of Vehicle can be used.
Modifier | Same class | Same package | Subclass in another package | Any other class |
|---|---|---|---|---|
| Yes | No | No | No |
default, no keyword ( | Yes | Yes | No | No |
| Yes | Yes | Yes | No |
| Yes | Yes | Yes | Yes |
Java's rules for class members (Java Language Specification, section 6.6).
These rules are checked by the Java compiler, so breaking them stops the program from compiling at all. In a test program, Vehicle is in a package fleet and has one member at each level; Minibus (a subclass) and Report (not a subclass) are in a different package, app.
package fleet;
public class Vehicle {
private String vehicleId = "C-101"; // Vehicle only
String depotCode = "HBR"; // default: package fleet only
protected double dailyRate = 45.0; // package fleet and subclasses anywhere
public boolean available = true; // everywhere
}In the package app, the subclass Minibus and the unrelated class Report each try to use the members of Vehicle. The two classes sit in separate files, app/Minibus.java and app/Report.java.
package app;
import fleet.Vehicle;
public class Minibus extends Vehicle {
public void show() {
System.out.println(dailyRate); // allowed: protected, inherited by a subclass
System.out.println(available); // allowed: public
System.out.println(depotCode); // error: default access, other package
System.out.println(vehicleId); // error: private
}
}
// ---- in the file app/Report.java ----
package app;
import fleet.Vehicle;
public class Report {
public void show(Vehicle v) {
System.out.println(v.available); // allowed: public
System.out.println(v.dailyRate); // error: protected, and Report is not a subclass
}
}The compiler accepts every line except three, and its messages name the rule that each one breaks.
app/Minibus.java:9: error: depotCode is not public in Vehicle; cannot be accessed from outside package
app/Minibus.java:10: error: vehicleId has private access in Vehicle
app/Report.java:8: error: dailyRate has protected access in VehicleMinibus can read dailyRate because it is a subclass, but not depotCode, because subclassing does not give access to package members from another package. Report cannot read dailyRate, because it is neither a subclass nor in the package. A private attribute such as vehicleId is still inherited: every Minibus object holds a vehicleId. Only the subclass's code is barred from naming it, so it reads the value through the public accessor getVehicleId().
Python has no access modifiers, so it relies on naming conventions that the interpreter does not enforce. A single leading underscore (self._daily_rate) means "protected: for this class and its subclasses". Two leading underscores (self.__vehicle_id) trigger name mangling: inside class Vehicle the name is stored as _Vehicle__vehicle_id. Code in a subclass that writes self.__vehicle_id is looking for _Car__vehicle_id, which does not exist, so mangling has the side effect of stopping subclasses from using the name by accident.
class Car(Vehicle):
def label(self):
return self.__vehicle_id + " (car)" # tries to read the private attribute
car = Car("C-101", 45.0)
print(car._daily_rate) # protected by convention only: allowed
print(car._Vehicle__vehicle_id) # the mangled name still works
print(car.label())The first two print calls succeed and the third fails.
45.0
C-101
AttributeError: 'Car' object has no attribute '_Car__vehicle_id'. Did you mean: 'get_vehicle_id'?The first two lines show that nothing is truly hidden in Python: any code can read _daily_rate, and the mangled name opens the "private" attribute too. The protection comes from programmers following the convention, whereas Java's comes from the compiler refusing to build the program. Both languages reach the same design, in which subclasses use the superclass's public methods for its private data.
The Java class Member sits in the package club; its subclass GoldMember sits in a different package, app.
package club;
public class Member {
private String password = "oak-2027";
String homeClub = "Oakfield";
protected int points = 120;
public String name = "Ana";
}GoldMember tries to print each of the four attributes it inherits.
package app;
import club.Member;
public class GoldMember extends Member {
public void show() {
System.out.println(name); // line A
System.out.println(points); // line B
System.out.println(homeClub); // line C
System.out.println(password); // line D
}
}Inheritance brings three main benefits. Code written once in the superclass is reused by every subclass, which shortens programs and means a bug fixed in Vehicle is fixed for every vehicle. Behaviour stays consistent, because every subclass starts from the same tested methods. New kinds of object can be added by writing one small subclass, without changing the classes that already work, which makes a design extensible.
The same link that gives these benefits also causes problems. A subclass depends on the details of its superclass, so a change in the superclass (a renamed method, a different rule in cost_for) can silently change or break every subclass; this is often called the fragile base class problem. Deep hierarchies of five or six levels become hard to follow, because the code that runs for one method call may be spread across several files. Inheritance is also fixed when the program is written: an object cannot change its class while running, so a car that becomes a van-like vehicle needs a new object.
The two languages also differ in how many parents a class may have. A Java class extends exactly one class (single inheritance), and every class without extends extends java.lang.Object. A Python class may list several superclasses (multiple inheritance), and Python searches them in a fixed order, its method resolution order, to find a method. Multiple inheritance is flexible but can be confusing when two parents define the same method; Java avoids that case for classes and offers interfaces instead.
Subtopic B3.1 described polymorphism as one method call producing different behaviour depending on the object that receives it. In code this happens in two ways, which differ in when the choice of method is made: while the program runs, or before it runs.
Both forms depend on how a language tells two methods apart. Java identifies a method by its name together with the number, order and types of its parameters. The return type and the parameter names are not part of this identity.
Python identifies a method by its name alone, because its parameters have no declared types. That single difference explains why the two languages handle overloading so differently.
A subclass often needs to do an inherited job differently. Vans carry a fixed cleaning fee on top of the daily rate, and e-bike hires are capped at 50.00 however long they last. The subclass writes a method with the same signature as the inherited one, and its version replaces the superclass version for objects of that subclass.
An overriding method can replace the inherited behaviour entirely, as EBike.cost_for does, or extend it by calling the superclass version and adding to the result, as Van.cost_for does with super().cost_for(days) in Python and super.costFor(days) in Java. Extending is usually better when the rule really is "the normal cost plus something", because the normal cost is still calculated in one place. In Java, the annotation @Override asks the compiler to check that the method really does override one in the superclass; a misspelt name such as costfor then causes a compile error instead of quietly creating a second method.
class Vehicle:
def __init__(self, vehicle_id, daily_rate):
self.__vehicle_id = vehicle_id
self._daily_rate = daily_rate
def get_vehicle_id(self):
return self.__vehicle_id
def cost_for(self, days):
return self._daily_rate * days
def describe(self):
return + .__vehicle_id
():
():
().__init__(vehicle_id, daily_rate)
.__seats = seats
():
+ .get_vehicle_id() + + (.__seats) +
():
CLEANING_FEE =
():
().__init__(vehicle_id, daily_rate)
.__load_kg = load_kg
():
().cost_for(days) + Van.CLEANING_FEE
():
+ .get_vehicle_id() + + (.__load_kg) +
():
MAX_CHARGE =
():
cost = ._daily_rate * days
cost > EBike.MAX_CHARGE:
cost = EBike.MAX_CHARGE
cost
():
+ .get_vehicle_id()
fleet = [Car(, , ), Van(, , ), EBike(, )]
vehicle fleet:
(vehicle.describe(), , vehicle.cost_for(), )import java.util.ArrayList;
class Vehicle {
private String vehicleId;
protected double dailyRate;
public Vehicle(String vehicleId, double dailyRate) {
.vehicleId = vehicleId;
.dailyRate = dailyRate;
}
String {
vehicleId;
}
{
dailyRate * days;
}
String {
+ vehicleId;
}
}
{
seats;
{
(vehicleId, dailyRate);
.seats = seats;
}
String {
+ getVehicleId() + + seats + ;
}
}
{
;
loadKg;
{
(vehicleId, dailyRate);
.loadKg = loadKg;
}
{
.costFor(days) + CLEANING_FEE;
}
String {
+ getVehicleId() + + loadKg + ;
}
}
{
;
{
(vehicleId, dailyRate);
}
{
dailyRate * days;
(cost > MAX_CHARGE) {
cost = MAX_CHARGE;
}
cost;
}
String {
+ getVehicleId();
}
}
{
{
ArrayList<Vehicle> fleet = <Vehicle>();
fleet.add( (, , ));
fleet.add( (, , ));
fleet.add( (, ));
(Vehicle vehicle : fleet) {
System.out.println(vehicle.describe() + + vehicle.costFor() + );
}
}
}Both programs print the same three lines.
Car C-101, 5 seats costs 225.0 for 5 days
Van V-201, 900 kg costs 375.0 for 5 days
E-bike B-301 costs 50.0 for 5 daysCar overrides only describe, so its cost comes from the inherited cost_for: . Van extends the inherited cost: . EBike replaces it:
The pricing loop calls vehicle.cost_for(5) on every element of a list whose declared type, in Java, is ArrayList<Vehicle>. The compiler only knows that each element is some kind of Vehicle. The choice of which cost_for to run is therefore left until the program runs, when the actual class of each object is known.
The search follows a fixed rule. The runtime starts at the object's own class; if that class defines the method, it runs; if not, it moves up to the superclass and looks again, continuing up the hierarchy until it finds a definition. The dispatch diagram traces this for the three objects in fleet.
![A list fleet of three Vehicle references [0], [1], [2] pointing to a Car object C-101, a Van object V-201 and an EBike object B-301. costFor(5): Car has none, so Vehicle.costFor gives 225.0; Van.costFor (super plus fee) gives 375.0; EBike.costFor (capped) gives 50.0.](/_next/image?url=https%3A%2F%2Fmedia.augment-education.com%2Fnotes%2F2026%2F10%2F0b508ea0-faf2-44fc-b8d7-16bf47625fe1.webp&w=3840&q=75)
Dynamic polymorphism makes programs easier to extend. The loop that prices the fleet contains no if statement asking "is this a van?", so adding a minibus class later means writing one subclass and changing nothing in the loop. Code that tests the class of every object with a chain of if/elif or instanceof checks has to be found and edited every time a class is added, which is slow and easy to get wrong.
YouTubeThe second form of polymorphism gives one method name several versions in the same class, each with a different parameter list. A price quote might be needed for a number of days, for a number of days with a percentage discount, or for a number of days with a promotional code. Three method names such as quoteDays, quoteDiscount and quotePromo would work, but one name that accepts all three kinds of request is easier to remember and use.
Java chooses among overloaded versions by matching the number and types of the arguments in the call against each signature. The compiler does this before the program runs, so the decision is fixed in the compiled code.
In the Java listing, Vehicle has three overloaded quote methods and Car has two overloaded constructors. The second constructor uses this(...) to call the first, so the code that sets the attributes exists only once. The (int, String) version of quote calls the other two, which is a common way to avoid repeating logic across overloads.
class Vehicle {
private String vehicleId;
protected double dailyRate;
public Vehicle(String vehicleId, double dailyRate) {
this.vehicleId = vehicleId;
this.dailyRate = dailyRate;
}
public String getVehicleId() {
return vehicleId;
}
public double costFor(int days) {
dailyRate * days;
}
{
costFor(days);
}
{
costFor(days) * ( - discountPercent) / ;
}
{
(promoCode.equals()) {
quote(days, );
}
quote(days);
}
}
{
seats;
{
(vehicleId, dailyRate);
.seats = seats;
}
{
(vehicleId, dailyRate, );
}
{
seats;
}
}
{
{
(, );
(, , );
System.out.println(family.getSeats() + + minibus.getSeats());
System.out.println(family.quote());
System.out.println(family.quote(, ));
System.out.println(family.quote(, ));
System.out.println(family.quote(, ));
}
}class Vehicle:
def __init__(self, vehicle_id, daily_rate):
self.__vehicle_id = vehicle_id
self._daily_rate = daily_rate
def get_vehicle_id(self):
return self.__vehicle_id
def cost_for(self, days):
return self._daily_rate * days
# One quote method with optional parameters replaces Java's three overloads
def quote(self, days, discount_percent=0, promo_code=""):
if promo_code == "WEEKEND":
discount_percent = 20.0
return self.cost_for(days) * ( - discount_percent) /
():
():
().__init__(vehicle_id, daily_rate)
.__seats = seats
():
.__seats
family = Car(, )
minibus = Car(, , )
(family.get_seats(), minibus.get_seats())
(family.quote())
(family.quote(, ))
(family.quote(, promo_code=))
(family.quote(, promo_code=))5 9
180.0
162.0
144.0
180.0The values follow from the rate: ; with 10% off, ; with the weekend code's 20% off,
Python has no method overloading. A class can hold only one attribute with a given name, so a second def with the same name replaces the first, and calls that matched the first version now fail.
class PriceChecker:
def quote(self, days):
return days * 45.0
def quote(self, days, discount_percent): # replaces the first quote
return days * 45.0 * (100 - discount_percent) / 100
checker = PriceChecker()
print(checker.quote(4, 10.0))
print(checker.quote(4))The first call matches the surviving two-parameter version; the second no longer matches anything.
162.0
TypeError: PriceChecker.quote() missing 1 required positional argument: 'discount_percent'Python reaches the same result in other ways. Default parameter values let one method accept different numbers of arguments: quote(self, days, discount_percent=0, promo_code="") handles all three of Java's cases, and Car's seats=5 replaces the second constructor. Keyword arguments such as promo_code="WEEKEND" let a call skip parameters in the middle. These give Python the convenience of overloading, but the choice of behaviour is made by if statements inside one method while it runs, not by the compiler.
Overriding and overloading often appear in the same class, and a call to one can trigger the other.
Operators can be polymorphic too. In Python, + adds two numbers, joins two strings and joins two lists, because each type defines what + means for it: 2 + 3 gives 5, "Car" + "-101" gives "Car-101" and [1, 2] + [3] gives [1, 2, 3]. A class can give an operator a meaning of its own by defining a special method whose name starts and ends with two underscores.
The listing gives Vehicle a __lt__ method so that one vehicle is "less than" another when a three-day hire costs less, then finds the cheapest vehicle with a hand-written loop. Inside __lt__, the call self.cost_for(3) is itself dynamically polymorphic, so each subclass's pricing rule is used. Java has no operator overloading for user classes (only + on strings is built in), so the Java version gives the same comparison an ordinary method name, isCheaperThan.
def __lt__(self, other): # gives meaning to vehicle_a < vehicle_b
return self.cost_for(3) < other.cost_for(3)With that method added to Vehicle, the < operator works between any two vehicles, so a loop can find the cheapest one.
fleet = [Van("V-201", 70.0, 900), Car("C-101", 45.0, 5), EBike("B-301", 12.0)]
cheapest = fleet[0]
for vehicle in fleet:
if vehicle < cheapest: # Python calls vehicle.__lt__(cheapest)
cheapest = vehicle
print("Cheapest for 3 days:", cheapest.describe()) public boolean isCheaperThan(Vehicle other) { // Java's stand-in for <
return costFor(3) < other.costFor(3);
}The method sits inside Vehicle; the loop is inside main.
ArrayList<Vehicle> fleet = new ArrayList<Vehicle>();
fleet.add(new Van("V-201", 70.0, 900));
fleet.add(new Car("C-101", 45.0, 5));
fleet.add(new EBike("B-301", 12.0));
Vehicle cheapest = fleet.get(0);
for (Vehicle vehicle : fleet) {
if (vehicle.isCheaperThan(cheapest)) {
cheapest = vehicle;
}
}
System.out.println("Cheapest for 3 days: " + cheapest.describe());Cheapest for 3 days: E-bike B-301The e-bike costs against 135.0 for the car and 235.0 for the van. Two other special methods appear in almost every Python class that is printed or compared: __str__ sets how an object prints and __eq__ sets when two objects count as equal. Java has no operator overloading, but it gives every class overridable methods with the same jobs, toString and equals.
The two techniques have similar names and are easy to mix up, but they solve different problems. Overriding lets different classes answer the same call differently; overloading lets one class accept different kinds of call under one name.
Feature | Method overriding | Method overloading |
|---|---|---|
Where the methods are | Superclass and subclass | Usually the same class |
Signatures | Identical | Same name, different parameter lists |
Chosen by | The class of the object (at run time) | The number and types of the arguments (at compile time) |
Form of polymorphism | Dynamic | Static |
Needs inheritance | Yes | No |
Python | Yes, the same as Java | No: use default parameters or keyword arguments |
Java marker |
| None |
One subtle case, which the table does not show, makes the difference especially clear. When a call combines both forms, Java picks the overload from the declared type of the variable (at compile time) but picks the override from the actual object (at run time).
The Java class Labels uses the classes Vehicle and Car from the overriding listing. It has two overloaded static methods named label, one taking a Vehicle and one taking a Car.
public class Labels {
static String label(Vehicle v) {
return "Vehicle label: " + v.describe();
}
static String label(Car c) {
return "Car label: " + c.describe();
}
public static void main(String[] args) {
Vehicle v = new Car("C-101", 45.0, 5);
Car c = new Car("C-102", 60.0, 7);
System.out.println(label(v));
System.out.println(label(c));
System.out.println(v.costFor(2) + c.costFor(2));
}
}"Abstraction" names three related ideas in computer science, each treated in its own subtopic. In computational thinking (subtopic B1.1), abstraction means leaving out detail that does not matter for the problem. An abstract data type (subtopic B4.1) describes a data structure by its operations, apart from how they are implemented. In OOP, abstraction applies the same idea to classes.
Abstraction is often confused with encapsulation, because both involve hiding something. Encapsulation hides an object's data (its state) behind methods, so other code cannot put the object into an invalid state. Abstraction hides the implementation of behaviour behind a shared set of method names, so other code can use many classes in the same way without knowing which one it has. A class can do both at once: Vehicle keeps vehicle_id private (encapsulation) and declares cost_for without saying how each vehicle prices a hire (abstraction).
In the hire system nobody hires a plain "vehicle": every real object is a car, a van or an e-bike. Vehicle exists only to hold what they share and to promise that every vehicle can price a hire. Making it abstract writes that design into the code.
The promise is made with an abstract method: a declaration with a name, parameters and return type, but no body. Every subclass that is meant to be instantiated must override it with a real body. A class with even one abstract method that it has not implemented must itself be abstract.
Java marks both with the keyword abstract: abstract class Vehicle and public abstract double costFor(int days);, ending in a semicolon instead of a body. Python needs the standard library module abc (abstract base classes): the class inherits from ABC, and each abstract method carries the decorator @abstractmethod, with pass as its body.
An abstract class may still contain concrete code. Vehicle keeps its constructor, its private attributes and get_vehicle_id, which every subclass inherits unchanged; only cost_for and describe are abstract. A constructor in an abstract class is never called with new Vehicle(...), but it runs through every subclass's super(...) call.
Both languages refuse to create an object of an abstract class, and both refuse to create an object of a subclass that forgot to implement an abstract method. They refuse at different times. The test program declares a subclass Truck that leaves out costFor, then tries to create a plain Vehicle.
abstract class Vehicle {
public abstract double costFor(int days);
}
class Truck extends Vehicle { // no costFor: still abstract
}
public class S4err {
public static void main(String[] args) {
Vehicle v = new Vehicle();
}
}from abc import ABC, abstractmethod
class Vehicle(ABC):
@abstractmethod
def cost_for(self, days):
pass
class Truck(Vehicle): # no cost_for: still abstract
pass
t = Truck()The Java compiler reports both errors before the program can run.
S4err.java:5: error: Truck is not abstract and does not override abstract method costFor(int) in Vehicle
S4err.java:10: error: Vehicle is abstract; cannot be instantiatedPython creates the class Truck without complaint and raises a TypeError only when the program tries to create a Truck object:
TypeError: Can't instantiate abstract class Truck without an implementation for abstract method 'cost_for'Some behaviour cuts across a hierarchy. Electric cars and e-bikes can both be recharged, but an electric car is a kind of car and an e-bike is not. Putting recharge in Vehicle would wrongly give it to vans, and a new superclass "electric vehicle" cannot help because a Java class can extend only one class.
A Java class implements an interface and may implement any number of them, alongside the one class it extends. An interface's methods are public and abstract unless written otherwise, and it has no constructor and no instance variables. UML calls implementing an interface realisation and draws it as a dashed line from the class to the interface, ending in a hollow triangle like the one used for inheritance. Python has no interface keyword; the same role is played by an abstract base class that contains only abstract methods, and a class "implements" it by listing it as an extra superclass, which Python's multiple inheritance allows.
The listing makes Vehicle abstract, adds the interface Chargeable, and creates ElectricCar (which extends Car) and EBike (which extends Vehicle), both of which implement Chargeable. The loop recharges any chargeable vehicle below 30%, whatever its class.
from abc import ABC, abstractmethod
class Vehicle(ABC): # abstract: no object of this class can exist
def __init__(self, vehicle_id, daily_rate):
.__vehicle_id = vehicle_id
._daily_rate = daily_rate
():
.__vehicle_id
():
():
():
():
():
():
():
().__init__(vehicle_id, daily_rate)
.__seats = seats
():
._daily_rate * days
():
+ .get_vehicle_id()
(Car, Chargeable):
():
().__init__(vehicle_id, daily_rate)
.__charge_percent = charge_percent
():
.__charge_percent
():
.__charge_percent =
():
+ .get_vehicle_id()
(Vehicle, Chargeable):
():
().__init__(vehicle_id, daily_rate)
.__charge_percent = charge_percent
():
cost = ._daily_rate * days
cost > :
cost =
cost
():
.__charge_percent
():
.__charge_percent =
():
+ .get_vehicle_id()
fleet = [Car(, ), ElectricCar(, , ), EBike(, , )]
vehicle fleet:
(vehicle, Chargeable) vehicle.get_charge_percent() < :
vehicle.recharge()
(vehicle.describe(), , vehicle.get_charge_percent())
:
v = Vehicle(, )
TypeError error:
(, error)Electric car E-401 recharged to 100
TypeError: Can't instantiate abstract class Vehicle without an implementation for abstract methods 'cost_for', 'describe'The Java version prints only the first line, because its compiler stops the attempt to create a Vehicle before the program can run. The electric car at 22% is recharged; the e-bike at 80% is left alone; the plain car is skipped because it is not Chargeable. In Java the cast (Chargeable) vehicle does not change the object: it lets the compiler treat the same reference as a Chargeable, so the interface's methods can be called on it.
The choice between an abstract class and an interface depends on what the shared part contains. The comparison table sets out the differences for Java, where both exist.
Feature | Abstract class | Interface |
|---|---|---|
Can be instantiated | No | No |
Instance variables | Yes | No (constants only) |
Constructors | Yes, called through | No |
Concrete methods | Yes | Only |
How many a class can use | Extends one | Implements any number |
Relationship expressed | Is a (a car is a vehicle) | Can do (an e-bike can be charged) |
Python equivalent | Class inheriting from | An |
Java SE language rules; default methods in interfaces were added in Java 8 (2014).
An abstract class suits a family of closely related classes that share state and code, such as the vehicles. An interface suits a capability that unrelated classes may share, such as being chargeable, printable or comparable. Large Java libraries combine them: the List interface is implemented by both ArrayList and LinkedList, so code written against List works with either.
Inheritance relates a class to a more general version of itself. Most relationships between objects are of another kind, in which one object contains or holds other objects: a depot holds vehicles, a vehicle keeps a service log, a playlist holds songs.
The "has a" sentence is the test, alongside "is a" for inheritance: "a depot has vehicles" is true and "a depot is a vehicle" is false, so Depot holds a list of vehicles and does not inherit from Vehicle. Has-a relationships come in two strengths, which differ in who creates the parts and how long the parts live. The UML notation diagram shows them with the other relationship lines.

In composition the whole creates its parts and owns them for their entire life. A Vehicle creates its ServiceLog inside its own constructor and never hands a reference to it to any other object. The log has no meaning apart from its vehicle, and when the vehicle is no longer used, its log goes with it.
"Destroyed" means something specific in Python and Java, which free memory automatically. Once no reference to an object remains anywhere, no code can reach it again, and the garbage collector may reclaim its memory. Because only the vehicle ever holds a reference to its log, losing the last reference to the vehicle also leaves the log with no references, so the two disappear together. If the vehicle handed its log out, for example through a get_log() method, other code could keep the log alive after the vehicle had gone, and the relationship would no longer be a true composition.
In aggregation the whole holds references to parts that were created elsewhere and that can carry on existing without it. Vehicles are bought before they are given to a depot, move between depots, and survive the closure of a depot.
The test is visible in the code. A composed part is created inside the whole, usually in its constructor with new or a direct call of the class; an aggregated part is passed in from outside, usually as an argument to a method such as add_vehicle. The listing adds ServiceLog (composed into Vehicle) and Depot (which aggregates vehicles), moves a car from one depot to another, then closes the first depot.
from abc import ABC, abstractmethod
class ServiceLog: # a part that only a Vehicle creates
def __init__(self):
self.__entries = []
def add(self, note):
self.__entries.append(note)
def latest(self):
(.__entries) == :
.__entries[(.__entries) - ]
():
():
.__vehicle_id = vehicle_id
._daily_rate = daily_rate
.__log = ServiceLog()
():
.__vehicle_id
():
.__log.add(note)
():
.__log.latest()
():
():
():
._daily_rate * days
():
():
._daily_rate * days +
:
():
.__name = name
.__vehicles = []
():
.__vehicles.append(vehicle)
():
.__vehicles.remove(vehicle)
():
(.__vehicles)
car = Car(, )
van = Van(, )
harbour = Depot()
station = Depot()
harbour.add_vehicle(car)
harbour.add_vehicle(van)
car.record_service()
harbour.remove_vehicle(car)
station.add_vehicle(car)
harbour =
(station.vehicle_count(), car.get_vehicle_id(), car.last_service())
(van.get_vehicle_id(), van.last_service())import java.util.ArrayList;
class ServiceLog {
ArrayList<String> entries = <String>();
{
entries.add(note);
}
String {
(entries.size() == ) {
;
}
entries.get(entries.size() - );
}
}
{
String vehicleId;
dailyRate;
ServiceLog log;
{
.vehicleId = vehicleId;
.dailyRate = dailyRate;
.log = ();
}
String {
vehicleId;
}
{
log.add(note);
}
String {
log.latest();
}
;
}
{
{
(vehicleId, dailyRate);
}
{
dailyRate * days;
}
}
{
{
(vehicleId, dailyRate);
}
{
dailyRate * days + ;
}
}
{
String name;
ArrayList<Vehicle> vehicles;
{
.name = name;
.vehicles = <Vehicle>();
}
{
vehicles.add(vehicle);
}
{
vehicles.remove(vehicle);
}
{
vehicles.size();
}
}
{
{
(, );
(, );
();
();
harbour.addVehicle(car);
harbour.addVehicle(van);
car.recordService();
harbour.removeVehicle(car);
station.addVehicle(car);
harbour = ;
System.out.println(station.vehicleCount() + + car.getVehicleId() + + car.lastService());
System.out.println(van.getVehicleId() + + van.lastService());
}
}1 C-101 Tyres replaced
V-201 no services yetAfter harbour is set to None (null in Java), no reference to the Harbour depot remains, so it can be destroyed. The van it held is still there, because the variable van refers to it: aggregated parts outlive their container. The car moved to the Station depot as the same object, not a copy, and kept its service log, because the log belongs to the car and not to any depot. Vehicle exposes only record_service and last_service, passing the work to its log (a technique called delegation), so no outside code can reach the ServiceLog object itself.
Every relationship between classes makes one class depend on another. The strength of that dependence determines how far a change in one class spreads into others.
Loose coupling is the aim, because loosely coupled classes can be changed, tested and replaced one at a time. Inheritance is the tightest coupling OOP offers: a subclass depends on the superclass's code, its protected members and the order in which its methods call each other. Composition is looser, because the whole uses its parts only through their public methods and could swap a part for another class with the same methods. For this reason many designers follow the rule "favour composition over inheritance", which appears in the Design Patterns book: reuse code by building objects out of other objects unless the is-a test clearly holds.
Composed parts also make behaviour changeable while the program runs. A vehicle that holds a pricing object could switch to a holiday pricing object for a week; a vehicle that inherits its pricing from a superclass cannot change its superclass. Composition costs a little more code, because the whole must pass calls on to its parts, and the design is spread over more classes.
The system class diagram brings every relationship of the hire system together: inheritance and realisation, aggregation and composition, with multiplicities at the ends of the has-a lines. Abstract names and abstract methods are written in italics, and the stereotypes «abstract» and «interface» make them clear in print.

The same design problems recur in program after program: making sure there is only one of something, creating objects without tying code to their classes, and telling many objects when something changes. Experienced developers solved them in similar ways long before the solutions had names. Naming and documenting the solutions lets a team say "make it a singleton" instead of describing the design from scratch.
The book that made patterns standard vocabulary, Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides, describes 23 patterns. The four authors are known as the "Gang of Four". The singleton, factory and observer patterns all build on tools already met: static members, private constructors, abstract types, interfaces and polymorphism.
Source: Design Patterns (Addison-Wesley), first published 21 October 1994; more than 500,000 copies sold in English and 13 other languages (Wikipedia, "Design Patterns", 2026).
Some things must exist exactly once in a running program. The hire company has one set of daily rates; if two parts of the program each created their own rate card, a price change made through one would not be seen by the other, and customers would be quoted different prices.
The structure has three parts, shown in the singleton diagram. A private static variable holds the one instance (empty at first). The constructor is private, so no other class can call new. A public static method, conventionally getInstance(), creates the instance the first time it is called and returns the same object every time after.

class RateCard:
__instance = None # class variable: the one shared object
def __init__(self):
if RateCard.__instance is not None:
raise RuntimeError("Use RateCard.get_instance()")
self.__rates = {"car": 45.0, "van": 70.0, "ebike": 12.0}
@staticmethod
def get_instance():
if RateCard.__instance is None: # created on first use only
RateCard.__instance = RateCard()
return RateCard.__instance
def rate_for(self, kind):
return self.__rates[kind]
def set_rate(self, kind, rate):
self.__rates[kind] = rateclass RateCard {
private static RateCard instance = null; // the one shared object
private HashMap<String, Double> rates;
private RateCard() { // private: no other class can call new
rates = new HashMap<String, Double>();
rates.put("car", 45.0);
rates.put("van", 70.0);
rates.put("ebike", 12.0);
}
public static RateCard getInstance() {
if (instance == null) { // created on first use only
instance = new RateCard();
}
return instance;
}
public double {
rates.get(kind);
}
{
rates.put(kind, rate);
}
}The demonstration asks for the rate card twice, changes the van rate through one reference and reads it through the other.
card_a = RateCard.get_instance()
card_b = RateCard.get_instance()
card_a.set_rate("van", 75.0)
print(card_a is card_b) # True: one object, two references
print(card_b.rate_for("van")) # the change is seen through every reference
try:
RateCard()
except RuntimeError as error:
print("RuntimeError:", error)RateCard cardA = RateCard.getInstance();
RateCard cardB = RateCard.getInstance();
cardA.setRate("van", 75.0);
System.out.println(cardA == cardB); // true: one object, two references
System.out.println(cardB.rateFor("van")); // the change is seen through every referenceTrue
75.0
RuntimeError: Use RateCard.get_instance()Java prints true and 75.0, and needs no runtime check: a line such as new RateCard() in another class does not compile, failing with error: RateCard() has private access in RateCard. Python cannot make a constructor private, so the Python version guards __init__ itself and raises an exception if an instance already exists; the class variable __instance is name-mangled, which keeps other code from replacing it by accident.
Java's standard library uses the pattern for the running program's environment. Every Java application has a single instance of java.lang.Runtime, which gives access to facts such as the number of processors and the free memory; it is obtained from the static method Runtime.getRuntime(), and Runtime has no public constructor.
Source: Oracle, Java SE 21 API documentation, class Runtime.
The singleton is useful for configuration, a shared log or a single connection to a database, but it has serious costs. It is a form of global state, because any class can reach it and change it, so a bug in one corner of the program can change prices everywhere. Classes that call getInstance() inside their methods are tightly coupled to the singleton, which makes them hard to test with different settings. The simple "create on first use" check is also unsafe when two threads call getInstance() at the same moment, because both may find the instance empty and create one each.
The same three-part structure fits any object that must exist once, such as the high-score table of a game.
Code that creates objects with new Car(...) or Van(...) names the concrete classes, so every such line must change when a class is renamed, a new subclass is added or the way objects are built changes. A booking screen should only need to say "a van, please".
The factory diagram shows the structure. The client (BookingScreen) calls VehicleFactory.create("van", "V-210") and receives a reference of the abstract type Vehicle. Only the factory names Car, Van and EBike, and it also looks up each vehicle's rate from the singleton rate card, so the two patterns work together.

The three pattern listings form one program. In Python the rate card is saved as singleton.py, the factory as factory.py and the observer classes as observer.py, and all three use a shortened version of the vehicle classes saved as vehicles.py: an abstract Vehicle with an identifier, a daily rate and an abstract cost_for, and the three pricing rules of Car, Van and EBike, each constructed from an identifier and a rate.
from abc import ABC, abstractmethod
class Vehicle(ABC):
def __init__(self, vehicle_id, daily_rate):
self.__vehicle_id = vehicle_id
self._daily_rate = daily_rate
def get_vehicle_id(self):
return self.__vehicle_id
@abstractmethod
def cost_for(self, days):
pass
class Car(Vehicle):
def cost_for(self, days):
return self._daily_rate * days
class Van(Vehicle):
def cost_for(self, days):
return ._daily_rate * days +
():
():
cost = ._daily_rate * days
cost > :
cost =
costThe factory itself is a single static method.
from vehicles import Car, Van, EBike
from singleton import RateCard
class VehicleFactory:
@staticmethod
def create(kind, vehicle_id): # the only place that names the subclasses
card = RateCard.get_instance()
if kind == "car":
return Car(vehicle_id, card.rate_for("car"))
elif kind == "van":
return Van(vehicle_id, card.rate_for("van"))
elif kind == "ebike":
return EBike(vehicle_id, card.rate_for("ebike"))
raise ValueError("Unknown vehicle kind: " + kind)class VehicleFactory {
public static Vehicle create(String kind, String vehicleId) { // the only place that names the subclasses
RateCard card = RateCard.getInstance();
if (kind.equals("car")) {
return new Car(vehicleId, card.rateFor("car"));
} else if (kind.equals("van")) {
return new Van(vehicleId, card.rateFor("van"));
} else if (kind.equals("ebike")) {
return new EBike(vehicleId, card.rateFor("ebike"));
}
throw new IllegalArgumentException("Unknown vehicle kind: " + kind);
}
}The demonstration runs in the same program as the singleton's, after the van rate was raised to 75.0. The Python expression type(vehicle).__name__ (vehicle.getClass().getSimpleName() in Java) gives the name of the object's actual class, to show which subclass the factory chose.
orders = [["car", "C-110"], ["ebike", "B-310"], ["van", "V-210"]]
for order in orders:
vehicle = VehicleFactory.create(order[0], order[1])
print(vehicle.get_vehicle_id(), type(vehicle).__name__, vehicle.cost_for(2))
try:
VehicleFactory.create("boat", "X-1")
except ValueError as error:
print("ValueError:", error)String[][] orders = {{"car", "C-110"}, {"ebike", "B-310"}, {"van", "V-210"}};
for (String[] order : orders) {
Vehicle vehicle = VehicleFactory.create(order[0], order[1]);
System.out.println(vehicle.getVehicleId() + " " + vehicle.getClass().getSimpleName() + " " + vehicle.costFor(2));
}
try {
VehicleFactory.create("boat", "X-1");
} catch (IllegalArgumentException error) {
System.out.println("IllegalArgumentException: " + error.getMessage());
}C-110 Car 90.0
B-310 EBike 24.0
V-210 Van 175.0
ValueError: Unknown vehicle kind: boatThe van's two-day cost is , which confirms that the factory read the changed rate from the single rate card. Java throws IllegalArgumentException with the same message for the unknown kind.
The factory keeps creation decisions in one place, so adding a minibus means one new subclass and one new branch in create, while every caller stays the same. Callers work only with the abstract type, which is abstraction and dynamic polymorphism applied to object creation. The costs are an extra class to maintain, a chain of if/else if branches that grows with every subclass, and a choice made from a string, so a misspelt kind such as "vna" is caught only when the program runs.
When a vehicle is returned to a depot, several parts of the system need to know: the staff screen must update, and a customer waiting for a van should get a text. If Depot called each of these directly, it would need to know about every screen and every customer class, and adding a new kind of listener (an email service, a usage report) would mean editing Depot again.
The subject depends only on the observer interface, here DepotObserver with one method, update. Any class that implements the interface can subscribe with add_observer and unsubscribe with remove_observer. When something happens, the subject loops through its list and calls update on each observer; dynamic polymorphism runs each observer's own version. The observer sequence diagram shows the calls made by the demonstration, one row per call, in time order down the page.

from abc import ABC, abstractmethod
from vehicles import Van
class DepotObserver(ABC): # the interface every observer implements
@abstractmethod
def update(self, depot_name, vehicle):
pass
class StaffScreen(DepotObserver):
def update(self, depot_name, vehicle):
print("[screen]", vehicle.get_vehicle_id(), "returned to", depot_name)
class WaitingCustomer(DepotObserver):
def __init__():
.__name = name
():
(vehicle, Van):
( + .__name + , depot_name)
:
():
.__name = name
.__vehicles = []
.__observers = []
():
.__observers.append(observer)
():
.__observers.remove(observer)
():
.__vehicles.append(vehicle)
.__notify_observers(vehicle)
():
observer .__observers:
observer.update(.__name, vehicle)interface DepotObserver { // the interface every observer implements
void update(String depotName, Vehicle vehicle);
}
class StaffScreen implements DepotObserver {
@Override
public void update(String depotName, Vehicle vehicle) {
System.out.println("[screen] " + vehicle.getVehicleId() + " returned to " + depotName);
}
}
class WaitingCustomer implements {
String name;
{
.name = name;
}
{
(vehicle Van) {
System.out.println( + name + + depotName);
}
}
}
{
String name;
ArrayList<Vehicle> vehicles = <Vehicle>();
ArrayList<DepotObserver> observers = <DepotObserver>();
{
.name = name;
}
{
observers.add(observer);
}
{
observers.remove(observer);
}
{
vehicles.add(vehicle);
notifyObservers(vehicle);
}
{
(DepotObserver observer : observers) {
observer.update(name, vehicle);
}
}
}harbour = Depot("Harbour")
amira = WaitingCustomer("Amira")
harbour.add_observer(StaffScreen())
harbour.add_observer(amira)
harbour.return_vehicle(VehicleFactory.create("ebike", "B-302"))
harbour.return_vehicle(VehicleFactory.create("van", "V-202"))
harbour.remove_observer(amira) # Amira has her van: stop texting her
harbour.return_vehicle(VehicleFactory.create("van", "V-203"))Depot harbour = new Depot("Harbour");
WaitingCustomer amira = new WaitingCustomer("Amira");
harbour.addObserver(new StaffScreen());
harbour.addObserver(amira);
harbour.returnVehicle(VehicleFactory.create("ebike", "B-302"));
harbour.returnVehicle(VehicleFactory.create("van", "V-202"));
harbour.removeObserver(amira); // Amira has her van: stop texting her
harbour.returnVehicle(VehicleFactory.create("van", "V-203"));[screen] B-302 returned to Harbour
[screen] V-202 returned to Harbour
[text to Amira] A van is free at Harbour
[screen] V-203 returned to Harbournotify_observers is private (two underscores in Python), because only the depot's own methods should trigger a notification. The demonstration runs in the same program as the singleton and factory demonstrations, which is why it can create its vehicles with VehicleFactory.create.
Graphical user interfaces are built on this pattern. In Java's Swing library a button is the subject: a class implements the ActionListener interface, its object is registered with the button's addActionListener method, and the object's actionPerformed method runs when the button is clicked. In a web page, JavaScript's addEventListener() registers a function to be called whenever a chosen event reaches an element. Java's own early Observable class and Observer interface were deprecated in Java 9, with the documentation noting that the order of notifications was unspecified; programs now write their own observer interfaces or use the listener style.
Sources: Oracle, Java SE 21 API documentation (ActionListener, Observable); MDN Web Docs, EventTarget.addEventListener().
The observer pattern keeps the subject loosely coupled to its observers, supports any number of them, and lets them change while the program runs. Its drawbacks follow from the same indirection. Following what happens after one change means finding every registered observer, which can be hard to debug; observers that are never removed keep receiving updates and stay in memory; and a slow observer delays all the others, because the subject calls them one after another.
Each pattern answers one recurring question. The table matches problems in the hire system, and elsewhere, to the pattern that solves them.
Pattern | Problem it solves | Key structure | Typical uses | Main drawback |
|---|---|---|---|---|
Singleton | Exactly one shared object | Private constructor, static instance, static | Configuration, log, rate card | Global state; tight coupling; hard to test |
Factory | Create objects without naming their classes | Method that chooses a subclass and returns the superclass type | Creating vehicles, shapes, documents | Extra class; growing chain of branches |
Observer | Tell many objects about a change | Subject with a list of observers sharing an interface | GUI events, notifications, live displays | Hard to trace; forgotten observers |
Every class a Java programmer writes is already a subclass. java.lang.Object is the root of Java's class hierarchy, and every class has Object as a superclass: a class written without extends extends Object automatically. Python 3 does the same with its built-in class object. The methods these root classes define are inherited by every object in every program, and overriding them is the most common use of polymorphism in everyday code.
Two of Object's methods matter most. toString() returns a text form of the object; its inherited version returns the class name, an @ sign and the object's hash code in hexadecimal, which is rarely what a user needs. equals(Object other) decides whether two objects count as equal; its inherited version returns true only when both references point to the same object, the same test as ==. Python's object provides the matching special methods __str__ and __eq__, with the same default behaviour.
Library code is written against the root class and relies on dynamic polymorphism to reach the overriding versions. System.out.println(x) and Python's print(x) call toString() or __str__() on whatever object they are given. ArrayList.contains, Python's in operator and both languages' sets use equals or __eq__ to compare elements. A class that overrides these methods therefore changes how the whole library treats its objects, without the library knowing the class exists.
class Plain: # no superclass written: object is the superclass
pass
class Car:
def __init__(self, vehicle_id):
self.__vehicle_id = vehicle_id
def __str__(self): # overrides object.__str__
return "Car " + self.__vehicle_id
def __eq__(self, other): # overrides object.__eq__
if not isinstance(other, Car):
return False
return self.__vehicle_id == other.__vehicle_id
def __hash__():
(.__vehicle_id)
((Plain, ), (Car, ))
(Plain())
a = Car()
b = Car()
(a)
(a b, a == b)
(b [a])
(({a, b})) import java.util.ArrayList;
import java.util.HashSet;
class Plain { // no extends written: Object is the superclass
}
class Car {
private String vehicleId;
public Car(String vehicleId) {
this.vehicleId = vehicleId;
}
@Override
public String toString() { // overrides Object.toString
return "Car " + vehicleId;
}
@Override
{
(!(other Car)) {
;
}
(Car) other;
vehicleId.equals(that.vehicleId);
}
{
vehicleId.hashCode();
}
}
{
{
();
System.out.println(o);
();
();
System.out.println(a);
System.out.println((a == b) + + a.equals(b));
ArrayList<Car> fleet = <Car>();
fleet.add(a);
System.out.println(fleet.contains(b));
HashSet<Car> unique = <Car>();
unique.add(a);
unique.add(b);
System.out.println(unique.size());
}
}True True
<__main__.Plain object at 0x104246a50>
Car C-101
False True
True
1The second line is the inherited __str__ (Java prints Plain@1dbd16a6); the hexadecimal part can differ between runs and machines, because it comes from where the object is stored (Python) or from its identity hash code (Java). The cars a and b are two separate objects, so a is b (a == b in Java) is false, but the overriding __eq__ (equals) compares identifiers, so they are equal by value, and the list and the set treat them as the same car.
equals and hashCode go togetherSets and dictionaries store objects in a hash table, which first uses an object's hash code to choose where to look and only then calls equals. Java's rules therefore say that a class overriding equals should almost always override hashCode too, because equal objects must have equal hash codes. Removing hashCode from the Java Car class and running the program again changes the last line from 1 to 2: the two equal cars get different inherited hash codes, land in different places in the HashSet, and are never compared, so the set holds a duplicate. Python protects programmers from this mistake differently: a class that defines __eq__ without __hash__ becomes unhashable, so adding its objects to a set raises TypeError: unhashable type: 'Car'.
Sources: Oracle, Java SE 21 API documentation, class Object; outputs from OpenJDK 25.0.2 and Python 3.13.7, October 2026.
The root class turns inheritance, overriding and polymorphism into something every program relies on. Inheritance gives every object a working toString and equals from the start. Overriding lets each class replace them, and dynamic polymorphism lets library code written decades earlier call the replacements. The equals/hashCode contract also shows the main risk of inheritance: a subclass that overrides one inherited method can break assumptions the superclass's users depend on, so overriding needs to respect what the original method promised, not just its signature.
Inheritance, polymorphism, abstraction, class relationships and design patterns are assessed on Paper 2 for HL students only, in the language you study (Python or Java). Questions usually give a scenario with a UML class diagram or part of a class, then ask you to construct a subclass, an overriding method or an overloaded method (4–6 marks), explain why a subclass cannot access a private attribute (2 marks), deduce or trace the output of code that mixes overriding and overloading, explain abstraction, composition, aggregation or a design pattern in the context given (3–4 marks), or draw or complete a UML diagram with inheritance and has-a relationships. Code is marked by its features (the extends or bracketed superclass, the super call, the attribute, the return value), and small syntax slips are not normally penalised when the logic is clear.
Code questions on multiple classes reward the same few features again and again. Practise writing subclasses quickly until the header, the superclass constructor call and the use of accessors for private data are automatic.
Design-pattern questions are answered in the scenario's terms, not as definitions learnt by heart.
Question 4 refers to the program accounts, which uses three classes.
class Account:
def __init__(self, balance):
self._balance = balance
def fee(self):
return 2
def month_end(self):
self._balance = self._balance - self.fee()
return self._balance
class StudentAccount(Account):
def fee(self):
return 0
class PremiumAccount(Account):
def fee(self):
return super().fee() * 3
accounts = [Account(10), StudentAccount(10), PremiumAccount(10)]
for account in accounts:
(account.month_end())
(accounts[].month_end())class Account {
protected int balance;
public Account(int balance) {
this.balance = balance;
}
public int fee() {
return 2;
}
public int monthEnd() {
balance = balance - fee();
return balance;
}
}
class StudentAccount extends Account {
public StudentAccount(int balance) {
super(balance);
}
@Override
public int fee() {
;
}
}
{
{
(balance);
}
{
.fee() * ;
}
}
{
{
Account[] accounts = { (), (), ()};
(Account account : accounts) {
System.out.println(account.monthEnd());
}
System.out.println(accounts[].monthEnd());
}
}Define the term method signature. [1]
Distinguish between aggregation and composition, with an example of each from a hospital system. [4]
Outline the difference between protected and default (package) access in Java. [2]
Deduce the four values printed by the program accounts, and identify the line in which dynamic polymorphism selects the method that runs. [5]
Explain why a Java class can extend only one class but implement several interfaces, and how Python achieves the same effect. [3]
A class Thermometer needs to display a temperature as display(double celsius) and as display(double celsius, boolean showFahrenheit). Construct the two methods in Java, and outline how the same behaviour is written in Python. [4]
Explain two disadvantages of the singleton pattern. [4]
A game creates enemies of the classes Goblin, Troll and Dragon, all subclasses of an abstract class Enemy, from level files that contain words such as "troll". Explain how the factory pattern would help, and state one drawback. [4]
Evaluate the use of inheritance, compared with composition, for adding a GPSTracker to every vehicle in the hire system. [5]
The name of a method together with the number, types and order of its parameters (not the return type).
In both, one object holds others (has-a). In aggregation the parts are created elsewhere and continue to exist without the whole: a ward has patients, who are discharged or moved to another ward. In composition the whole creates and owns its parts, which are destroyed with it: a patient record owns its list of observations, which has no meaning without the record. UML: hollow diamond for aggregation, filled for composition, at the whole.
A default member can be used only by classes in the same package; a protected member can be used by classes in the same package and also by subclasses in any other package.
Output 8, 10, 4, -2. Account: ; StudentAccount: fee 0, so 10; PremiumAccount: fee
Inheritance lets a subclass reuse and extend a superclass (extends / class Sub(Super):); use it only when "subclass is a superclass" is true, and call the superclass constructor first (super(...) / super().__init__(...)).
Private fields still exist inside a subclass object, but they are not inherited as members the subclass can use; protected (#) allows subclasses, default (~, Java only) allows the package, public (+) allows everyone. Python uses _ and __ conventions, which are not enforced.
Overriding uses the same signature in a subclass and is resolved at run time from the object's class (dynamic polymorphism); overloading uses one name with different parameter lists and is resolved at compile time from the arguments (static polymorphism). Python has no overloading; it uses default and keyword arguments, and supports operator overloading through methods such as __lt__ and __add__.
Abstraction (OOP) exposes what objects do and hides how. An abstract class cannot be instantiated and declares abstract methods that every concrete class must implement; an interface is a stateless contract a class can implement alongside its one superclass. Python uses abc.ABC and @abstractmethod for both.
Composition (filled diamond) means the whole creates and owns its parts; aggregation (hollow diamond) means the parts are created elsewhere and outlive the whole. The diamond goes at the whole.
Inheritance gives the tightest coupling; composition is looser and more flexible, hence "favour composition over inheritance".
Singleton: private constructor, static instance, static getInstance(); one shared object, at the cost of global state. Factory: one method chooses and creates the subclass and returns the superclass type. Observer: a subject notifies a changing list of observers through a shared interface.
Every Java class inherits from Object and every Python class from object; overriding toString/__str__ and
I can apply the is-a test to decide whether inheritance is appropriate.
I can write a subclass in my language that calls the superclass constructor correctly and adds attributes and methods.
I can state where a private, default, protected and public member can be used, and explain Python's naming conventions.
I can explain why a subclass must use an accessor to read a private inherited attribute.
I can write an overriding method that replaces or extends the inherited version with super.
I can explain how dynamic polymorphism chooses the method from the object's actual class.
I can write overloaded methods and constructors in Java, and the Python equivalent with default parameters.
I can deduce the output of code that mixes overloading and overriding.
I can define an abstract class with abstract methods in Java and with abc in Python, and explain why it cannot be instantiated.
I can distinguish an abstract class from an interface, and abstraction from encapsulation.
I can tell composition from aggregation in code and draw both in UML with the diamond at the whole.
I can explain coupling and why composition is often preferred to inheritance.
115 exam-style questions on this subtopic, written in the style of the IB papers. The first has its markscheme open. Counted on
A school library lends books and DVDs. Every item is stored as an object of one of the classes below.
class LibraryItem:
def __init__(self, title):
self.title = title
def loan_days(self):
return 21
def label(self):
return self.title + " (" + str(self.loan_days()) + " days)"
class Book(LibraryItem):
def __init__(self, title, author):
super().__init__(title)
self.author = author
class DVD(LibraryItem):
def loan_days(self):
return 7public class LibraryItem {
protected String title;
public LibraryItem(String title) {
this.title = title;
}
public int loanDays() {
return 21;
}
public String label() {
return title + " (" + loanDays() + " days)";
}
}
public class Book extends LibraryItem {
private String author;
public Book(String title, String author) {
super(title);
this.author = author;
}
}
public class DVD extends LibraryItem {
public DVD(String title) {
super(title);
}
{
;
}
}State the name of the class that DVD inherits from.
1 mark
LibraryItem;
Note: Accept "the superclass / parent class LibraryItem".
Identify two members (attributes or methods) that a Book object has but that are not declared in the Book class.
2 marks
Award [1] per member, up to [2 max].
title;loan_days() / loanDays();label();
Note: Do not accept author (declared in Book). Do not accept the constructor.
The following code is run.
items = [Book("Dune", "Herbert"), DVD("Up"), LibraryItem("Atlas")]
for item in items:
print(item.label())A ride-share company models its journeys with the classes in Figure 1. In the UML, - means private, + public and # protected. (In Python, private attributes are written with two leading underscores and protected ones with one.)

The constructor of Ride sets baseFare to 3.0. The method fare() in Ride returns baseFare + 2.0 * distanceKm. In PoolRide, fare() returns the normal Ride fare divided by the number of passengers.
State the name of the superclass in Figure 1.
1 mark
Outline what the # symbol before baseFare means for the classes in Figure 1.
2 marks
Identify two members that LuxuryRide inherits from Ride.
2 marks
In LuxuryRide, fare() returns the normal Ride fare plus the surcharge.
A hospital's staff system has a class Employee and a subclass Nurse. A nurse is paid one twelfth of the yearly salary each month, plus 50 for each night shift.
class Employee:
def __init__(self, name, salary):
self._name = name
self.__salary = salary
def get_salary(self):
return self.__salary
class Nurse(Employee):
def __init__(self, name, salary, night_shifts):
super().__init__(name, salary)
self.__night_shifts = night_shifts
def monthly_pay(self):
return self.__salary / 12 + 50 * self.__night_shiftsOne of 60 short multiple-choice questions on B3.2. Pick an answer to see why it's right or wrong.
A developer changes how a superclass method works out a value, and several subclasses written by other people now give wrong results. What is this problem called?
The official deck for this part of the syllabus.
Define the term superclass.
Define the term subclass.
protected int passwordLength() {
return password.length();
}Inside GoldMember, passwordLength() then returns 8 for "oak-2027".
In Python, the same classes would use self.name, self._points and self.__password. Lines A and B would run (the underscore on _points is only a convention), and the line reading self.__password would fail at run time with AttributeError: 'GoldMember' object has no attribute '_GoldMember__password', because name mangling looks for a name that belongs to GoldMember.
Marks go to: the inheritance (extends Vehicle / (Vehicle)) [1]; the superclass constructor call with the identifier and rate [1]; the private load attribute set from the parameter [1]; can_carry returning the comparison [1]. Van("V-201", 70.0, 900).can_carry(950) returns False.
Python:
class Ticket:
def __init__(self, base_price):
self._base_price = base_price
def price(self, quantity=1): # default parameter in place of overloading
return self._base_price * quantity
class StudentTicket(Ticket):
def price(self, quantity=1): # overrides Ticket.price
return super().price(quantity) * 70 / 100(c) In Java the compiler picks the overload price(int) from the argument 3. StudentTicket does not override that version, so Ticket.price(int) runs, and its call price() is dispatched dynamically to StudentTicket.price(), which gives . The result is . In Python, StudentTicket.price(3) calls Ticket.price(3), giving 30.0, then takes 70%: also 21.0.
A check with an adult Ticket(10.0) and a StudentTicket(10.0) prints the same four values in both languages, for adult.price(), student.price(), adult.price(3) and student.price(3) in that order.
10.0
7.0
30.0
21.0A student ticket bought in threes is discounted because the overloaded method reuses the overridden one rather than reading basePrice itself.
210.0Explanation [2]: the overloaded label is chosen at compile time from the declared type Vehicle (static polymorphism); describe is chosen at run time from the object's actual class Car (dynamic polymorphism).
Python has no overloading, so its nearest version is one function that tests the class itself with isinstance. The test runs while the program runs, so it sees the actual object and both calls print "Car label: …".
def label(vehicle):
if isinstance(vehicle, Car):
return "Car label: " + vehicle.describe()
return "Vehicle label: " + vehicle.describe()Python:
class ScoreBoard:
__instance = None
def __init__(self):
if ScoreBoard.__instance is not None:
raise RuntimeError("Use ScoreBoard.get_instance()")
self.__high_score = 0
@staticmethod
def get_instance():
if ScoreBoard.__instance is None:
ScoreBoard.__instance = ScoreBoard()
return ScoreBoard.__instance
def submit(self, score):
if score > self.__high_score:
self.__high_score = score
def get_high_score(self):
return self.__high_scoreExplanation [1]: a private constructor stops every other class from writing new ScoreBoard(), so the only way to obtain a ScoreBoard is getInstance(), which always returns the same object and so guarantees a single high score.
To check, submit 340 and 125 through two separate calls of getInstance(), then 410 through a third reference board. All three calls reach the same object, so board.getHighScore() returns 410 in both languages: 340 replaces 0, 125 is ignored, and 410 replaces 340.
Java (the classes sit in the same file as the patterns, which imports java.util.ArrayList and java.util.HashMap):
abstract class Vehicle {
private String vehicleId;
protected double dailyRate;
public Vehicle(String vehicleId, double dailyRate) {
this.vehicleId = vehicleId;
this.dailyRate = dailyRate;
}
public String getVehicleId() {
return vehicleId;
}
public abstract double costFor(int days);
}
class Car extends Vehicle {
public Car(String vehicleId, double dailyRate) {
super(vehicleId, dailyRate);
}
@Override
public double costFor(int days) {
return dailyRate * days;
}
}
class Van extends Vehicle {
public Van(String vehicleId, double dailyRate) {
super(vehicleId, dailyRate);
}
@Override
public double costFor(int days) {
return dailyRate * days + 25.0;
}
}
class EBike extends Vehicle {
public EBike(String vehicleId, double dailyRate) {
super(vehicleId, dailyRate);
}
@Override
public double costFor(int days) {
double cost = dailyRate * days;
if (cost > 50.0) {
cost = 50.0;
}
return cost;
}
}Python:
class NotifierFactory:
@staticmethod
def create(channel):
if channel == "email":
return EmailNotifier()
elif channel == "sms":
return SmsNotifier()
elif channel == "push":
return PushNotifier()
raise ValueError("Unknown channel: " + channel)Benefit [1]: the rest of the app asks only for NotifierFactory.create(setting) and calls send on a Notifier, so it never names the subclasses; adding a new channel, such as a messaging app, means one new subclass and one new branch, with no change to the code that sends reminders.
A loop over the channels "sms" and "push" that sends "Room changed to B12" prints SMS: Room changed to B12 and then Push: Room changed to B12 in both languages, each line produced by a different subclass's send through dynamic polymorphism.
constructor with three parameters calling the superclass constructor with title and duration;
describe() with the same signature as the abstract method (@Override not required);
return string built from episode number, getTitle() and durationSeconds in the required format.
Python:
class Podcast(MediaItem):
def __init__(self, title, duration_seconds, episode_number):
super().__init__(title, duration_seconds)
self.__episode_number = episode_number
def describe(self):
return ("Podcast " + str(self.__episode_number) + ": " + self.get_title()
+ " (" + str(self._duration_seconds) + " s)")Java:
class Podcast extends MediaItem {
private int episodeNumber;
public Podcast(String title, int durationSeconds, int episodeNumber) {
super(title, durationSeconds);
this.episodeNumber = episodeNumber;
}
@Override
public String describe() {
return "Podcast " + episodeNumber + ": " + getTitle() + " (" + durationSeconds + " s)";
}
}(d) Award [1] for each point, up to [3]:
each subclass overrides the abstract describe() method;
the list's declared type is MediaItem, so describe() can be called on every element;
the version run is chosen at run time from the actual class of each object (dynamic polymorphism / dynamic binding);
so songs run Song.describe() and podcasts run Podcast.describe() without any type checks in the loop.
(e) Aggregation [1]; the media items exist independently of the playlist / are created elsewhere and stay in the library when the playlist is deleted [1].
Total [13]
month_endself._balance - self.fee()balance - fee()month_endfee()feeExtending more than one class could give a class two different inherited implementations (and copies of state) for the same method, which is ambiguous; Java allows one superclass and avoids this. Interfaces declare methods without state, so a class can promise several sets of behaviour and implement each method once. Python allows multiple inheritance and uses its method resolution order to choose; an abstract base class with only abstract methods plays the role of an interface.
Two methods named display in Thermometer, one with parameters (double celsius) and one with (double celsius, boolean showFahrenheit), the second calling the first when showFahrenheit is false (Java code after these answers). Python has no overloading: write one method with a default parameter, def display(self, celsius, show_fahrenheit=False):, and an if inside it.
Any two, each with its consequence: it is global state, so any part of the program can change it and errors spread far from their cause; classes that call getInstance() are tightly coupled to it, so they cannot easily be tested with a different object; lazy creation is not safe with several threads, which may create two instances.
The game asks EnemyFactory.create("troll") and receives an Enemy reference, so the level-loading code never names Goblin, Troll or Dragon; adding a new enemy means one subclass and one branch in the factory, and the rest of the game uses enemies through Enemy's abstract methods. Drawback: the factory's chain of branches grows with each enemy, or an unknown word is only detected at run time.
Inheritance (Vehicle extends GPSTracker) fails the is-a test: a vehicle is not a tracker, and every vehicle would expose all the tracker's public methods and depend on its internals (tight coupling). Composition (Vehicle creates a private GPSTracker in its constructor and delegates location() to it) passes the has-a test, keeps the tracker hidden, and allows a different tracker class to be fitted later. Composition needs a little more code to pass calls on. Conclusion: composition is the better choice.
Answer 6 in Java:
public void display(double celsius) {
System.out.println(celsius + " C");
}
public void display(double celsius, boolean showFahrenheit) {
if (showFahrenheit) {
System.out.println(celsius * 9 / 5 + 32 + " F");
} else {
display(celsius);
}
}For 20.0, display(20.0) prints 20.0 C and display(20.0, true) prints 68.0 F.
__eq__hashCode__hash__I can explain the singleton, factory and observer patterns with their structure, a code outline and a drawback.
I can apply a design pattern to an unfamiliar scenario, naming which class plays each role.
ArrayList<LibraryItem> items = new ArrayList<LibraryItem>();
items.add(new Book("Dune", "Herbert"));
items.add(new DVD("Up"));
items.add(new LibraryItem("Atlas"));
for (LibraryItem item : items) {
System.out.println(item.label());
}State the output.
3 marks
Award [1] per correct line, in the correct order.
Dune (21 days);Up (7 days);Atlas (21 days);
Note: Award [2 max] if the lines are correct but in the wrong order. Ignore minor spacing differences.
Construct, in Python or Java, the LuxuryRide class.
5 marks
The following code is run.
rides = [Ride("R1", 10), PoolRide("R2", 10, 2), LuxuryRide("R3", 10, 8.0)]
total = 0
for r in rides:
print(r.fare())
total = total + r.fare()
print(total)ArrayList<Ride> rides = new ArrayList<Ride>();
rides.add(new Ride("R1", 10));
rides.add(new PoolRide("R2", 10, 2));
rides.add(new LuxuryRide("R3", 10, 8.0));
double total = 0;
for (Ride r : rides) {
System.out.println(r.fare());
total = total + r.fare();
}
System.out.println(total);State the output.
3 marks
Explain how polymorphism allows the loop in part (e) to calculate the correct total without checking the type of each ride.
3 marks
public class Employee {
protected String name;
private double salary;
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
public double getSalary() {
return salary;
}
}
public class Nurse extends Employee {
private int nightShifts;
public Nurse(String name, double salary, int nightShifts) {
super(name, salary);
this.nightShifts = nightShifts;
}
public double monthlyPay() {
return salary / 12 + 50 * nightShifts;
}
}When the programmer tries to work out a nurse's monthly pay, the method monthly_pay / monthlyPay fails.
Explain why monthly_pay / monthlyPay fails.
3 marks
Construct a corrected version of monthly_pay / monthlyPay that does not change the Employee class.
2 marks
Outline one reason why the hospital might keep salary private rather than making it protected.
2 marks