Clastify logo
Subjects
Features
Review
HOT
Tutoring

B3.2: Fundamentals of OOP for multiple classes

Master IB Computer Science B3.2: Fundamentals of OOP for multiple classes with notes created by examiners and strictly aligned with the syllabus.

This content can't be printed from the browser. Use Export PDF with Pro for an offline copy.

IB Syllabus Requirements for Fundamentals of OOP for multiple classes

B3.2.1

Inheritance and code reuse

HL

B3.2.2

Dynamic and static polymorphism

HL

B3.2.3

Abstraction and abstract classes

HL

B3.2.4

Composition and aggregation

HL

B3.2.1

INHERITANCE AND CODE REUSE

HL

Inheritance establishes a parent–child relationship in which a child class reuses and can extend the accessible functionality of a parent class. Explain inheritance by identifying what the parent supplies, what the child adds and how access modifiers restrict direct access.

A hierarchy of related classes

A parent class, also called a superclass, holds features shared by related classes. A child class, also called a subclass, represents a more specialized kind of the parent. This is an is-a relationship: a LoanCamera is a Resource.

A resource-loan system could have Resource as the parent of LoanCamera and LoanProjector. Resource supplies a label and a usage count; each child adds its own operations. The figure shows this hierarchy, with the shared features kept in Resource rather than duplicated in both children.

Changing a shared operation in the parent can update the behaviour inherited by several children. This reduces duplicated code and the risk of fixing one copy while leaving another unchanged. Inheritance is appropriate when the child genuinely represents a kind of the parent, not merely when two classes happen to contain similar code.

Reusing and extending functionality

In this example, LoanCamera inherits the public description operation from Resource and adds a photograph-taking operation. The new operation calls a protected parent method to increase the usage count.

Java

1  class Resource {
2      private String label;
3      protected int uses = 0;
4      public Resource(String label) {
5          this.label = label;
6      }
7      protected void recordUse() {
8          uses++;
9      }
10      public String describe() {
11          return label + ":" + uses;
12      }
13  }
14  class LoanCamera extends Resource {
15      public LoanCamera(String label) {
16          super(label);
17      }
18      public void takePhoto() {
19          recordUse();
20      }
21  }
22  public class Main {
23      public static void main(String[] args) {
24          LoanCamera camera = new LoanCamera("Camera A");
25          camera.takePhoto();
26          System.out.println(camera.describe());
27      }
28  }

Python

1  class Resource:
2      def __init__(self, label):
3          self.__label = label
4          self._uses = 0
5      def _record_use(self):
6          self._uses += 1
7      def describe(self):
8          return f"{self.__label}:{self._uses}"
9  class LoanCamera(Resource):
10      def __init__(self, label):
11          super().__init__(label)
12      def take_photo(self):
13          self._record_use()
14  camera = LoanCamera("Camera A")
15  camera.take_photo()
16  print(camera.describe())

Java uses extends to declare the relationship; Python places the parent class in parentheses. In both versions, super invokes the parent's initialization code. Constructors are not inherited in Java; the child constructor explicitly calls the parent constructor here.

The trace follows the object's state. The private label is initialized by parent code, and the child operation changes the usage count through the inherited method.

EventLabelUsage countOutput
Parent initialization completesCamera A0—
Photograph operation completesCamera A1—
Inherited description is printedCamera A1Camera A:1

Access to parent members

Java access modifiers determine where a member can be accessed directly. Default access means that no access modifier is written; it is also called package-private access.

Java modifierInside the parent classChild in the same packageChild in a different packageUnrelated class in a different package
privateYesNoNoNo
defaultYesYesNoNo
protectedYesYesYes, subject to subclass-access rulesNo
publicYesYesYesYes

For a child in another package, protected instance-member access must be through the child's inherited context or a suitable child-type reference, not an arbitrary parent object.

A private parent field still forms part of the object's state, but child code cannot access it directly. An accessible parent method can provide controlled access: the inherited description method reads the private label in this example.

Python does not enforce Java's four access levels. A leading underscore, as in _uses, signals non-public implementation detail by convention. A double-leading underscore, as in __label, triggers name mangling to reduce accidental access and name clashes; it is not a security boundary. Python has no equivalent of Java's package-private default access.

Key terms: inheritance, parent class, child class, superclass, subclass, code reusability, private, public, protected, default.

B3.2.2

