Finding Relationships Between Classes

The immediate extension of the previous step, deciding the features of each class, is deciding how the classes use each other's features. While some of the classes you identify can exist in isolation, many of them cannot. Classes build upon and cooperate with other classes.

Often one class depends upon another class because it cannot be used unless the other class exists. This is necessary when one class calls the member functions of the other class. For example, a Time class may have conversions functions that provide conversions between it and a String object. Such functions must call the constructor and access functions of the String class.

Another way one class can depend on another is when it has the other class embedded within it, meaning that it contains objects of the other class as members. For example, a Circle object might have a Point object representing its center, as well as an integer representing its radius.

This type of relationship is called a “containing relationship.” Classes that contain other classes are “aggregate” or “composite classes.” The process of containing member objects of other classes, known as “composition,” was described in Chapter 4. Composition is sometimes confused with inheritance; the distinction between these two is discussed in the next section.

Most relationships between classes arise because one class's interface depends on another. For example, the class Circle may have a getCenter function that returns a Point object, so users must know about Point to use Circle 's interface. However, it is also possible for a class's implementation to depend on another class. For example, you might design AddressBook with a private member object of the SortedList class. Users of AddressBook don't need to know anything about SortedList. They need only know about the interface of AddressBook. This provides another layer of encapsulation, since it is possible to change the implementation of AddressBook without changing the interface.

When identifying relationships, you must consider how a class performs its assigned behavior. Does it need to know information that is maintained by other classes? Does it use the behavior of other classes? Conversely, do other classes need to use this class's information or behavior?

As you define the relationships between classes more fully, you'll probably modify some of the decisions you made in previous steps. Information or behavior that was previously assigned to one class may be more appropriate in another. Don't give objects too much information about their context. For example, suppose you have a Book class and a Library class for storing Book objects. There's no need for a Book object to know which Library holds it; the Library objects already store that information. By adjusting the borders between classes, you refine your original ideas of each class's purpose.

You might be tempted to use friend classes when you have one class that needs special knowledge about another class. However, the friend mechanism in C++ should be used very sparingly, because it breaks the encapsulation of a class. Modifying one class may require rewriting all its friend classes.

After identifying the relationships that one class has with others, you reach a closer approximation of the class's interface. You know which attributes require member functions to set them and which attributes require only query functions. You have a clearer idea of how best to divide the class's behavior into separate functions.