11.5.4. Actor 4

The Actor 4 example begins where the Actor 3 example ends. As illustrated in the following UML class diagram, Actor 4 adds an overloaded inserter operator (operator<<) to each of the Actor 3 classes. The inserter serves the same purpose as the display function, and a "real" program would typically have one or the other but not both.

A complex UML class diagram with five related classes:
Person
--------------------
-name : string
--------------------
+Person(n : string, s : string, c : string)
+Person(Person & : p)
+ ~Person()
+setDate(y : int, m : int, d : int) : void
+display() : void
+operator<<(out : ostream&, p : Person&) : ostream&

Address
--------------------
-street : string
-city : string
--------------------
+Address(s : string, c : string)
+display() : void
+operator<<(out : ostream&, me : Address&) : ostream&

Date
--------------------
-year : int
-month : int
-day : int
--------------------
+Date(y : int)
+display() : void
+operator<<(out : ostream&, me : Date&) : ostream&

Actor
--------------------
-agent : string
--------------------
+Actor(n : string, a : string, s : string, c : string)
+display() : void
+operator<<(out : ostream&, a : Actor&) : ostream&

Star
--------------------
-balance : double
--------------------
+Star(n : string, a : string, b : double, s : string, c : string)
+display() : void
+operator<<(out : ostream&, s : Star&) : ostream&

A composition relationship connects Person (the whole) and Address (the part). An aggregation relationship connects Person (the whole) and Date (the part). Actor is a subclass of Person, and Star is a subclass of Star.
The Actor 4 UML class diagram. The Actor 4 example adds an overloaded inserter function, operator<<, to each class. The example demonstrates how programmers can chain inserter calls together by using the class relationships that bind the objects into a working program.

The example begins execution when the program creates an instance of the Star class in Actor 3, and the constructors "push" data from the Star constructor call to the other classes. When called, the Star inserter "pulls" the information from the other classes.

UML class diagrams are language agnostic (3), and so language-specific syntax, like pass and return by reference, is typically not included in the diagrams. Nevertheless, I have included the reference notation in the Actor example to clarify and reinforce some of the patterns that help us overload the inserter operator for any class. As you study the following examples, look for these elements. The inserter always:

All the inserters in the Actor example are very sort. So, each function is implemented inside its respective class. Place these function inside the Actor 3 class specifications to create the complete, final classes.

Address
friend ostream& operator<<(ostream& out, Address& me)
{
	out << me.street << ", " << me.city << endl;
	return out;
}
Date
friend ostream& operator<<(ostream& out, Date& me)
{
	out << me.year << "/" << me.month << "/ " << me.day << endl;
	return out;
}
The part classes: Address and Date. Composition and aggregation are unidirectional or one-way whole/part relationships, and Address and Date are both parts. Therefore, neither class references any of the other classes in the class diagram. As a result, both inserter functions are very simple.
friend ostream& operator<<(ostream& out, Person& me)
{
	out << me.name << " " << me.addr << endl;	// composition and Person data
	if (me.date != nullptr)
		out << *me.date << endl;		// aggregation
	return out;
}
Person. Person is a complex class that plays two distinct roles. First, it is at the top of an inheritance hierarchy - the superclass to Actor and Star. But inheritance is unidirectional and a superclass does not "know" about its subclasses. Therefore, Actor and Star don't appear in the Person class's inserter function. But Person plays a second role as the whole to the Address and Date parts. Whole classes do "know" about their parts, and so the Person inserter must reflect that knowledge by calling the inserter functions in each of its parts in addition to printing any whole member data.

Aggregation and its pointers are the source of much of the whole class's complexity. Attempting to use a null pointer (e.g., as the left-hand operand to either the dot or arrow operator) causes a runtime error. So, we conditionally call the aggregated part only if the pointer is not null: if (me.date != nullptr). Next, no inserter function takes a pointer argument. So, we must dereference the pointer so the call matches the function's parameter list: *me.date.

Actor
friend ostream& operator<<(ostream& out, Actor& me)
{
	out << (Person &)me << " " << me.agent << endl;
	return out;
}
Star
friend ostream& operator<<(ostream& out, Star& me)
{
	out << (Actor &)me << " " << me.balance << endl;
	return out;
}
Inheritance classes. Inheritance is also a one-way relationship, but from the subclass to the superclass. So, a Star "knows about" an Actor, and an Actor "knows about" a Person. Said another way, a Star "is an" Actor, and an Actor "is a"Person. We can use the "is a" concept to chain inserter function calls together so that the subclass inserter calls its superclass's inserter. The compiler determines which inserter function to call based on the second parameter, which is a reference to the defining class, and is therefore unique for every inserter function.

To do this, we cast the second argument into an instance of the superclass: (Actor &)me and (Person &)me. Not every cast involving a reference requires the ampersand, but it is needed sometimes and it never causes a problem to include it. The cast operation doesn't modify the argument but creates an expression that matches the the second argument of the superclass constructor. So, out << (Actor &)me matches the Actor inserter's parameter list and calls that function. Similarly, out << (Person &)me matches the Person inserter's parameter list, and calls that function.