DYNAMIC AND STATIC POLYMORPHISM

HL

Polymorphism allows a common operation to work with objects or arguments of different forms. Construct it by providing compatible operations, then showing how the appropriate implementation is selected.

Dynamic polymorphism through overriding

Method overriding occurs when a subclass supplies its own implementation of an inherited method with the same compatible signature. Dynamic polymorphism selects the implementation at runtime according to the actual object receiving the call.

An alert system can process TextAlert and LightAlert objects through the same message operation. The figure shows a shared call reaching different implementations according to the object's class.

Java

1  class Alert {
2      public String message() {
3          return "alert";
4      }
5  }
6  class TextAlert extends Alert {
7      @Override
8      public String message() {
9          return "text";
10      }
11  }
12  class LightAlert extends Alert {
13      @Override
14      public String message() {
15          return "light";
16      }
17  }
18  public class Main {
19      public static void main(String[] args) {
20          Alert[] alerts = {new TextAlert(), new LightAlert()};
21          for (Alert alert : alerts) {
22              System.out.println(alert.message());
23          }
24      }
25  }

Python

1  class Alert:
2      def message(self):
3          return "alert"
4  class TextAlert(Alert):
5      def message(self):
6          return "text"
7  class LightAlert(Alert):
8      def message(self):
9          return "light"
10  alerts = [TextAlert(), LightAlert()]
11  for alert in alerts:
12      print(alert.message())
Loop iterationalert: actual classSelected methodOutput
FirstTextAlertTextAlert's message methodtext
SecondLightAlertLightAlert's message methodlight

In Java, the loop variable has the declared type Alert, but the actual objects have different child types. Runtime method selection therefore matters. Python also selects the overriding method from the actual object.

The processing loop is reusable: another alert subclass can be added without rewriting the loop, provided it supports the same operation. This gives flexibility without a separate class-checking branch for every possible alert type.

Static polymorphism through overloading

Method overloading provides methods with the same name but different parameter lists. Static polymorphism resolves which overloaded method to use at compile time, using the argument count and declared argument types.

Java can overload an operation that totals either two or three values. The return type alone cannot distinguish overloads.

Java

1  public class Main {
2      static int total(int first, int second) {
3          return first + second;
4      }
5      static int total(int first, int second, int third) {
6          return total(first, second) + third;
7      }
8      public static void main(String[] args) {
9          System.out.println(total(3, 4));
10          System.out.println(total(3, 4, 2));
11      }
12  }

Python does not provide Java-style compile-time method overloading. Defining another method with the same name replaces the earlier definition rather than creating an overload. An idiomatic Python equivalent for these calls uses a default parameter, but this is not static polymorphism.

Python

1  def total(first, second, third=0):
2      return first + second + third
3  first_value = 3
4  second_value = 4
5  third_value = 2
6  print(total(first_value, second_value))
7  print(total(first_value, second_value, third_value))
CallfirstsecondthirdOutput
Two arguments34Not present in the Java overload; 0 in Python7
Three arguments3429

Overloading makes related operations available through one meaningful name. In the Java example, the three-parameter version also reuses the two-parameter version. Compile-time selection avoids runtime selection between these overloads, but it does not guarantee that the whole program runs faster; the work performed by each method still matters.

Distinguishing the mechanisms

FeatureOverridingOverloading
Relationship between methodsA child replaces inherited behaviourSame name, different parameter lists
Selection in these Java examplesRuntime, using the actual objectCompile time, using the arguments
Main benefitCommon client code supports different object behavioursA consistent operation name supports different inputs

Key terms: polymorphism, dynamic polymorphism, static polymorphism, method overriding, method overloading, code flexibility, code reusability.

B3.2.3

ABSTRACTION AND ABSTRACT CLASSES

HL

Abstraction presents the essential operations of a component while leaving its implementation details out of the view of code that uses it. Explain its value by separating what an object promises to do from how a particular class does it.

Abstraction supports modular code

A delivery-pricing module can ask a Delivery object for its cost without knowing how every delivery type calculates that cost. The client depends on the cost operation, rather than on each subclass's internal calculation.

This separation supports modular development: one implementation can change without forcing changes in client code, as long as the promised operation and its meaning remain compatible. It also allows different subclasses to implement the same operation in different ways.

The figure separates client code, the common Delivery contract and the BicycleDelivery and VanDelivery implementations. Both subclasses provide the cost operation required by Delivery.

