10.8.1. Building Association

Time: 00:03:30 | Download: Large, Large (CC), Small | Streaming (CC) | Slides (PDF)
Review

It's often convenient to think of association as double-ended aggregation. While this conceptualization may not always work, it's a helpful starting point. Like aggregation, association has a weak or loose binding that we implement with pointers. Consequently, the program is not required to build the relationship with the constructors as it is in the case of composition. The program can create and break the association relationship binding the related objects whenever convenient. Programmers may choose to build the relationship with setter or constructor functions as best fits the problem.

Creating Association with Setter Functions

Setter functions allow programmers to establish or update the pointer variables implementing the association relationship. The program can create the associated objects on the stack or the heap. One of association's major challenges - one of the characteristics making it more difficult than aggregation to implement - is the necessity of changing both pointers when updating the relationship. Failing to change one pointer will result in incoherency and break the relationship's bidirectionality.

project.hcontractor.h
class contractor;		// forward reference

class project
{
    private:
        contractor* theContractor;

    public:
        void set_contractor(contractor* a_c)
        {
            theContractor = a_c;
        }
};
class project;			// forward reference

class contractor;
{
    private:
        project* theProject;

    public:
        void set_project(project* a_p)
        {
            theProject = a_p;
        }
};
client1.cpp (stack objects)client2.cpp (heap objects)
#include "contractor.h"
#include "project.h"

int main()
{
    project	big;
    contractor	fred;
		. . . .
    set_contractor(&fred);
    set_project(&big);
        . . . .

    return 0;
}
#include "contractor.h"
#include "project.h"

int main()
{
    project*	big = new project;
    contractor*	fred = new contractor;
		. . . .
    set_contractor(fred);
    set_project(big);
        . . . .

    return 0;
}
Creating association with setter functions. Creating an association relationship with setters requires a function in both related classes. When programmers write a setter to build aggregation, they have two choices for the function's arguments: pass an existing object or pass the "ingredients" needed for the setter instantiate a new object. While they have the same choices when building association, the former, passing an existing object, is more common. The setters can use different passing techniques - one with a pointer and the other with the object's "ingredients." (Recall that setters may not have initializer lists.)

When a client calls the setters to build or modify an association, it should call the second function immediately after the first without allowing any operations on either object between the setter calls. client1.cpp demonstrates passing stack objects to setter functions (notice the address-of operator in the function calls), while client2.cpp demonstrates passing heap objects.

Creating Association with Constructors

While programmers are not required to build association relationships with constructors, they may. Furthermore, while programmers can break or change an association relationship whenever convenient, the relationship may persist from its creation to program termination. The following examples demonstrate convenient patterns I've successfully used in the past.

project.hcontractor.h
#pragma once

#include <iostream>
using namespace std;

class contractor;			// forward dec
#include "contractor.h"

class project
{
    private:
        contractor* theContractor;

    public:
        project();			// Pair a
        project(contractor* a_c);	// Pair b
};
#pragma once

#include <iostream>
using namespace std;

class project;				// forward dec
#include "project.h"

class contractor
{
    private:
        project* theProject;

    public:
        contractor(project* a_p);	// Pair a
        contractor();			// Pair b
};
project.cppcontractor.cpp
#include "project.h"

project::project()			// pair a
{
	theContractor = new contractor(this);
}

project::project(contractor* a_c)	// pair b
{
	theContractor = a_c;
}
#include "contractor.h"

contractor::contractor(project* a_p)	// pair a
{
	theProject = a_p;
}

contractor::contractor()		// pair b
{
	theProject = new project(this);
}
client.cpp
#include "contractor.h"
#include "project.h"

int main()
{
    // Pair a
    project	little;			// stack
    project*	big = new project;	// heap
    . . . .

    return 0;
}
#include "contractor.h"
#include "project.h"

int main()
{
    // Pair b
    contractor	foo;			// stack
    contractor*	bar = new contractor;	// heap
    . . . .

    return 0;
}
Creating association with constructors. Whenever we create an association relationship, we must initialize two pointers, one in each object. Consequently, building an association relationship with constructors requires two complementary constructors and two chained constructor calls. Describing the constructors as "complementary" suggests that we must design them to work together when their calls are chained. The example demonstrates two pairs of complementary constructors. Programs may include both pairs, but only one is required. In the latter case, the problem determines which pair to use.

When member functions (such as the default constructors) call functions (such as the parameterized constructors) in the other peer, association only permits function prototypes of the called functions in the class specification.

Pair a
Construction begins when the program instantiates a project. this points to a project object that is passed to and saved by the contractor constructor, completing the association.
Pair b
Construction begins when the program instantiates a contractor. this points to a contractor object that is passed to and saved by the project constructor, completing the association.