10.4.2. Using Composition

Time: 00:3:14 | Download: Large, Large (CC), Small | Streaming (CC) | Slides (PDF)
Review

The simultaneous lifetimes of the whole and its parts in a composition relationship imply that the whole constructor instantiates and initializes the parts. Consequently, chaining the constructor calls is the most challenging aspect of managing an embedded, tightly bound whole-part relationship. It's relatively easy to use composition once established. However, embedding the part within the whole doesn't circumvent the protection the private keyword provides. So, we must use the part class's public interface to access or use the data it manages.

We use part objects by sending them messages. As outlined previously, we send a message to an object using its name and the now-familiar dot operator syntax that binds an object with a member function. We'll introduce a simple display function to help illustrate the syntax for calling part member functions and how to chain member function calls together. Recall that an object's member functions can directly access its private member variables without using a selection operator.

The following examples use the display function to demonstrate how to call functions in classes related by inheritance and composition. Although the function has a void return type and doesn't have any parameters, you can generalize the display function calls to any member function.
Two classes connected by a composition relationship:
Part
--
- member : int
--
+ display() : void

Whole
--

--
+ display() : void
class Part
{
    private:
        int member;
    public:
        void display() { cout << member << endl; }
};
class Whole
{
    private:
        Part my_part;
    public:
        void display() { my_part.display(); }
};
Using simple composition. The two partial classes demonstrate how to use a simple composition relationship. The program builds the relationship with a member variable, named my_part, in the Whole class. The Whole class sends the display message to the Part class using that variable. The example also illustrates the common situation where a whole class function calls a part class function with the same name.

 

class Address
{
    private:
	string city;
	string state;
    public:
	Address(string c, string s) : city(c), state(s) {}

	void display()
	{
	    cout << city << ", " << endl;
	}
};

class Person
{
    private:
	string name;
	Address addr;					// (a)
    public:
	Person(string n, string c, string s)
            : addr(c, s), name(n) {}			// (b)

	void display()
	{
	    cout << name << end;
	    addr.display();				// (c)
	}
};

class Student : public Person				// (d)
{
    private:
	double gpa;
    public:
	Student(string n, double g, string c, string s)
	    : Person(n, c, s), gpa(g) {}		// (e)

	void display()
	{
	    Person::display();				// (f)
	    cout << gpa << endl;
	}
}
A student is a Person, and a Person has an Address by composition.
  1. Building composition: Address becomes a data type and addr the name of an object.
  2. The part object's name, in the whole object's constructor, calls the part class's constructor. The arguments in the call match the constructor's parameters.
  3. Using composition: Calls the Address display function in the part object (i.e., sends the "display" message to the part object).
  4. Building inheritance: Student is the subclass and Person is the superclass.
  5. In the subclass's initializer list, the superclass's name calls its constructor. The arguments in the call match the constructor's parameters. Initializing inheritance is always the first operation in an initializer list.
  6. Using inheritance: Calls the display function in the Person superclass.
Using inheritance and composition (example 1). Programmers implement composition by translating the UML composition connector symbol into an instance of the part class embedded in the whole. Combining relationships makes building and using them more challenging. Inheritance and composition are "strong" relationships, so the connected objects have the same or coincident lifetimes. All member variables in the example are private and only accessible outside the defining class through its public interface. The Student constructor "pushes" data into the connected object through the chained constructor calls, and the display function call "pulls" it from the objects by chaining calls to the display functions in the program:
Student valedictorian("Alice", 4.0, "Ogden", "Utah");
valedictorian.display();

 

class Pet
{
    private:
	string name;
	string vaccinations;
    public:
	Pet(string n, string v) : name(n), vaccinations(v) {}

	void display()
	{
		cout << name << " " << vaccinations << endl;
	}
};

class Person
{
    private:
	string name;
	string phone;
    public:
	Person(string n, string p) : name(n), phone(p) {}

	void display()
	{
		cout << name << endl;
		cout << phone << endl;
	}
};

class Owner : public Person				// (a)
{
    private:
	int account;
	Pet my_pet;					// (b)
    public:
	Owner(string n, string p, int a, string pn, string v)
	    : Person(n, p),				// (c)
              my_pet(pn, v), account(a) {}		// (d)

	void display()
	{
		Person::display();			// (e)
                cout << account << endl;
		my_pet.display();			// (f)
	}
};
An Owner is a Person and an Owner has a Pet by composition.
  1. Building inheritance: Owner is the subclass and Person is the superclass.
  2. Building composition: The class name Pet becomes a data type, and my_pet the name of the object.
  3. C++ uses the superclass name to call its constructors. The arguments in the call match the constructor's parameters. A call to a superclass constructor is always the first operation in an initializer list.
  4. The part object's name calls the part class's constructor. The arguments in the call match the constructor's parameters.
  5. Using inheritance: Calls the Person display function. The superclass name and scope resolution operator tie the display function to the Person class, avoiding a recursive function call to the Owner function.
  6. Using composition: Calls the Pet display function. The part object name, my_pet and the dot operator bind the display function to the Pet object - it sends the "display" message to my_pet.
Using inheritance and composition (example 2). Like the previous example, this one also has three classes related by inheritance and composition, but this example moves the part from the superclass to the subclass. Moving the part object to the subclass is reflected by where the program calls the part constructor and display functions. The example begins when the program instantiates an Owner object and sends it the "display" message.
Owner pet_owner("Dilbert", "801-555-1234", 123456, "Fido", "2020/06/01");
pet_owner.display();