Inheritance and Composition
Prerequisites
The code blocks in this lesson will assume that some boilerplate C++ code is present.
In particular, we will assume that the following headers are included:
#include <iostream> #include <string> #include <vector>
We will also assume that you are using the C++17 language standard, or later.
This will be the default with most modern compilers.
Furthermore, for this lesson it will be assumed you are writing all your code in a single file.
Class definitions will be assumed to exist before
main
, and all other code will be assumed to exist within main
.Relationships Between Classes
We now have a language construct for grouping data and behaviour related to a single conceptual object.
The next step we need to take is to describe the relationships between the concepts in our code.
There are two fundamental types of relationship between objects which we need to be able to describe:
- Ownership - x has a y - this is composition
- Identity - x is a y - this is inheritance
Composition
You should hopefully have come across the term composition already - in the novice Software Carpentry, we use composition of functions to reduce code duplication.
That time, we used a function which converted temperatures in Celsius to Kelvin as a component of another function which converted temperatures in Fahrenheit to Kelvin.
In the same way, in object oriented programming, we can make things components of other things.
We often use composition where we can say 'x has a y' - for example in our game, we might want to say that a character has an inventory, and that an inventory has items.
In the case of our example, we're already saying that a character has a position, so we're already using composition here.
Write an inventory
Write a class called
Inventory
that has a capacity, and a vector of Item
objects.
These should be private, with a method that adds an item unless the inventory is full, and anything else you think is relevant.Modify your
Character
class to contain an Inventory
data member.We now have several examples of composition:
- Character has a position
- Item has a position
- Characher has an inventory
- Inventory has many items
You can see how we can build quickly build up complex behaviours.
Now have a think: would it be simple to build this behaviour without classes?
It would probably be very messy.
Inheritance
The other type of relationship used in object oriented programming is inheritance.
Inheritance is about data and behaviour shared by classes, because they have some shared identity - 'x is a y'.
For instance, we might have two types of character: warriors and mages.
We can create two classes:
Warrior
and Mage
.
But, fundamentally, they are both characters and have common code such as an inventory and a position.
We should not duplicate this code.We achieve this through inheritance.
If class
Warrior
inherits from (is a) Character
, we say that Character
is the base class, parent class, or superclass of Warrior
.
We say that Warrior
is a derived class, child class, or subclass of Character
.The base class provides a set of attributes and behaviors that the derived class can inherit.
The derived class can then add or override these attributes and behaviors as needed.
This terminology is common across many object-oriented programming languages.
A Warrior class may look something like this:
class Warrior : public Character { private: int strength; public: Warrior(std::string name, int health, Position position, int inventoryCapacity, int strength) : Character(name, health, position, inventoryCapacity), strength(strength) {} void physicalAttack() { // Unique behavior for Warrior... } int getStrength() const { return strength; } };
Let's examine the syntax:
- Inheritance declaration: The colon (
:
) following the class nameWarrior
signifies inheritance.public Character
specifies thatWarrior
is a derived class of theCharacter
base class. Thepublic
keyword here specifies the type of inheritance: in this case,public
means thatpublic
andprotected
members of the base class remainpublic
andprotected
in the derived class. - Private member variable:
int strength;
declares a private integer variablestrength
which is specific to theWarrior
class. - Constructor: The
Warrior
constructor accepts the same parameters as theCharacter
constructor, plus an additionalstrength
parameter. The constructor uses a member initializer list to call theCharacter
constructor and initialize thestrength
member variable. - Methods:
void physicalAttack()
is a public method unique toWarrior
. This could be an example of method overriding, if there was aphysicalAttack()
method in theCharacter
class that we wanted to behave differently forWarrior
.int getStrength() const
is a getter method forstrength
.
Note: in this example,
Character(name, health, position, inventoryCapacity)
is the call to the base class constructor, which will be executed before the body of the Warrior
constructor.
After the base class constructor has been called, the Warrior
constructor will continue with its own initialisation, setting the value of strength
in this case.
This sequence ensures that the base class portion of the Warrior
object is properly constructed before the Warrior
constructor attempts to use it or modify it.
This is a fundamental feature of how constructors and inheritance work together in C++.Write a Mage class
Write a class called
Mage
that inherits from Character
, and give it some unique data and behaviour.Composition vs Inheritance
When deciding how to implement a model of a particular system, you often have a choice of either composition or inheritance, where there is no obviously correct choice.
For example, it's not obvious whether a photocopier is a printer and is a scanner, or has a printer and has a scanner.
Both of these would be perfectly valid models and would work for most purposes.
However, unless there's something about how you need to use the model which would benefit from using a model based on inheritance, it's usually recommended to opt for composition over inheritance.
This is a common design principle in the object oriented paradigm and is worth remembering, as it's very common for people to overuse inheritance once they've been introduced to it.
Composition, on the other hand, tends to offer greater flexibility.
It allows you to change behavior on the fly by changing the component at runtime and leads to a more decoupled system, which is easier to maintain and evolve.
The downside can be that it might result in a little more boilerplate code as you delegate methods to the component classes.
Swords and Shields
Swords and shields are types of
Item
.
A warrior can carry a sword and a shield, but a mage can only carry a sword.Update your code to reflect this, and identify the inheritance and composition necessary to achieve this.
Key Points
- Relationships between concepts can be described using inheritance (is a) and composition (has a).
Full code sample for lession
Here is working code for this lesson that defines the classes and then gives an example of how to use them.
You can also see this code in action, and play with it and run it, on Compiler Explorer:
#include <iostream> #include <vector> #include <string> class Position { public: float x; float y; Position(float x, float y) : x(x), y(y) {} }; class Item { private: std::string name; public: Item(const std::string& name) : name(name) {} std::string getName() const { return name; } }; class Sword : public Item { private: int damage; public: Sword(const std::string& name, int damage) : Item(name), damage(damage) {} int getDamage() const { return damage; } }; class Shield : public Item { private: int defense; public: Shield(const std::string& name, int defense) : Item(name), defense(defense) {} int getDefense() const { return defense; } }; class Inventory { private: int capacity; std::vector<Item> items; public: Inventory(int capacity) : capacity(capacity) {} bool addItemToInventory(const Item& item) { if (items.size() < capacity) { items.push_back(item); return true; } return false; } Item& getInventoryItem(size_t index) { return items.at(index); } }; class Character { private: static inline int characterCount = 0; std::string name; Position position; Inventory inventory; public: Character(const std::string& name, Position position) : name(name), position(position), inventory(10) { ++characterCount; } std::string getName() const { return name; } Position getPosition() const { return position; } Inventory& getInventory() { return inventory; } static int getCharacterCount() { return characterCount; } }; class Warrior : public Character { private: Sword* equippedSword = nullptr; Shield* equippedShield = nullptr; public: Warrior(const std::string& name, Position position) : Character(name, position) {} void equipSword(Sword* sword) { equippedSword = sword; } void equipShield(Shield* shield) { equippedShield = shield; } Sword* getEquippedSword() { return equippedSword; } Shield* getEquippedShield() { return equippedShield; } }; class Mage : public Character { private: Sword* equippedSword = nullptr; public: Mage(const std::string& name, Position position) : Character(name, position) {} void equipSword(Sword* sword) { equippedSword = sword; } Sword* getEquippedSword() { return equippedSword; } }; int main() { Sword sword("Excalibur", 10); Shield shield("Aegis", 5); Warrior warrior("Arthur", Position(0, 0)); Mage mage("Merlin", Position(1, 1)); warrior.getInventory().addItemToInventory(sword); warrior.getInventory().addItemToInventory(shield); warrior.equipSword(&sword); warrior.equipShield(&shield); mage.getInventory().addItemToInventory(sword); mage.equipSword(&sword); if (warrior.getEquippedSword()) { std::cout << "Warrior's sword: " << warrior.getEquippedSword()->getName() << std::endl; } if (warrior.getEquippedShield()) { std::cout << "Warrior's shield: " << warrior.getEquippedShield()->getName() << std::endl; } if (mage.getEquippedSword()) { std::cout << "Mage's sword: " << mage.getEquippedSword()->getName() << std::endl; } return 0; }