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.
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, Enginehas-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:raiseValueError("Capacity must be non‐negative.")self._level = capacity # percentagedef use_power(self, amount: int) ->None:if amount <0:raiseValueError(f"Power usage must be non-negative.")self._level -= amountifself._level <0:self._level =0print(f"Power used: {amount}%. Remaining: {self._level}%.")def get_level(self) ->int:"""Return remaining battery charge (percentage)."""returnself._levelclass Camera:def__init__(self, resolution: str="12MP") ->None:self._resolution = resolutiondef 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 # Wattsself._is_running =Falsedef start(self) ->None:self._is_running =Trueprint(f"Motor started.")def stop(self) ->None:self._is_running =Falseprint(f"Motor stopped.")def is_running(self) ->bool:returnself._is_runningclass 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_idself._altitude =0.0self._battery = batteryself._camera = cameraself._motor = motorself._check_invariants()def _check_invariants(self) ->None:assert (isinstance(self._id, str)andself._id.isalnum()andlen(self._id) ==self._ID_LEN ), "ID must be a 6‑character alphanumeric string."assert0.0<=self._altitude <=self._MAX_ALT, "Altitude out of range."assert0<=self._battery.get_level() <=100, "Battery out of range."def get_flight_id(self) ->str:"""Return the immutable flight identifier."""returnself._iddef get_altitude(self) ->float:"""Return current altitude in metres."""returnself._altitudedef set_altitude(self, amount: float) ->float:"""Return current altitude in metres."""self._altitude = amountdef get_battery(self) ->int:"""Return remaining battery charge (percentage)."""returnself._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:raiseValueError(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)ifself._motor.is_running():self._motor.stop()self._check_invariants()def take_photo(self) ->None:ifself._battery.get_level() <5:print("Not enough battery to take photo.")returnself._camera.take_photo()self._battery.use_power(5)