The previous discussion of part ownership in an aggregation relationship suggested that a whole class can have multiple instances of a part class. Rather than using a member variable for each instance, the Warehouse class used an array of pointers, establishing multiple aggregation relationships. In addition to arrays, we can implement multiple aggregation or composition with any container or collection class. Common examples are vectors, array lists, linked lists, and hash maps, to name just a few. These structures are classes that save and organize data in various ways. While they are typically more flexible and capable than arrays, arrays are adequate in many situations.
Class developers can design and implement multiple composition and multiple aggregation - the UML and C++ support both. In this context, the most significant difference between aggregation and composition is the strength or tightness of their respective bindings. Aggregation's weak or loose binding allows programs to create or change the parts when convenient. Conversely, composition's strong or tight binding requires them to create the whole and its parts simultaneously and to maintain the relationship throughout the whole object's lifetime. (The program can't replace the parts but can change the data saved in their member variables.) When a program implements multiple composition relationships with an array, it bears the expense of constructing each object, even if only using a few. Consequently, the following discussion focuses on aggregation.
Imagine that we are programming a card game. While games have various rules, we'll assume some common values: The game has a single deck of 52 cards, and each player holds a hand with five cards. We represent these aspects of the game with three classes, Deck, Hand, and Card, organized as two whole-part relationships.
The Hand, Deck, and Card classes: An introduction to multiplicity. Modeling a player's hand as a whole class with five distinct parts is cumbersome, time-consuming, and consumes considerable diagram space. But modeling a deck of 52 cards this way is clearly impractical: it would take too much time to draw, use too much diagram real estate, and be too difficult to read.
Multiplicity Operators
The Deck and Card aggregation is an authentic problem exemplifying a pattern appearing in other complex problems. The UML defines a compact notation, called multiplicity operators, for conveying the number of parts. The notation is simple but most easily presented by examples.
(a)
(b)
(c)
(d)
(e)
UML multiplicity operators. The UML multiplicity operators are optional and unnecessary when a whole class has exactly one part. But in other cases, the operators succinctly specify the number of part classes a whole has. Class designers place the operators on the undecorated end of the aggregation or composition connector, the end nearest to the part.
A Hand has 5 Cards: A non-negative integer specifies an exact number of parts.
A Deck has 52 Cards.
A Deck has 0 to 52 Cards: .. specifies a range.
A Whole class has 0 or 1 Part.
A Whole class has any number of Parts: the asterisk indicates an unspecified number. Some designers write this multiplicity as 0..*.
Multiplicity Examples
We can implement multiple aggregation in many ways. As we explore various options, we typically encounter trade-offs between them. The following examples illustrate array and vector implementations, each offering advantages and conceding disadvantages. The examples focus on creating and using relationships and do not implement a complete game. The ellipses represent unnecessary and omitted details.
The Shuffle Operation
Although unrelated to aggregation, shuffling the cards in the deck is the most complex aspect of the examples. Randomizing the elements of an array is the inverse of ordering (i.e., sorting) them, so we use a modified version of the selection sort to shuffle the deck. The selection sort finds and selects the smallest element, min, in the sub-array between top and the array's bottom, then swaps the top and min elements. After swapping the elements, the algorithm advances top, shrinking the sub-array by one element. Equivalently, the selection sort algorithm can select the largest element and swap it with the bottom element in the sub-array.
The shuffle algorithm works similarly, randomly selecting a card and swapping it with the card at the bottom.
bottom = the last or bottom card in the deck
while (bottom != the top card in the deck)
random = a card selected at random from the cards between 0 and bottom, inclusive
swap (the random card, with the bottom card)
bottom = one card up from the current bottom
goto to 2
Multiple Aggregation With An Array
The array version implements the UML diagram illustrated Figure 2(c). The array implementation is efficient, and our familiarity with them and stacks makes this approach "comfortable." Nevertheless, it also prevents us from easily adapting the Deck class to games using larger decks.
#include <iostream> // (a)
#include <random>
#include <chrono>
using namespace std;
using namespace chrono;
class Card // (b)
{
private:
. . .
private:
. . .
void display();
};
Implementing multiple aggregation: array version. The Deck class implements multiple aggregation with an array of Card pointers. The array behaves like a Stack, with the add_card and deal functions performing the same operations as push and pop.
The <random> and <chrono> header files provide the prototypes for accessing the random number generator and system time functions.
Beyond having a display function, the Card class's details are unnecessary for demonstrating multiple aggregation.
Initially, the Deck is empty (i.e., the card count is zero), but it has a capacity of 52 aggregated cards. Please see Creating an array of objects in C++, example (c).
The constructor can fill the deck or
The program can add cards one at a time.
Once the program randomizes the cards, it deals them one at a time, removing them from the deck as it deals them.
Returns true while at least one card remains in the deck.
Randomizes the cards.
A helper function exchanging two cards in a deck as part of the randomizing or shuffling operation.
The program evaluates the two dot operators from left to right. now is a static member of the system_clock class. It returns a chrono::time_point object.
The time_point class has a member function named time_since_epoch that returns a duration object.
The duration class has a member function named count that returns a representation of the system's internal time, which the program casts to an unsigned integer.
The loop selects a card randomly and exchanges it with the card at the bottom of the deck. It moves the bottom of the deck up one card and repeats the operation.
Choosing a card at random:
uniform_int_distribution<int> is a template class that selects a value in a range. Each value has an equal probability of selection. We'll discuss template classes in a later chapter.
range is the name of the local uniform_int_distribution object and is a call to its constructor. The arguments, 0 and bottom, define the inclusive range (i.e., the least and greatest possible value) for the generated random number.
Generates a random number indicating which card is swapped or exchanged with the bottom card.
The vector class is part of the C++ standard template library or STL. The STL provides programmers with ready-made template classes (presented in a later chapter) - called containers - that store and organize data, including objects. You'll experience a more formal treatment of the STL in CS 2420. The vector version implements the UML diagram in Figure 2(d). The example consists of two variations: the first is a straightforward conversion of the array version to a vector, while the second uses iterators. An iterator is an object tightly bound to a container, allowing a program to access its stored data in various ways. (We previously used string iterators in one solution of the palindrome/number problem.) The vector and iterator operations may seem a little cryptic, but they allow us to create a Deck of any size.
To simplify the presentation, please note the following:
The Card class and main are unchanged from the previous example
The program requires one additional header file, #include <vector>
The example includes two overloaded swap functions, one for the array-like version and one for the iterator version, but a "real" program needs only one
Overloaded swap functions. If you experience difficulty understanding the data types each version uses, try drawing a picture of the cards array.
The first vector version uses this function, unchanged from the array example.
The "Element access" functions return element references, not pointers. So, taking advantage of the vector functions requires us to work with reference. Notice that temp is not a reference. The function assumes Card supports the assignment operation.