University of Wales Trinity Saint David
ACCB5017 - Games Architecture and Mechanics

Composition

Overview

This lab builds two C++ classes, Engine and Car, that show how composition lets one class reuse another by owning it rather than inheriting from it. Starting from a fully standalone Engine class, you will build a Car class that stores an Engine as a private member, initializes it through a member initializer list, and delegates its own start() to the Engine it owns. In the Inheritance lab, Car was declared class Car : public Vehicle and inherited everything a vehicle needed. This lab's Car has no base class at all. it reuses behaviour by holding an Engine as a value, the same way it holds an int or a std::string.

Learning Objectives

By the end of this lab you will be able to:

Concept

The Inheritance lab's Car : public Vehicle relationship is decided once, in the class declaration, and never changes for the lifetime of the program. Every Car is permanently a Vehicle, whether that is useful or not, and a change to Vehicle ripples into every class that derives from it, the fragile base class problem. Inheritance also struggles when two classes need to share behaviour but are not really the same kind of thing. a Car and a Motorbike both need an engine, but forcing them to share a common base class just to reuse engine logic pulls in fields like doors that make no sense for a motorbike.

Composition solves this by building a class out of other objects it owns instead of classes it derives from. A class declares another class as an ordinary private member, exactly like Car in this lab declares Engine engine; alongside its int wheels;. If the composed type has no default constructor, the owning class initializes it through the member initializer list, Car(...) : engine(engine) {}, before its own constructor body runs. The owning class then reuses the composed object's behaviour by calling its methods directly, engine.start(), a pattern called delegation, rather than by overriding or inheriting anything.

This is why composition is so often recommended over inheritance. the composed class stays completely decoupled (Engine has no idea Car exists), the relationship reuses behaviour across types that share no ancestry, and nothing about it forces a rigid hierarchy onto types that only partially resemble each other. Inheritance still earns its place when the relationship really is is-a and many types need to be treated uniformly through one shared interface, dispatched polymorphically through a base pointer, exactly the Shape* pattern from the Abstraction and Interfaces lab. Composition is the better default when a class simply needs another object's behaviour without taking on its full identity.

Tasks

Task 1 - The standalone Engine class

Open Engine.h and Engine.cpp and complete the Engine class. It knows nothing about Car and must work entirely on its own.

  1. In Engine.h, declare three private members, displacementLitres (float), horsepower (int) and fuelType (std::string).
  2. Declare a constructor taking the three matching parameters, a start() method, and three getters, getDisplacementLitres(), getHorsepower() and getFuelType().
  3. In Engine.cpp, implement the constructor by assigning each parameter to its matching member using this->.
  4. Implement start() to print the engine's displacement, horsepower and fuel type to std::cout, followed by has started.
  5. Implement the three getters to return their matching member.

Success condition: a small test in main(), Engine e(1.6f, 130, "Petrol"); e.start();, compiles and runs with no Car anywhere in the program, and prints a single line describing that engine.

Engine.h
#pragma once
#include <string>

// Engine is a complete, independent class. Nothing about it knows or cares
// that a Car is going to own one - it works just as well standing alone.
class Engine
{
private:
    // TODO: declare three private members: displacementLitres (float),
    // horsepower (int), fuelType (std::string)

public:
    Engine(float displacementLitres, int horsepower, std::string fuelType);

    // TODO: declare start() const, returning void

    // TODO: declare getDisplacementLitres() const returning float,
    // getHorsepower() const returning int, getFuelType() const returning
    // std::string
};
Engine.cpp
#include "Engine.h"
#include <iostream>

Engine::Engine(float displacementLitres, int horsepower, std::string fuelType)
{
    // TODO: assign each parameter to its matching member using this->
}

void Engine::start() const
{
    // TODO: print "Engine (<displacementLitres>L, <horsepower>hp,
    // <fuelType>) has started" followed by std::endl
}

float Engine::getDisplacementLitres() const
{
    // TODO: return displacementLitres
}

int Engine::getHorsepower() const
{
    // TODO: return horsepower
}

std::string Engine::getFuelType() const
{
    // TODO: return fuelType
}

Task 2 - Car: composed of an Engine

Open Car.h and Car.cpp and complete the Car class. Unlike the Inheritance lab's Car, this one has no base class at all. it declares an Engine as a member the same way it declares any other private field.

  1. In Car.h, declare six private members, wheels and doors (int), engine (Engine), and colour, make, model (std::string).
  2. Declare a constructor taking the matching six parameters, a start() method, six getters (getEngine() returns const Engine&), and five setters (there is no setEngine in this lab).
  3. In Car.cpp, implement the constructor. Since Engine has no default constructor, initialize engine through the member initializer list, Car::Car(...) : engine(engine), then assign the remaining five parameters to their matching members in the constructor body using this->.
  4. Implement start() to print The car has started, then call engine.start(). Car::start() does not print an engine message itself. it delegates that to the Engine it owns.
  5. Implement the six getters, and the five setters, matching the behaviour of the Inheritance lab's Vehicle. setWheels silently ignores a negative value, and setColour prints You have great taste before assigning if the given colour is "RED".

