4.10 Implementing a Class using Aggregation




The Concept of Aggregation

The private data of a class may contain objects that are instances of other classes. The Location class seen in the previous section contained only built-in types (two ints). However, it is more common to find classes that use other classes, in addition to built-in types, to define their private data. An example of such a class will be seen in this section.

The term aggregation is frequently used to describe the structure of a class or object whose encapsulated data includes instances (objects) of other user defined classes. The term subobject is used to refer to each of the encapsulated objects.

Aggregation is related to, but distinct from, association. The difference between aggregation and composition is illustrated in the following figures that show the objects making up a timer system developed earlier. As shown in the next figure, creating the timer system by association involves individually creating the Clock, Counter, Textbox and Button objects, and using their methods to build up the desired structure of connections among these objects.

Composition

In this figure, the solid arrows show how the objects are connected through pointers. The StopButton object, for example, maintains a pointer to the Clock object that it controls. The dotted arrows in the figure represent the relationship between a Frame object and the user interface components that are displayed in that Frame.

The second the figure shows how the same system would appear when aggregation is used. With aggregation, the basic machinery of the timer system is encapsulated inside another object whose class, StopWatch, will be developed throughout this section. The public interface of the StopWatch class provides methods that allow the timer system to be manipulated as a whole.

Aggregation

A given class, for example the Clock class, may be used in several different aggregations. For example, a different kind of timer system might not need buttons to allow user control. This system, a SimpleTimer, could be built using aggregation as shown in the following figure.

Another Aggregation

Another timer system might be used only for the measurement of time relevant to the program and would not be display to the user. Such a timer system, an InternalTimer, could be organized as shown in the following figure.

Yet Another Aggregation

Similarly, there might be a variety of systems that aggregate together other combinations of these and other classes.

Advantages of Aggregation


There are several advantages of using aggregation. These advantages flow mostly from the use of encapsulation, but they are important to recognize and reiterate. The advantages are:

simplicity

the aggregating class or object allows the entire assembly encapsulated subobjects to be referred to as a single unit. This makes it easier to construct and manage multiple, independent instances of the system of subobjects. Image, for example, the difference in building a system that has three independent timer systems. Using composition, the code would directly manipulate 15 different objects (3 Clock objects, 3 Counter objects, 3 Textbox objects, and 6 Button objects) in addition to 1 Frame object. Using aggregation, the code would directly manipulate only 3 objects (3 StopWatch objects) in addition to one Frame object.

safety

through encapsulation the subobjects of the timer system are protected from accidental misuse by elements outside the timer system itself.

specialization

the public interface of the StopWatch class can provide operations that:

structure

the existence of the encapsulating boundary captures the intent that the components of the timer system are designed to function as a unit. Their organization and relationships are captured directly in the StopWatch class. This class can be studied and understood as a separate entity apart from any specific application.

substitution

an alternative implementation of the object defined by aggregation can be substituted without affecting other parts of the system as long as the public interface of the aggregating object remain unchanged.

The Problem of Indirect Control


When using aggregation, an important design issue is the degree of indirect control over the encapsulated objects that is reflected in the public interface of the aggregating class. Indirect control refers to the ability of the object's user to affect the detailed organization or operation of the subobjects through the public interface of the aggregating class. In the StopWatch example the following indirect control questions arise: As can be seen, these questions touch on all of the encapsulated subobjects in the StopWatch class.

Ideally, no indirect control over the encapsulated objects should be allowed. However, this may not always be reasonable or possible. However, providing excessive indirect control over the encapsulated objects begins to weaken the advantages of aggregation. In the extreme case, the aggregating class relinquishes complete control of its subobjects, becoming little more than a weak wrapper to hold the subobjects together as a group. The designer of a good class must strike a balance between providing sufficient indirect control of the encapsulated objects so as to be usable in different applications and yet not so much indirect control as to lose the benefits of aggregation.

The designer of an aggregating class has several options to deal with the issue of subobject control. First, several similar classes can be designed, each providing a different degree of control. At the expense of creating and naming several classes, this approach allows a spectrum of choices for programmers who may have varying needs for control over the subobjects. Second, the interface of the aggregating class may use default arguments so that control reverts to the aggregating class itself if these arguments are not specified. While this approach also achieves a spectrum of choices, the argument lists become more complicated and more difficult to design. Due to the ordering of the default arguments, a user of the aggregating class may find it necessary to assume more control than is desired. Third, one or more auxiliary classes can be defined for specifying the control information. An instance of this auxiliary class is provided as an argument to the aggregating class. This argument may also have a default value or the auxiliary class may have a default constructor so that users may assume no indirect control. In the case of the StopWatch class, the arrangement of the user interface components can be collected into a StopWatchLayout class. This auxiliary class would contain all of the placement and labeling information for a StopWatch. The default StopWatchLayout constructor would provide standard (default) values for this information. The constructor for the StopWatch class would have a StopWatchLayout argument whose default value is a StopWatchLayout object constructed by the default constructor of the StopWatchLayout class.

Implementing the StopWatch Class