Abstract classes establish a common interface

An abstract class is a class that cannot be instantiated directly and can specify operations that concrete subclasses must implement. An abstract method declares a required operation without supplying its implementation in that class.

An abstract class can also contain fields and implemented methods shared by its subclasses. Its common interface is the set of operations clients can rely on; this does not require a separate Java interface declaration.

Java

1  abstract class Delivery {
2      public abstract int cost();
3  }
4  class BicycleDelivery extends Delivery {
5      @Override
6      public int cost() {
7          return 3;
8      }
9  }
10  class VanDelivery extends Delivery {
11      @Override
12      public int cost() {
13          return 5;
14      }
15  }
16  public class Main {
17      public static void main(String[] args) {
18          Delivery[] deliveries = {
19              new BicycleDelivery(), new VanDelivery()
20          };
21          for (Delivery delivery : deliveries) {
22              System.out.println(delivery.cost());
23          }
24      }
25  }

Python

1  from abc import ABC, abstractmethod
2  class Delivery(ABC):
3      @abstractmethod
4      def cost(self):
5          pass
6  class BicycleDelivery(Delivery):
7      def cost(self):
8          return 3
9  class VanDelivery(Delivery):
10      def cost(self):
11          return 5
12  deliveries = [BicycleDelivery(), VanDelivery()]
13  for delivery in deliveries:
14      print(delivery.cost())
Loop iterationdelivery: actual classCost returnedOutput
FirstBicycleDelivery33
SecondVanDelivery55

Neither version creates a Delivery object directly. The abstract class represents an incomplete specification, while the concrete subclasses supply the required implementation. A subclass that still lacks a required abstract operation cannot be instantiated as a concrete object.

The client loop illustrates abstraction because it only needs to know that a delivery can supply a cost. Its calls also use polymorphism, but the ideas answer different questions: abstraction identifies the essential contract; polymorphism allows different implementations to respond through it.

Key terms: abstraction, modular code fragments, abstract class, abstract method, common interface, sub-classes.

B3.2.4

COMPOSITION AND AGGREGATION

HL

Composition is a whole–part relationship in which a component belongs to its containing object and has no independent lifetime within the model. Aggregation is a whole–part relationship in which a component can exist and function independently of the object that groups it.

Explain either relationship by identifying the whole, its components and whether those components have an independent existence.

Building objects from smaller objects

Both relationships let a class use smaller component objects instead of implementing every responsibility itself. They express a has-a relationship, rather than the is-a relationship used by inheritance.

Consider a Workshop object. Its ControlPanel is an integral component created for that workshop: this is composition. A Technician exists independently and is assigned to the workshop: this is aggregation. The figure contrasts these ownership relationships.
FeatureCompositionAggregation
ExampleWorkshop and its ControlPanelWorkshop and an assigned Technician
IndependenceThe panel has no independent role or lifetime in this modelThe technician can function without that workshop
OwnershipStrong ownership by the wholeThe whole holds a reference to an independent object
If the whole ceases to existIts component's modelled lifetime ends tooThe independent component can remain

Implementing the relationships

The workshop constructor creates its own panel but receives an already-created technician. These choices express the intended ownership: the panel is internal to the workshop, while the technician is supplied from outside.

Java

1  class ControlPanel {
2      public String status() {
3          return "ready";
4      }
5  }
6  class Technician {
7      public String name;
8      public Technician(String name) {
9          this.name = name;
10      }
11  }
12  class Workshop {
13      private final ControlPanel panel;
14      private final Technician technician;
15      public Workshop(Technician technician) {
16          this.panel = new ControlPanel();
17          this.technician = technician;
18      }
19      public String report() {
20          return technician.name + ":" + panel.status();
21      }
22  }
23  public class Main {
24      public static void main(String[] args) {
25          Technician technician = new Technician("Mira");
26          Workshop workshop = new Workshop(technician);
27          System.out.println(workshop.report());
28      }
29  }

Python

1  class ControlPanel:
2      def status(self):
3          return "ready"
4  class Technician:
5      def __init__(self, name):
6          self.name = name
7  class Workshop:
8      def __init__(self, technician):
9          self.__panel = ControlPanel()
10          self.__technician = technician
11      def report(self):
12          return f"{self.__technician.name}:{self.__panel.status()}"
13  technician = Technician("Mira")
14  workshop = Workshop(technician)
15  print(workshop.report())
EventtechnicianworkshopOutput
Technician createdIndependent Technician named MiraNot yet created—
Workshop createdSame independent TechnicianOwns a new ControlPanel; references the Technician—
Report printedUnchangedUnchangedMira:ready

