10.4.1. Building Composition

Time: 00:04:09 | Download: Large, Large (CC), Small | Streaming (CC) | Slides (PDF)
Review

Aggregation and composition are similar, constructive relationships whose primary difference is their relative binding strengths. Aggregation's weak binding allows a program to create the whole- and part objects at different times and to establish and break the connection between them at will. Alternatively, composition's strong binding requires a program to create and destroy the objects and the connection joining them simultaneously. Although C++ embeds the parts inside the whole, they are objects requiring initialization or construction. Composition requires the whole-class constructor to initialize its embedded or composed parts by calling a part-class constructor. Programmers manage the initialization process by creating chains of constructor calls - one constructor calls another. Chaining is automatic if the part class has a default constructor but requires an explicit constructor call otherwise.

Part-Classes With A Default Constructor

When available, a program can call a default constructor automatically whenever it instantiates an object. Although we don't see this implicit call in the code, the program makes the call, and the function runs.

A UML Person class diagram:

Person
--
-weight : int
-height : double
--
+Person()
class Person
{
    private:
	string	name;
	int	weight;
	double	height;
    public:
	Person() : weight(0), height(0) {}
};
An object, p, instantiated from the Person class and abstractly represented in main memory by a square. Embedded inside p are three variables represented by rectangles corresponding to the name, the weight, and the height attributes.
(a)(b)(c)
Constructing or initializing a part-object with a default constructor. Strings are so fundamental that it is easy to forget that many programming languages implement them with classes. The C++ string class defines a default constructor that creates an "empty" string object - a fully functional string that does not contain any textual data and has a length of 0.
  1. The Person class defines a default constructor, which the program calls when it creates a new Person object: Person p;
  2. The Person constructor (highlighted) is short, and a programmer can appropriately inline it. While the initializer list explicitly initializes the weight and height members, the Person constructor implicitly calls the default string constructor, which creates an empty string.
  3. An abstract representation of a Person object in memory.
Although string is a class, it represents simple, basic data. So, this example treats it like a fundamental data type.

Part-Classes Without A Default Constructor

As stated in the previous chapter, I don't advocate that every class must have a default constructor. And even when a class has a default, sometimes a constructor chain must call a parameterized constructor. Furthermore, default constructors often create "empty" objects. For example, an "empty" Person object may have an "empty" name (no characters and a zero-length) and zero-values for weight and height. If the class allows these values, we may need to program the other member functions to test for that condition and adjust their behaviors appropriately. Alternatively, the class designer may require valid data for all three members when instantiating an object, simplifying the member functions. The syntax illustrated in the following example works when the part class does not have a default constructor or the default constructor is inappropriate.

UML class diagram with two classes in a whole-part relationship. The Person 'has a' string, or a string 'is part of' a Person:

Person
--
-weight : int
-height : double
--
+Person(n : string,w : int, h : double)

The string UML class diagram elides most of the class details, showing only the copy constructor:
string
--

--
+string(n : string)
class Person
{
    private:
        string	name;
        int	weight;
        double	height;
    public:
        Person(string n, int w, double h)
            : name(n), weight(), height(h) {}
};
(a)(b)
Constructing a part-object with a parameterized constructor.
  1. This version of Person defines a parameterized constructor but not a default. So, the only way a program can instantiate a Person is by providing its weight and height as arguments to the constructor call. Furthermore, the class does not have an explicit attribute called name. In its place, it has a whole-part relationship with the string class where Person is the whole, and string is the part. As you design and implement classes with string members, choose the best representation for your problem.

    Class designers may optionally name composition and aggregation relationships by labeling the connector symbol. Labeling or naming a relationship is helpful when the whole has two unrelated parts that are the same class type. For example, if the Person class has two string parts: a name and an address. If the relationship is labeled, programmers typically use the label to name the member variable implementing it. Otherwise, they invent a suitable member name.

  2. Recall that only constructors may have initializer lists. The lists begin with a colon and initialize member variables with constructor parameters. In this example, the only way a program can create a Person object is by calling a constructor with three arguments:
    Person p("Alice", 130, 5.5);
    The syntax calling a part constructor is similar to the syntax introduced in the last chapter to initialize fundamental-type variables like integers and doubles. But this is a function call, so the number and type of arguments in the call must match the number and type of parameters in the function definition. The names in the initializer list are not arbitrary and must match the member and parameter names.