Success condition: Car.h and Car.cpp compile alongside Engine.h/Engine.cpp. Task 3 builds a full program that constructs Cars with different Engines to confirm the delegation works.

Car.h
#pragma once
#include <string>
#include "Engine.h"

// Car has no base class at all - compare this declaration to the
// Inheritance lab's Car, which reads "class Car : public Vehicle". This
// Car reuses Engine's behaviour by owning one as a member ("has-a",
// composition) instead of inheriting it from a base class ("is-a",
// inheritance).
class Car
{
private:
    // TODO: declare six private members: wheels (int), doors (int),
    // engine (Engine), colour, make, model (std::string)

public:
    Car(int wheels, int doors, Engine engine, std::string colour, std::string make, std::string model);

    // TODO: declare start() const, returning void

    // TODO: declare getWheels(), getDoors() (int), getEngine() (const
    // Engine&), getColour(), getMake(), getModel() (std::string), all const

    // TODO: declare setWheels(int), setDoors(int), setColour(std::string),
    // setMake(std::string), setModel(std::string)
};
Car.cpp
#include "Car.h"
#include <iostream>

Car::Car(int wheels, int doors, Engine engine, std::string colour, std::string make, std::string model)
    // TODO: initialize engine through the member initializer list, since
    // Engine has no default constructor for the compiler to fall back on
{
    // TODO: assign the remaining five parameters to their matching
    // members using this->
}

void Car::start() const
{
    // TODO: print "The car has started", then call engine.start() -
    // delegate the engine-starting message to the Engine this Car owns
}

int Car::getWheels() const
{
    // TODO: return wheels
}
int Car::getDoors() const
{
    // TODO: return doors
}
const Engine& Car::getEngine() const
{
    // TODO: return engine
}
std::string Car::getColour() const
{
    // TODO: return colour
}
std::string Car::getMake() const
{
    // TODO: return make
}
std::string Car::getModel() const
{
    // TODO: return model
}

void Car::setWheels(int wheels)
{
    // TODO: ignore negative values, otherwise assign to the wheels member
}
void Car::setDoors(int doors)
{
    // TODO: assign to the doors member
}
void Car::setColour(std::string colour)
{
    // TODO: if colour is "RED", print "You have great taste" first, then
    // assign it to the colour member either way
}
void Car::setMake(std::string make)
{
    // TODO: assign to the make member
}
void Car::setModel(std::string model)
{
    // TODO: assign to the model member
}

Task 3 - Building cars from independent engines

Open CompositionDemo.cpp and complete main() so it exercises Engine on its own, then two Cars each built from a different Engine.

  1. Construct a standalone Engine, Engine standaloneEngine(1.6f, 130, "Petrol");, and call start() on it directly, with no Car involved.
  2. Print a separator line, "---".
  3. Construct Engine hatchbackEngine(1.0f, 99, "Petrol");, then construct Car myCar(4, 2, hatchbackEngine, "blue", "Peugeot", "106");. Call myCar.start(), then print its make, model and getEngine().getHorsepower().
  4. Print another separator line, then construct a second car, Car myTruck(6, 2, Engine(3.0f, 210, "Diesel"), "white", "Ford", "Transit");, passing a temporary Engine straight into the constructor. Call myTruck.start() and print its make, model and horsepower the same way.

Success condition: running the program prints the standalone engine's line, then each car's own start message followed by its own engine's line, then its make, model and horsepower, with the two cars reporting completely different horsepower and fuel type because each one owns a separately built Engine. No class in this file shares a common base class with any other. every relationship here is a value one class happens to hold.

CompositionDemo.cpp
#include <iostream>
#include "Engine.h"
#include "Car.h"

int main()
{
    // TODO: construct Engine standaloneEngine(1.6f, 130, "Petrol") and
    // call start() on it directly, with no Car involved

    // TODO: print "---"

    // TODO: construct Engine hatchbackEngine(1.0f, 99, "Petrol"), then
    // Car myCar(4, 2, hatchbackEngine, "blue", "Peugeot", "106"). Call
    // myCar.start(), then print its make, model and
    // getEngine().getHorsepower()

    // TODO: print "---"

    // TODO: construct Car myTruck(6, 2, Engine(3.0f, 210, "Diesel"),
    // "white", "Ford", "Transit"), passing a temporary Engine straight
    // into the constructor. Call myTruck.start(), then print its make,
    // model and horsepower
}

Solution

Try the tasks above before revealing the solution.