Composition

Introduction to Software Engineering (CSSE 1001)

Author

Paul Vrbik

Published

May 21, 2026

Code Reuse in OOP

In software engineering, we follow the DRY (don’t repeat yourself) principle. In general, duplication and copy/paste are bad, because among other things, it:

  • Increases risk of bugs
  • Adds maintenance overhead
  • Leads to poor readability
  • Leads to code bloat
  • Signals that a common abstraction / refactoring is needed

There are two major strategies for reuses classes in a new class:

  • Composition
  • Inheritance

In these notes, we will discuss composition.

Composition

  • We can use objects built from other classes (implemented by us or others) as attributes of our new class/object
  • Not new – we have already used integers, strings, lists, … as attributes for our classes
  • Can do this with other (more complex) classes as well
  • For example, if we are implementing a “Robot” class, we can use an object from the “Battery” class (that’s already implemented by us or others) as one of its attributes

Modularity:

  • A “Student” has a “String” (e.g., to store the student’s name)
  • A “Robot” has a “Sensor”, “Motor”, etc

Composition (Has-a)

Composition (the has-a relationship) is a popular alternative to Inheritance (the is-a relationship; see Inheritance notes).

When an object (say Car) includes another object inside of itself (say Engine) this forms a has-a relationship.

class Engine():
    def __init__(self) -> None:
        self._odometer = 0
    
    def add_to_odometer(self, amount: int) -> None:
        self._odometer += amount

    def read_odometer(self) -> str:
        return self._odometer
    
class Car:
     def __init__(self):
         self._engine = Engine()   # Car has-a Engine
     
     def read_odometer(self) -> str:
         return self._engine.read_odometer()
    
car = Car()
car.read_odometer()
0
car._engine.add_to_odometer(20)  # Bad practice (private variable access)
car.read_odometer()
20

The has-a relationship also applies to all the attributes of an object. That is, Engine has-a odometer.

DroneFlight example

DroneFlight class (recap)

You have joined a start-up that records quick test flights for hobby drones. Write a minimal DroneFlight class that always keeps its state valid by enforcing these representation invariants:

  • Drone ID - exactly six alphanumeric characters (e.g., “A1B2C3”).
  • Altitude - must stay within 0 m -120 m, the CASA (Civil Aviation Safety Authority) legal ceiling for recreational drones.
  • Battery - an integer percentage in the range 0 - 100.

Composition vs inheritance

Which one sounds clearer:

“A Drone is a Camera” or “A Drone has a Camera”?

Composition (DroneFlight Class)

A drone “has a” camera, “has a” motor, and “has a” battery.

DroneFlight Example

class Battery:
    def __init__(self, capacity: int = 100) -> None:
        if capacity < 0:
            raise ValueError("Capacity must be non‐negative.")
        self._level = capacity  # percentage

    def use_power(self, amount: int) -> None:
        if amount < 0:
            raise ValueError(f"Power usage must be non-negative.")
        self._level -= amount
        if self._level < 0:
            self._level = 0
            print(f"Power used: {amount}%. Remaining: {self._level}%.")

    def get_level(self) -> int:
        """Return remaining battery charge (percentage)."""
        return self._level


class Camera:
    def __init__(self, resolution: str = "12MP") -> None:
        self._resolution = resolution

    def take_photo(self) -> None:
        print(f"Photo taken at {self._resolution} resolution.")


class Motor:
    def __init__(self, power_rating: float = 100.0) -> None:
        self._power_rating = power_rating  # Watts
        self._is_running = False

    def start(self) -> None:
        self._is_running = True
        print(f"Motor started.")

    def stop(self) -> None:
        self._is_running = False
        print(f"Motor stopped.")

    def is_running(self) -> bool:
        return self._is_running


class DroneFlight:
    _ID_LEN = 6
    _MAX_ALT = 120.0  # metres (CASA limit)

    def __init__(self, flight_id: str, battery: Battery,
                 camera: Camera, motor: Motor) -> None:
        self._id = flight_id
        self._altitude = 0.0
        self._battery = battery
        self._camera = camera
        self._motor = motor
        self._check_invariants()

    def _check_invariants(self) -> None:
        assert (
            isinstance(self._id, str)
            and self._id.isalnum()
            and len(self._id) == self._ID_LEN
        ), "ID must be a 6‑character alphanumeric string."
        assert 0.0 <= self._altitude <= self._MAX_ALT, "Altitude out of range."
        assert 0 <= self._battery.get_level() <= 100, "Battery out of range."

    def get_flight_id(self) -> str:
        """Return the immutable flight identifier."""
        return self._id

    def get_altitude(self) -> float:
        """Return current altitude in metres."""
        return self._altitude

    def set_altitude(self, amount: float) -> float:
        """Return current altitude in metres."""
        self._altitude = amount

    def get_battery(self) -> int:
        """Return remaining battery charge (percentage)."""
        return self._battery.get_level()

    def __str__(self) -> str:
        return (f"DroneFlight({self._id}, alt={self._altitude:.1f} m, "
                f"bat={self.get_battery()}%)")

    def ascend(self, metres: float) -> None:
        """Climb *metres* metres (cannot exceed the legal ceiling)."""
        if metres < 0:
            raise ValueError(f"ascend()expects a positive distance.")
        self.set_altitude(min(self._altitude + metres, self._MAX_ALT))
        self._check_invariants()

    def land(self) -> None:
        """Land the drone and consume 5% battery."""
        self.set_altitude(0.0)
        self._battery.use_power(5)
        if self._motor.is_running():
            self._motor.stop()
        self._check_invariants()

    def take_photo(self) -> None:
        if self._battery.get_level() < 5:
            print("Not enough battery to take photo.")
            return
        self._camera.take_photo()
        self._battery.use_power(5)
b = Battery()
c = Camera()
m = Motor()
d = DroneFlight("SAD564", b, c, m)
d.ascend(80)
d.take_photo()
Photo taken at 12MP resolution.
d.land()
print(d)
DroneFlight(SAD564, alt=0.0 m, bat=90%)

Hot-Swapping

class ThermalCamera:
    def __init__(self, resolution: str = "12MP"):
        self.resolution = resolution

    def take_photo(self):
        print(f"Thermal image saved.")


b = Battery()
c = Camera()
m = Motor()
d = DroneFlight("SAD564", b, c, m)
d.take_photo()
Photo taken at 12MP resolution.
d._camera = ThermalCamera()  # Bad practice (accessing private variable)
d.take_photo()
Thermal image saved.

Summary

  • Build by pieces: snap self-contained objects together to create a richer whole.
  • Modular: each component handles one clear task
  • Reusable: same parts work in new contexts; no code rewrite
  • Readable: data flow is explicit and easy to trace
  • Few side effects: isolated state keeps surprises and bugs low