Inheritance And Composition

Classes may participate in multiple relationships at the same time. The following examples demonstrate three classes connected by two relationships: inheritance and composition. The constructors for all three classes are chained together through initializer lists. When the program instantiates an object, it passes data to the constructor. The first constructor uses some data to initialize the first object and passes the rest to the next constructor in the chain.

UML class diagram with three classes. Person and Student are related by inheritance and Person and Address by composition.

Person
--
-name : string
--
Person(n : string, c : string, s : string)

Sudent
--
-gpa : double
+Student(n : string, g: double, c : string, s : string)

Address
--
-city : string
-state : string
--
+address(c : string, s : string)
  • Person plays two roles:
    • Inheritance - a Student (subclass) is a Person (superclass)
    • Composition - a Person (whole) has an Address (part)
  • Programmers translate the UML composition symbol into a part-class member variable (which does not appear on the class diagram).
class Address
{
    private:
	string city;
	string state;
    public:
	Address(string c, string s) : city(c), state(s) {}
};

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

class Student : public Person						// (c)
{
    private:
	double gpa;
    public:
	Student(string n, double g, string c, string s)
	    : Person(n, c, s), gpa(g) {}				// (d)
}
  1. Building composition: Address is the part class and addr is the name of the object implementing composition. Programs access the part object's members through this name.
  2. Calls the Address constructor using whole-class member addr.
  3. Building inheritance: Student and Person is the superclass.
  4. Calls the superclass constructor using the superclass name. When an initializer list calls a superclass constructor to initialize inheritance, the call must be the first element in the list. The rest of the initializer list elements may be in any order.
Combining inheritance and composition (example 1). The example has three classes and two relationships: Inheritance (a Student is a Person) and composition (a Person, the whole, has an Address, the part). The constructor chain begins when the program creates a Student object:
Student valedictorian("Alice", 4.0, "Ogden", "Utah");

The classes in the following example are taken from a program used to help manage a veterinarian's office. If the veterinarian specializes in small animals, it makes sense for the program to have a Pet class. The Pet needs an Owner, so the program includes one as a subclass of Person, and we build a whole-part relationship between Pet and Owner.

UML class diagram with three classes. Person and Owner are related by inheritance, and Owner and Pet form a whole-part relationship.

Person
--
-name : string
-phone : string
--
+Person(n : string, p : string)

Owner
--
-account : int
--
+Owner(n : string, p : string, a :int, pn : string, v : string)

Pet
--
-name : string
-vaccinations : string
--
+Pet(n : string, v : string)
  • Now Owner plays two roles:
    • Inheritance: Person (subclass) is an Owner (superclass)
    • whole-part: Owner (whole) has a Pet (part)
  • Programmers translate the UML composition symbol into an Owner member variable.
class Pet
{
    private:
	string name;
	string vaccinations;
    public:
	Pet(string n, string v) : name(n), vaccinations(v) {}
};

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

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)
};
  1. Builds inheritance: Owner is the subclass and Person is the superclass.
  2. Pet is the part-class and my_pet implements the composition relationship with the whole class, Owner.
  3. Calls the superclass constructor and must be the first element in the initializer list.
  4. Calls the Pet constructor, passing it two arguments that match the function's two parameters.
Combining inheritance with a whole-part (example 2). The example has three classes connected by inheritance and composition. The example begins by instantiating an Owner object. The Owner constructor calls the Person and Pet constructors:
Owner pet_owner("Dilbert", "801-555-1234", 123456, "Fido", "2020/06/01");