Syntactically, programmers use associated classes similarly to how they use them in an aggregation relationship. Whole-class objects send messages to their aggregated parts with the part name, the arrow operator, and the function name. But whereas aggregation is unidirectional, association is bidirectional, meaning that both associated objects can send messages. Due to its greater flexibility, association can be more complex than aggregation.
A Typical Association Example
Association is a bidirectional relationship between two peers. Program objects can send messages to either peer, and either peer can send messages to its associate along the relationship path. Association relationships can form extended linear or web-like structures, but individual relationships always connect just two classes, and we can often understand their interactions by focusing on class pairs.
Designing With The UML
To illustrate associations, imagine writing a program implementing a two-player board game. The single program runs simultaneously on two computers connected over a local area network. The example excerpts the classes in the following figure from an upper-division course project implementing such a game. Each class has a set of distinct responsibilities. When a player moves on one computer, the move is validated, sent to, and displayed on the opposing player's computer. The classes shift program control among themselves by sending messages, allowing each to fulfill its responsibilities.
We can more easily understand how the program objects exchange messages by exploring a few scenarios. Although sequence diagrams are beyond the scope of the textbook, they are a convenient way of exploring the messages objects exchange while a program runs. We only need a few of their basic features, and the summary linked here describes them. Programs dynamically alter their execution based on tests made while running. We explore some possible variations with different scenarios that assume differing test outcomes.
Game Source Code
The following member functions focus on the syntax associated objects use to send messages to their associates - that is, on using association.
class Board
{
private:
Model* mod;
public:
void moveHandler()
{
Move m = new Move(...);
if (mod != nullptr)
mod->validate(m);
}
void update(Move m)
{
// update pieces on the board
}
};
class Model
{
private:
Board* b;
Game* g;
public:
bool validate(Move m)
{
// validate m
if (m is not a legal move)
return false;
if (g != nullptr)
g->moved(m);
return true;
}
void makeMove(Move m)
{
// save the new state
if (b != nullptr)
b->update(m);
}
};
(a)
(b)
class Game
{
private:
Model* mod;
Network* n;
public:
void moved(Move m)
{
if (n != nullptr)
n->sendMove(m);
}
void makeMove(Move m)
{
if (mod != nullptr)
mod->makeMove(m);
}
};
class Network
{
private:
Game* g;
public:
void sendMove(Move m)
{
if (g != nullptr)
writeMove(m);
}
private:
void readMove(Move m)
{
if (g != nullptr)
g->makeMove(m);
}
void writeMove(Move M);
};
(c)
(d)
Message passing in association relationships. The skeletonized classes illustrate the syntax used to send messages to (i.e., call functions in) classes related by association. The example omits some detail for simplicity, so the tests for nullptr may be unnecessary.
The moveHandler function collects the information about a move from the player's mouse gestures and encapsulates it in a new Move object. It passes the move to the Model a validate function argument.
The Model receives the move and tests its legality based on the game rules (implemented as functions not illustrated) and the current game state (the arrangement of the game pieces on the board). It returns false if the move is illegal. If the move is legal, it updates the saved game state to reflect the move, passes the move to Game, and returns true.
The Game object coordinates messages between the local and remote computers. sendMove relays local moves to the remote computer and makeMove relays remote moves to the Model.
The readMove function runs continuously but in a waiting (or quiescent or sleeping) state. It wakes when the remote Network object sends a move, and relays it to the Game.
An Atypical Association Example
Previous examples demonstrated a practice common in hierarchies: one class sharing or delegating some of its responsibilities with other classes. Each example used the display function to demonstrate the concept and syntax. In the case of inheritance, the child's display function displayed its member variables and called the parent's display function to display the inherited members. For aggregation and composition, the whole class's display function displayed the whole's members and called the parts' display functions to complete the operation. This practice is generally safe because these relationships are unidirectional, making an inadvertent cycle less likely.
Alternatively, association is a bidirectional relationship, warranting additional caution. The related classes "know about" and can send messages to each other, making it easy to create an endless cycle. The Game and Model classes each have a function named makeMove but don't exhibit this problem because the functions never call each other. The following figure illustrates how easily we can create this error:
Incorrect
class contractor;
class project
{
private:
string title;
contractor* c;
public:
void display
{
cout << title << endl;
if ( != nullptr)
c->display();
}
};
class project;
class contractor
{
private:
string name;
project* p;
public:
void display()
{
cout << name << endl;
if (p != nullptr)
p->display();
}
};
Incorrect use of association. The display functions call each other endlessly, causing runaway recursion.
Function calls chained with association must be "complementary" in the same sense previously described for constructors. Programmers must design the functions so that they work together correctly. The following figure uses the display function to illustrate pairs of complementary functions.
Correct but atypical
class contractor;
class project
{
private:
string title;
contractor* c;
public:
void display() // Pair a
{
cout << title << endl;
if (c != nullpter)
c->display();
}
void display() // Pair b
{
cout << title << endl;
}
};
class project;
class contractor
{
private:
string name;
project* p;
public:
void display() // Pair a
{
cout << name << endl;
}
void display() // Pair b
{
cout << name << endl;
if (p != nullptr)
p->display();
}
};
(a)
(b)
Correctly calling the member functions of associated classes. The easiest way to solve the problem of an endless cycle of function calls is to designate one function as the primary and the other as the secondary. The primary function calls the secondary, but the secondary does not call the primary, preventing the cycle. The details of a specific problem may dictate which function plays the primary; otherwise, we assign the roles arbitrarily.
The projectdisplay is primary - a program calls it first
The contractordisplay primary - a program calls it first