Aggregation, composition and class diagrams

Has-a relationships, drawing them on class diagrams, and the design principles that favour composition over inheritance.

A1.9Programming techniques and object-oriented programmingA level25 min

Do this lesson in the simulator

Inheritance 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")

Run this in the simulator

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.

Rover is composed of a Buzzer and aggregates a RouteBuzzer+ beep(note : Integer)Rover- name : String- buzzer : Buzzer- route : Route+ run()Route- legs : List+ get_legs() : List+ length() : Integer
Rover has a Buzzer by composition (filled diamond) and a Route by aggregation (hollow diamond)

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())

Run this in the simulator

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.

  • Buzzer has one method, beep(self, note), which plays note Hz for 0.2 seconds.
  • Route has a constructor __init__(self, legs) that stores legs, 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.
  • Rover has a constructor __init__(self, name, route) that stores the name, creates its own Buzzer() in an attribute (composition), and stores the route it was given (aggregation). Its method run(self) drives each leg in turn, forward cm then turn right angle degrees, then beeps once at 880 Hz and prints <name> drove <cm> cm, using the route's length().

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

  1. Give the rover a Straight or Sideways mover from the last cell, by composition, and use it in run.
  2. Is a robot's battery better modelled by aggregation or composition? What about the mat it drives on? Give reasons.
  3. Draw a class diagram for a Classroom with Student objects and a Register. Mark which relationships are aggregation and which composition.