Composition and Its Implementation

What is composition?

Complex objects are typically constructed by assembling smaller and simpler components. For instance, a car is composed of wheels, doors, an engine, and a frame, while a computer comprises a CPU, a hard disk, a motherboard, and several other components. Similarly, the human body comprises various parts, such as the head, legs, arms, etc. The process of creating a more intricate object by combining simpler ones is referred to as composition.

Object composition can generally be considered to establish a HAS-A or WHOLE-PART relationship between two objects. For instance, a car HAS-A tire(s), a computer HAS-A motherboard, and a human HAS-A leg(s). The complex object involved in the relationship is commonly referred to as the whole or the parent, while the simpler object is often known as the part, child, or component.

Why composition?

Object composition proves to be beneficial as it enables us to construct intricate classes by aggregating simpler, more manageable components. This results in decreased complexity and enables faster and more error-free coding because we can reuse the code of individual composed unit (created, implemented, and tested separately) that has already been tested and verified to function properly.

Real-life example

To exemplify composition relationships, let’s consider the connection between a car and its engine. A car’s engine is an essential part of its body. It can’t function without it. A part in a composition relationship can only belong to one object at a time. Once an engine is installed in a car, it can’t simultaneously power another vehicle.

In a composition relationship, the object controls the existence of its parts. Typically, the part is created with the object’s creation and is destroyed when the object is no longer in use. Therefore, the user does not need to manage the part’s lifetime. For instance, when a car is manufactured, the engine is created and installed within the vehicle. When the car is eventually scrapped, the engine is also scrapped. This aspect of composition is sometimes called a death relationship.

Furthermore, the part is unaware of the whole’s existence. The engine works independently and is unaware of the larger structure in which it is placed. We call this a unidirectional relationship since the car recognizes the engine, but the engine doesn’t recognize the car.

Let’s draw the class diagram:

Press + to interact
An example of composition
An example of composition

It is crucial to note that the transferability of parts is not a consideration in composition relationships. For instance, an engine can be removed from one car and placed in another, but once installed in the new car, it becomes a part of that car’s composition.

Implementation of composition in C++

In C++, the composition relationship can be implemented in at least two ways.

  • One approach is to declare an object as a private or public data member and initialize it in the member initialization list of the whole constructor. This involves calling the constructor of the part class’s object, ensuring that the part is created before the whole. This technique is a nice application of member initialization.

  • Another recommended method is to use a pointer-based placeholder and allocate the dynamic object during the constructor or any member function. By using a pointer, we can manage the object’s lifecycle and delete the part when needed to free the memory occupied by the whole. This ensures proper memory management and prevents memory leaks. The choice of implementation depends on the specific requirements of the program and the relationships between the objects.

Example: Engine-car composition relation

Let’s write down the first implementation in C++ according to the example above:

Press + to interact
#include <iostream>
#include <string>
using namespace std;
class Engine
{
string company_name;
public:
Engine(string n)
{
company_name = n;
cout << "An engine of " << company_name << " has been installed." << endl;
}
~Engine()
{
cout << "An engine of " << company_name << " has been destroyed." << endl;
}
};
class Car
{
private:
Engine engine;
string owner;
public:
Car(string eng_name, string own_name)
:engine(eng_name) // member initialization list
{
owner = own_name;
cout << "A car has been created and its owner is: " << endl;
}
~Car()
{
cout << "The car has been destroyed which was the ownership of: "
<< owner << endl;
}
};
int main()
{
Car car("NISSAN", "Bob");
return 0;
}

The given code demonstrates object composition, where the Car class is composed of an Engine object.

  • In the order of creation, the Engine object is created first because it is a member of the Car class. Inside the Car constructor, the Engine object is initialized using the eng_name parameter passed to the Car constructor. Therefore, when a Car object is created, an Engine object is created as well.

  • In the main() function, the statement Car car("NISSAN", "Bob"); creates a Car object named car. This triggers the constructor of the Car class. Inside the Car constructor, the Engine object is created using the provided eng_name parameter, which, in this case, is "NISSAN". So, the output would be:

An engine of NISSAN has been installed.
A car has been created and its owner is: Bob
  • When the program reaches the end of the main() function, the Car object car goes out of scope, and its destructor is called. Inside the Car destructor, the message The car has been destroyed which was the ownership of: Bob is displayed.

  • Since the Engine object is a member of the Car class, its destructor is automatically called when the Car object is destroyed. Inside the Engine destructor, the message An engine of NISSAN has been destroyed. is displayed.

  • Therefore, the destruction order is as follows:

    • Car object car (which triggers the destruction of the Engine object)

    • Engine object (owned by Car)

Remember: The order of creation and destruction follows the order of composition, where the composed object (Engine) is created before the owner object (Car) and destroyed after it.

Exercise: Managing university departments with composition

You are tasked with implementing a program to manage departments in a university using object composition. Each department has a name, and the university can have a maximum of five departments. Implement the University and Department classes to satisfy the following requirements:

  • The Department class should have a constructor that takes a string parameter representing the name of the department. When a Department object is created, it should display a message indicating the creation of the department. The Department class should also have a destructor that displays a message indicating the destruction of the department.

  • The University class should have a static (fixed size) array of Department pointers with a maximum capacity of five. It should also have member variables for the current size of the array and the maximum capacity. Implement a member function called insertNewDepartment() that takes a string parameter representing the name of the department to be created and inserted into the array. The insertNewDepartment() function should dynamically create a new Department object with the provided name and insert it into the array. If the array is already at maximum capacity, it should display a message indicating that the department cannot be inserted.

  • The University class should have a destructor that deallocates the memory occupied by the Department objects in the array.

Implement the classes and demonstrate their functionality by creating a University object and inserting departments into it using the insertNewDepartment() function.

Write a program that demonstrates the functionality of the University and Department classes. The program should create a University object, insert at least three departments into it, and attempt to insert more than the maximum capacity to test the handling of a full array.

Ensure that the program displays appropriate messages when departments are created, inserted, and destroyed.

Example output:

Department "Computer Science" has been created.
Department "Computer Science" has been inserted into the University.
Department "Mathematics" has been created.
Department "Mathematics" has been inserted into the University.
Department "Physics" has been created.
Department "Physics" has been inserted into the University.
Department "Chemistry" has been created.
Cannot insert a new department. University capacity is full.
Department "Computer Science" has been destroyed.
Department "Mathematics" has been destroyed.
Department "Physics" has been destroyed.

Note: Remember to implement the necessary destructors to ensure proper memory deallocation.

Press + to interact
Example of composition
Example of composition

Your implementation

Enter your implementation in the following code widget. If you have trouble getting to the solution, you can click the “Show Solution” button to see how to solve the problem.

Press + to interact
Department.h
main.cpp
University.h
#include "Department.h"
class University
{
private:
static const int MAX_DEPARTMENTS = 5;
Department* departments[MAX_DEPARTMENTS];
int capacity;
int size;
public:
University() : capacity(MAX_DEPARTMENTS), size(0) {}
void insertNewDepartment(string name)
{
// Write your code here
}
~University()
{
// Write your code here
}
};