The definition of the StopWatch class is shown below. The public interface provides methods to start and stop the timer from the program level, to identify a Frame in which the StopWatch can display its user interface components (TextBox and Buttons), and to query the StopWatch for its current elapsed time.

StopWatch Class Interface

  class StopWatch {
    private:
     Clock        clock;
     Counter      counter;
     TextBox      tbox;
     StartButton  start;
     StopButton   stop;

    public:
	   StopWatch( int interval,     // timer interval
                      Location where);  // placement of UI objects
      void Start();			// program starts timer
      void Stop();			// program stop timer
      int  ElapsedTime();		// reply time since first started 
      void ShowIn(Frame& frame);	// where to show UI objects
	    ~StopWatch();
  };

Notice that this design makes a number of decisions about the control issues raised earlier. This is not to suggest that these are the "right" decisions, only reasonable and illustrative ones.

The methods of the StopWatch class can be implemented as follows:

StopWatch Class Implementation


  void StopWatch::Start()      { clock.Start(); }

  void StopWatch::Stop()       { clock.Stop(); }

  int  StopWatch::ElapsedTime(){ return counter.Value(); }

  void StopWatch::ShowIn(Frame& frame){  
	frame.Display(start);
	frame.Display(stop);
	frame.Display(tbox); }

  StopWatch:: ~StopWatch(){}


This code illustrate how the methods of the StopWatch class achieve their effect by manipulating the internal subobjects. For example, to display a StopWatch in a given Frame, the ShowIn method simply tells the Frame object to display each of the StopWatch's user interface subobjects. Similarly, the elapsed time of the StopWatch is found simply by querying the value of the Counter subobject.

The constructor for the StopWatch class was not shown above. This constructor will be shown in the next section where the concept of constructing subobjects is considered separately.

Constructing Subobjects

The constructor of an aggregating class must insure that its subobjects are properly initialized. It is expected, for example, that when a StopWatch object is constructed, all of its subobjects are also properly constructed and ready for use.

The constructor for a subobject may be related to the constructor of the aggregating class in one of three ways:

independent
the subobject constructor is fixed and independent of the arguments of the aggregating class.

direct
the subobject constructor depends directly on one or more arguments of the aggregating class's constructor.

indirect
the subobject constructor depends on one or more values computed from the aggregating class's constructor arguments.

These relationships are shown pictorially in the following figure.

Subobject Construction

In the independent case, the subobject has a fixed constructor that does not depend in any way on the construction of the aggregating class. For example, in the StopWatch design the Counter subobject is always initialized to zero regardless of any other properties of the StopWatch object being constructed.

The direct case is illustrated by the Clock subobject in the StopWatch class. Here, the Clock object's constructor argument, the timer interval, is taken exactly from the StopWatch constructor argument. No change is made in this value.

The indirect case is illustrated by the construction of the user interface subobjects (StartButton, StopButton, TextBox). The StopWatch constructor has a single Location argument relative to which the three user interface objects are positioned. Assume that the user interface subobjects are positioned as shown in the following figure:

Layout of the StopWatch User Interface Subobjects

Using the layout picture above, the locations of the user interface subobjects would be determined as shown in the table below. The computation of the subobject locations involves invoking methods (Xcoord and Ycoord) of the StopWatch constructor argument (where), performing simple addition, and constructing a new (anonymous) Location object.

Location of User Interface Subobjects
TextBox: Location( where.Xcoord() + 10, where.Ycoord() + 10)
StartButton: Location( where.Xcoord() + 10, where.Ycoord() + 50)
StopButton: Location( where.Xcoord() + 70, where.Ycoord() + 50)

In C++, the subobject constructors are placed as shown in this general form:


   ClassName::ClassName( <argument list> ) : <subobject constructor list>
       { <body of constructor> }

where <subobject constructor list> is a comma-separated list of constructors for subobjects. This list may contain only constructors, and only constructors for subobjects.

Using the layout decisions made above and the C++ syntax just introduced, the constructor for the StopWatch class can now be written as shown in the following figure.

StopWatch Class Constructor

 StopWatch::StopWatch( int interval, Location where ) 

   :			// subobject constructor list

     count(0),		// independent
     clock(interval),   // direct
		        // the following are indirect

     tbox( Location( where.Xcoord() + 10, where.Ycoord() + 10)), 
     start(Location( where.Xcoord() + 10, where.Ycoord() + 50)), 
     stop( Location( where.Xcoord() + 70, where.Ycoord() + 50))

			// now the body of the constructor

   { clock.ConnectTo(&counter);
     counter.ConnectTo(&tbox);
   }
	


Next Stop


Exercises

  1. Add a label (a Message subobject) to the StopWatch class so that the label appears above the TextBox. Add appropriate constructor arguments to the StopWatch constructor to give a character string for the label.

  2. Implement and test the SimpleTimer class described above.

  3. Implement and test the InternalTimer class described above.

  4. Modify the StopWatch class so that it gives more control over the layout of the user interface subobjects by using default argument values on the StopWatch constructor.

  5. Implement and test the StopWatchLayout class described above.

Last Updated: March 12, 1996 / kafura@cs.vt.edu