Managed Containers, Composites, and Aggregates
Value containers are containers of uniform (same-typed) values, for example, QString, byte, int, float, etc. Pointer containers are containers of pointers to (polymorphic commonly typed) objects. They can be managed or unmanaged.
Both kinds of containers can grow at runtime by allocating additional heap memory as needed. This is always done in an exception-safe way, so you don't need to worry about possible memory leaks.
In the case of pointer containers to heap objects, however, one must decide which class is responsible for managing the heap objects. UML diagrams can distinguish between managed and unmanaged containers by using composite (filled diamond) and aggregate (empty diamond) connectors, as shown in Figure 10.2.
Figure 10.2. Aggregates and compositions
In general, we can say that a managed container is a composite, because the container manages its pointed-to objects. In other words, when a composite is destroyed, it destroys (cleans up) its entire self (because the smaller objects are part of its composition).
When one object embeds another as a sub-object, it is also considered a composition.
In Figure 10.2, there are two kinds of Customer containers, CustomerList and CustDb. CustDb and CustomerList both reuse template containers. CustomerList objects are aggregatestemporary structures to hold the results of a query, or a user selection. CustDb, on the other hand, is a singleton composite that manages all of the Customer objects that exist.
In the case of the Customer and Address relationship, this diagram indicates that one or more Address objects should be associated with a particular Customer. When the Customer object is destroyed, it is reasonable to destroy all of its Address objects at the same time. Thus, the Customer object manages its Addresses, which gives us another example of a composite relationship.
This suggested design does impose some limitations on possible use of Address; in particular, there is no easy way to find all customers at a particular address. If Address and Customer were independently managed, then we could form bidirectional relationships between the classes. |
Typically, a managed container deletes any heap objects it "owns" when the container itself is destroyed. With a Qt container of pointers, one can use qDeleteAll(container), an algorithm that calls delete on each element in the container.
Copying a managed container can be defined in a number of ways:
- For some containers, the feature might be disabled.
- For others, it might be called a deep copy, where all contained objects are cloned and placed in the new container.
- Another approach, taken with the design of Qt containers, is implicit sharing, explained in the next section.
When a container only provides an indexing or reference navigation mechanism to its objects we call it an aggregate container.
In this case, the container does not manage its objects, it only provides a convenient way to access them. When an aggregate container is copied, only references to the collected objects are copied. When an aggregate container is deleted, only the references are removed. There is no impact on the underlying objects in the container.
A managed container is a composition, and an unmanaged container of objects is usually (but not always) represented in a UML diagram as aggregation. |
Exercise: Managed Containers, Compositions and Aggregates
This exercise involves designing some data types to represent a deck and a hand of cards. The following UML diagram suggests one way of representing them.
Here are some hints:
- The CardDeck constructor generates a complete deck of 52 cards in a convenient order.
- CardDeck::deal(int k) should use the random() function from to pick k cards from the deck (removing each one from the deck after it is picked) to fill a CardHand object.
- Initialize the random() function from the system clock so that the results will be different each time you run the app. The syntax is
srandom(time(0));
- Evaluate the hand using the rules of the game of bridge: Ace =4, King =3, Queen =2, Jack =1; all other cards have zero value. Use this formula to calculate the return values for the getValue() functions.
- Example 10.6 gives a piece of client code that you can start with for testing.
Example 10.6. src/cardgame/datastructure/cardgame-client.cpp
#include "carddeck.h" #include using namespace qstd; <-- 1 int main() { CardDeck deck; CardHand hand; int handSize, playerScore, progScore; cout << "How many cards in a hand? " << flush; handSize = promptInt(); do { hand = deck.deal(handSize); cout << "Here is your hand:" << endl; cout << hand.toString() << endl; playerScore = hand.getValue(); cout << QString("Your score is: %1 points.") .arg(playerScore) << endl; // Now a hand for the dealer: hand = deck.deal(handSize); progScore = hand.getValue(); cout << "Here is my hand:" << endl; cout << hand.toString() << endl; cout << QString("My score is: %1 points.") .arg(progScore) << endl; cout << QString("%1 win!!") .arg((playerScore > progScore)?"You":"I") << endl; } while(more("hand")); }
(1)for cout, endl, and more()