The workshop delegates the status operation to its panel. This keeps the panel's responsibility in a separate class and lets the workshop combine component behaviour.

Ownership is a design decision

Holding an object reference does not, by itself, prove composition or aggregation. The important distinction is the intended ownership and lifetime. Creating a component internally supports composition, while receiving an independent component supports aggregation, but the complete design must preserve that meaning.

Java and Python do not provide a composition keyword that automatically destroys a component when its owner disappears. Their memory-management rules still apply. For composition, the design should avoid exposing the component for independent use outside its owner; for aggregation, independent references are expected.

Key terms: composition, aggregation, class relationships, component objects, independent, tightly coupled.

B3.2.5

SINGLETON, FACTORY AND OBSERVER PATTERNS

HL

A design pattern offers a reusable way to organize classes and objects to solve a recurring design problem. To explain one, start with the problem, identify the objects involved and show how their responsibilities solve it.

Patterns aren't complete programs to copy unchanged. They describe structures you can adapt to a particular application. Singleton, factory and observer are three important examples, though other patterns exist.

Singleton: one shared instance

The singleton pattern limits a class to one instance and provides a common way to obtain it.

Consider a greenhouse-control application that needs one Settings object to represent its active configuration. If different parts of the program create unrelated settings objects, they could use inconsistent values. A singleton makes them use the same instance instead.

The class holds a reference to its single instance. An access operation creates the instance if necessary, then returns it. Later calls return that same object. In Java, a private constructor can stop client code from calling the constructor directly. Python needs an appropriate controlled-creation approach; it cannot rely on Java-style private constructors.

In the singleton part of the figure, Client A and Client B obtain the same Settings instance through a shared access operation.

Use this pattern when one shared instance is genuinely required, not simply because an object would be convenient to access everywhere. Shared state can make components harder to isolate and test.

Factory: centralize object creation

The factory pattern puts object-creation decisions behind a common creation operation. Client code therefore doesn't have to select and construct every concrete class itself.

The greenhouse application might support TemperatureSensor and MoistureSensor objects. To obtain a sensor, client code sends a sensor type to a SensorFactory. The factory selects and returns a suitable sensor object. From there, the client uses the common Sensor operations without needing to know the concrete construction details.

In the factory part of the figure, Client passes a sensor type to SensorFactory. The factory creates either TemperatureSensor or MoistureSensor and returns it through the common Sensor interface.

Keeping creation in one place reduces repeated construction logic. If a supported sensor type changes, the creation decision can be updated there rather than throughout the client code. The factory still needs to know which concrete classes it can create. The dependency has been concentrated, not eliminated.

Observer: notify interested objects

The observer pattern lets a subject notify registered observer objects when its state changes.

In the greenhouse application, a SensorReading object could act as the subject. A Dashboard and an EventLog register as observers. When SensorReading changes, it notifies each registered observer. Each then carries out its own update operation.

The observer part of the figure shows Dashboard and EventLog registering with SensorReading. SensorReading then notifies both through a common update operation.

The subject keeps track of its observer registrations and supports adding or removing observers. It sends notifications without needing to know each observer's detailed behaviour. Depending on the design, a notification can carry the new reading or prompt observers to obtain it from the subject.

This separates the source of a change from the objects that react to it. You can register a further display without rewriting the subject's reading-management logic. Registrations still need careful management: remove an observer when it is no longer needed. Unnecessary notifications can also create extra work.

Choosing the pattern that fits the problem

Recurring problemSuitable patternMain responsibility
Several clients must use one controlled instanceSingletonControl instance creation and provide shared access
Clients should not repeat decisions about concrete object constructionFactorySelect and create the appropriate object
Several independent objects must react to a changeObserverMaintain registrations and notify observers

These patterns address different relationships. Singleton controls how many instances are available. Factory controls how objects are created, while observer controls how changes are communicated. Choose a pattern because its structure matches the problem, rather than adding patterns merely to make a design look sophisticated.

Key terms: design patterns, singleton, factory, observer, recurring programming challenges.

Were those notes helpful?

B3.1 Fundamentals of OOP for a single class