10.8.2. Using Association

Time: 00:08:19 | Download: Large, Large (CC), Small | Streaming (CC) | Slides (PDF)
Supporting Terminology

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 to implement a two-player board game. The single program runs simultaneously on two computers connected over a local area network. The classes in the following figure are excerpted 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.

The UML class diagram shows four classes connected by association.

class Board
--

--
+ moveHandler() : void
+ update(m : Move) : void
--

class Model
--

--
+ validate(m : Move) : bool
+ makeMove(m : Move) : void
--

class Game
--

--
+ moved(m : Move) : void
+ makeMove(m : Move) : void
--

class Network
--

--
+ sendMove(m : Move) : void
- writeMove(m : move) : void
- readMove() : Move
--

The classes are connected by three association relationships: Board and Model, Model and Game, and Game and Network.
A typical association example. The UML class diagram presents four classes, connected by association relationships, forming part of a board game implemented as a computer program. The figure describes the high-level responsibilities of each class; the following figures examine the messages exchanged between the objects instantiated from the classes. Although not explicitly shown in the diagram, the example also assumes a Move class. Move objects encapsulate the information about a player's move made during gameplay. The program uses Move objects, passed as function arguments, to validate the move and update the displays on both computers.
Board
Implements the MVC design pattern's view and controller parts. The class is responsible for visually displaying the board and game pieces. The user interacts with the Board and pieces with mouse gestures, which the moveHandler function gets and processes. It passes the move to the Model for validation. It also provides a function, update, to reflect moves made on the remote computer.
Model
Implements the model part of the MVC design pattern by logically maintaining the game state, including the location of the game pieces. It is responsible for implementing the game rules, validating that moves conform to them, and rejecting those that do not. While the Board maintains a visual representation of the board, it's not always efficient or possible to access or convert the visual data for rule validation. The makeMove function updates the model, reflecting a move made on the remote computer.
Game
An instance of this high-level class is the first to run. It creates the other objects and initializes the relationships. It "knows about" the other classes and coordinates their operations.
Network
Manages the network connection between the two computers. It exchanges moves with the remote computer through the connection between the local and remote Network objects. The writeMove function sends a move from the local to the remote computer, and readMove receives moves from the remote computer.
Move
Encapsulates the information about the moves made during gameplay.

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 brief 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.

Scenario 1: Illegal move. Working together, sharing program responsibilities, two objects get a local player's move and determine that it violates the game's rules.
  1. The local player's mouse gestures on the Board initiates a move, calling moveHandler.
  2. The Board instantiates a Move object, encapsulating the move information, and sends it to the Model with the validate message.
  3. Based on its saved game data, the Model determines that the move is illegal and returns false.
  4. The Board receives the false reply to the validate message and returns the visual display to its pre-move state, ending the move and the scenario.
The Board and Model activation boxes illustrate the object's functions running long enough to gather information about the move and validate it against the game rules. None of the functions of the Game object run. The Network objects run continuously, listening for moves made on the remote computer.
A UML sequence diagram for scenario 2. See the table below for a description of the message sequence.
Scenario 2: Valid move. Again, sharing game responsibilities, the associated objects get a player's move, validate it, and update the local and remote displays.
  1. The local player's mouse gestures on the Board initiates a move, calling moveHandler.
  2. The Board instantiates a Move object, encapsulating the move information, and sends it to the Model with the validate message.
  3. Based on its saved game data, the Model determines that the move is legal and sends it to the local Game object with a moved message.
  4. The Game relays the move to the local Network object with the public sendMove message.
  5. The local Network sends the move to the remote Network with the private writeMove message to display the move on the remote Board. The writeMove function "hides" the details of sending data over a LAN from the rest of the program.
  6. writeMove, sendMove, and moved return void, so a "return" arrow is unnecessary.
  7. validate returns true, which is illustrated in the sequence diagram, signaling Board to finalize the move on the visible board.
The activation boxes show that all objects contribute to gathering the move data, validating it, and displaying it on both computers. Notice that the Board getMove function does not end until it receives the reply to the validate message. The Network objects behave as described in the previous figure.
A UML sequence diagram for scenario 3. See the table below for a description of the message sequence.
Scenario 3: Remote move. We can view the final scenario in two ways. We can view it as a separate, third scenario describing the program's behavior when it receives a move from the remote player. Or, we can view it as a continuation of the second scenario but with the perspective shifted to the second computer, reversing the roles of the remote and local computers.
  1. The remote player makes a move and sends it over the network connection, where the local Network object receives it. The Network object labeled "remote" in this scenario was the "local" object in scenario 2.
  2. The local Network object (the "remote" object in scenario 2) receives the move with the readMove function and sends a makeMove message to Game.
  3. The Game relays the move to the Model by sending the makeMove message.
  4. The Model updates the game state (the locations of the game pieces) and sends an update message to the Board.
  5. The Board updates the local view of the game board, ending the move.

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(...);
            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 (not a legal move)
                return false;
            g->moved(m);
            return true;
        }

        void makeMove(Move m)
        {
            // save the new state
            b->update(m);
        }
};
(a)(b)
class Game
{
    private:
        Model*   mod;
        Network* n;

    public:
        void moved(Move m)
        {
            n->sendMove(m);
        }

        void makeMove(Move m)
        {
            mod->makeMove(m);
        }
};
 
 
class Network
{
    private:
        Game* g;

    public:
        void sendMove(Move m)
        {
            writeMove(m);
        }

    private:
        void readMove(Move m)
        {
            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.
  1. 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.
  2. 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.
  3. 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.
  4. 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 class "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;
            c->display();
	}
};
class project;

class contractor
{
    private:
	string		name;
	project*	p;

    public:
	void display()
	{
            cout << name << endl;
            p->display();
	}
};
Incorrect use of association. The display functions cycle endlessly.

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; 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; 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.
  1. The project display is primary
  2. The contractor display primary