Aggregation, composition and class diagrams
Has-a relationships, drawing them on class diagrams, and the design principles that favour composition over inheritance.
Do this lesson in the simulatorInheritance models "is a": a chirp is a behaviour. Most relationships between objects are "has a" instead: a rover has a buzzer, a rover has a route to follow. An object that holds references to other objects is built from them, and object-oriented design distinguishes two strengths of "has a": aggregation and composition. This lesson also covers the design principles that decide between inheritance and composition, and puts a whole design on one class diagram.
Association, aggregation and composition
An association is any relationship in which objects of one class use or refer to objects of another. Aggregation and composition are two kinds of association where one object is the whole and the others are its parts.
- Aggregation: the whole refers to parts that exist independently. The parts are usually made elsewhere and handed to the whole, they can be shared with other objects, and they carry on existing if the whole is destroyed. A rover uses a route; the route was planned before the rover existed and could be given to another rover.
- Composition: the whole owns its parts. The parts are created by the whole, belong to it alone, and are destroyed with it. A rover's buzzer is part of that rover; it makes no sense without it.
In code, the difference shows in where the part is created:
class Buzzer:
def beep(self):
print("beep")
class Route:
def __init__(self, legs):
self.legs = legs
class Rover:
def __init__(self, name, route):
self.name = name
self.buzzer = Buzzer() # composition: created here, owned by this rover
self.route = route # aggregation: created elsewhere, only referred to
patrol = Route([20, 20, 20])
scout = Rover("scout", patrol)
spare = Rover("spare", patrol) # the same route shared by two rovers
print(scout.route is spare.route)
print(scout.buzzer is spare.buzzer)
del scout
del spare
print("the route still has", len(patrol.legs), "legs")
Python destroys an object when nothing refers to it any more. del scout only removes the name scout. After both rovers are deleted, nothing refers to their buzzers, so those are destroyed with them, as composition says. The route is still referred to by patrol, so it lives on, as aggregation says.
Class diagrams for "has a"
Both are drawn as a line between the classes with a diamond at the whole's end: filled for composition, hollow for aggregation.
Put the whole picture together and a class diagram shows every kind of relationship: hollow triangles for inheritance, diamonds for aggregation and composition, and +, - and # for access.
Design principles
AQA names three principles for good object-oriented design:
Encapsulate what varies. Find the part of a design most likely to change, and put it in a class of its own behind a fixed set of methods. How the rover moves might change (driving, sidestepping, following a line), so moving belongs in its own class; the rest of the rover need not change when it does.
Favour composition over inheritance. Inheritance is fixed when the code is written, and every combination needs its own subclass: a rover that beeps, one that follows lines, one that does both, and so on. Composition builds an object from parts chosen when the program runs, and swaps them as needed.
Program to interfaces, not implementation. An interface is a set of method names and parameters that a class promises to provide, without saying how. Code that relies only on the interface, "this object has a move() method", works with any class that provides it. Java and C# have an interface keyword; Python relies on the methods simply being there.
All three at once:
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
class Straight: # two classes with the same interface: move(cm)
def move(self, cm):
forward(50, distance=cm)
class Sideways:
def move(self, cm):
right(50, distance=cm)
class Rover:
def __init__(self, mover):
self.mover = mover # composition over inheritance: the part is plugged in
def patrol(self):
self.mover.move(15) # programmed to the interface, not a class
def set_mover(self, mover):
self.mover = mover # what varies is swapped while the program runs
rover = Rover(Straight())
rover.patrol()
rover.set_mover(Sideways())
rover.patrol()
print(position())
With inheritance this would need a StraightRover and a SidewaysRover, and a rover could not change from one to the other mid-run.
Task: a rover made of parts
Write three classes from the class diagram above.
Buzzerhas one method,beep(self, note), which playsnoteHz for 0.2 seconds.Routehas a constructor__init__(self, legs)that storeslegs, a list of(cm, angle)tuples, in a private attribute;get_legs(self)returns the list;length(self)returns the total of the cm parts, worked out from the legs.Roverhas a constructor__init__(self, name, route)that stores the name, creates its ownBuzzer()in an attribute (composition), and stores therouteit was given (aggregation). Its methodrun(self)drives each leg in turn, forwardcmthen turn rightangledegrees, then beeps once at 880 Hz and prints<name> drove <cm> cm, using the route'slength().
In the main program make route = Route([(20, 90), (20, 90), (20, 180)]) and rover = Rover("scout", route), and call rover.run(). Then del rover, and print route still has <cm> cm using route.length(). Do not type the length.
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
class Buzzer:
def beep(self, note):
tone(note, 0.2)
route = [(20, 90), (20, 90), (20, 180)]
Challenges
- Give the rover a
StraightorSidewaysmover from the last cell, by composition, and use it inrun. - Is a robot's battery better modelled by aggregation or composition? What about the mat it drives on? Give reasons.
- Draw a class diagram for a
ClassroomwithStudentobjects and aRegister. Mark which relationships are aggregation and which composition.