Object-oriented programs consist of objects bound together and cooperating to solve problems. We can represent the bindings with class relationships between pairs of classes. The UML defines five class relationships:
UML class diagrams help us visualize the otherwise intangible relationships binding classes into programs. They consist of UML class symbols connected by decorated lines representing one of the relationships1. The diagrams describe the program's static or time-invariant structure - they form the framework upon which we build an object-oriented program. Programs instantiate the bindings from the relationships just as they instantiate objects from classes.
Inheritance plays a central role in object-oriented programming as many of the paradigm's most fundamental characteristics rely on it. Its importance among the relationships is manifest by being the second requirement, following encapsulation, of the object-oriented paradigm. Furthermore, inheritance is necessary to implement polymorphism, the third paradigm requirement.
We frequently consider composition, aggregation, and association as "constructive" relationships. They are primarily responsible for binding objects together, forming the program framework. Composition and aggregation are different ways of describing a whole-part relationship; one of the related classes plays the role of the whole, while the other plays the part. Association portrays a peer-to-peer relationship - both related classes are peers of equal "importance." Dependency is ambiguous, but we'll add detail as the discussion progresses.
Class diagrams describing large object-oriented programs can become large, complex, and confusing. We can use multiple diagrams to describe a single program to manage the complexity and confusion. We choose which classes to show on a diagram and how much detail to provide. We may show the same classes on multiple diagrams, allowing us to focus on specific details in each diagram. Furthermore, the UML includes many diagrams beyond just class diagrams. Some diagrams use different notation and techniques to present the same information, and we are free to choose the diagram that works best in a given situation. Clearly describing a program - making a program's information easily accessible - is a diagrammer's ultimate goal.
Each class in an object-oriented program bears some responsibility for solving the program's problem. This observation implies that each class has a set of responsibilities, the data it manages and the functions that operate on that data. The class must share its data and functions while maintaining the encapsulation that is a hallmark of object orientation.
I attended a conference in 1990, early in my studies of object orientation. One session, an open discussion, quickly centered on how objects share the data they manage. The question was how to maintain an object's encapsulation while it contributes to a problem solution. Two lines of thought developed: extract the data from the object to use it, or leave it in the object and use the object, letting it perform the required task. The participants were passionate and excited, with frustration growing in both camps. One participant summarized his view by observing (I'm paraphrasing after more than three decades:)
A gorilla has a liver and is responsible for it. Cutting out the liver to use it somewhere is messy and annoys the hell out of the gorilla.
Metaphorically, how do we safely use the liver if we legitimately need it but want to avoid annoying the gorilla? The best object-oriented practice is a well designed public interface. A class is responsible for maintaining its member variables and providing meaningful member functions that operate on them. Rather than extracting data from an object, the program "asks" it to perform the needed operation. In some cases, the program can ask the object directly. In other cases, it asks indirectly through an instance of a related class. The latter case exemplifies one class sharing some of its responsibilities with another. The program requests objects to operate, fulfilling their responsibilities, by sending them messages.
In the vernacular of the object-orientated paradigm, "sending a message" means an object calls a member function in a related object. Programs can pass messages along any of the class relationship pathways illustrated in Figure 1, but the following example illustrates message passing with composition, one of the whole-part relationships. Whole-part relationships (described in greater detail later in the chapter) build large, complex classes from small, simple ones. Programmers implement composition with a member variable defined in the whole class; the variable manages an instance of the part class.
Part Class | Whole Class |
---|---|
class Engine { private: public: void start() { . . . . . . . . } }; |
class Car { private: Engine motor; public: void drive() { motor.start(); . . . . } }; |
(a) | (b) |
When a software developer specifies a class as a part of a problem solution, it must reflect or mirror the relevant details of the problem with its member variables and functions. Programmers can only solve elementary problems with a single-class program; solving more complex problems requires multiple classes. Developers connect the individual classes in an object-oriented program with one of the UML class relationships. Just as a class's features must reflect the parts of the problem they help solve, the relationships between classes must also reflect the interconnections developers observe in the real world between the problem parts. Each relationship has a meaning that developers must match to the conditions found in the problems their programs solve.
When software developers analyze a new problem using the object-oriented paradigm, they create an initial model of the problem. The model consists of connected classes, where each connection is an instance of one class relationship. Building an accurate model requires developers to choose an appropriate relationship. To help us decide, we develop the following taxonomic classification or categorization2 system.
Each class relationship forms one group or category with four dimensions or properties. Each property has two or three values that help distinguish between the relationships. Some relationships share many properties, but at least one is always different. I've described each property value with a word or short phrase to facilitate creating the summary tables at the end of the chapter. Nevertheless, the value's meaning is significant, but not the word or phrase, so I encourage you to use words or phrases that clarify that meaning for you.
A dichotomous key is "a key used to identify a plant or animal in which each stage presents descriptions of two distinguishing characters, with a direction to another stage in the key, until the species is identified." Although we often use dichotomous keys to identify plants or animals (see BioNinja for examples), we can construct dichotomous keys to search or index any taxonomic system. The following key will help identify the class relationships between the classes found during object-oriented analysis. While biologists can often use unambiguous features (e.g., the number of hairs between a spider's claws) in their keys, the relationships between classes can be unclear, and we may need to examine multiple properties at any given stage.
Theoretically, we can uniquely identify the relationship between any two classes in a model by recognizing the property values of the connecting relationship. Practically, we can represent some constructive connections with more than one relationship. However, one relationship will generally reflect reality better than the others, but which is best is a function of the problem. If you can justify your chosen relationship (beyond making an "easy," "convenient," or default choice), and if your model is cohesive, then your selection will likely work.
Inheritance further distinguishes itself as the only relationship whose meaning is clear, well-established, and undisputed. Practitioners sometimes have differing views about what the remaining relationships mean or if they contribute value to object orientation.
The constructive relationships, association, aggregation, and composition, have many common properties. Their similarities cause confusion and disagreement over their interpretation. Summarizing the confusion, Fowler3 states:
One of the most frequent sources of confusion in the UML is aggregation and composition. It's easy to explain glibly: Aggregation is the part-of relationship. It's like saying that a car has-an engine and wheels as the parts. This sounds good, but the difficult thing is considering what the difference is between aggregation and association.
In the pre-UML days, people were usually rather vague on what was aggregation and what was association. Whether vague or not, they were always inconsistent with everyone else. As a result, many modelers think that aggregation is important, although for different reasons. So the UML included aggregation . . . but with barely any semantics (p. 67).
Perdita & Pooley4 suggest that we could do without aggregation and composition altogether:
In our experience people new to object-oriented modeling use aggregation and composition far too often. Remember that both are kinds of association, so whenever an aggregation or composition is correct, so is plain association. If in doubt, use a plain association (p. 76).
Perdita & Pooley's advice, "If in doubt, use a plain association," echoes the recommendation I received as a working software engineer transitioning from procedural to object-oriented programming. After working with object orientation for most of my career, I now agree with Horstmann & Cornell5:
Some methodologists view the concept of aggregation with disdain and prefer to use a more general 'association' relationship. . . . But for programmers, the 'has-a' relationship [aggregation or composition] makes a lot of sense (p. 130).
In most program languages, aggregation is easier to program than association and always easier to maintain during execution. But more significantly, aggregation and association have different meanings, reflecting different interconnections between parts of a problem. In contrast to Perdita & Pooley, I suggest, "If in doubt, look at the problem more closely." Association may be the best relationship, but choose based on deliberate and informed principles - matching the relationship's semantics to the problem's features - rather than relying on thoughtless default recommendations.
If association, aggregation, and composition are all confusingly similar, dependency appears, by definition, to describe all UML relationships. Booch, Rumbaugh, and Jacobson6, the original authors of the UML, offer the following definition:
A dependency is a semantic relationship between two model elements in which a change to one element (the independent one) may affect the semantics of the other element (the dependent one) (p. 24).One of the primary reasons for building class relationships in an object-oriented program is to allow the objects to cooperate by sending messages. Any change to the receiving class's public interface would necessarily change how the objects cooperate, which implies a dependency between any two related classes. So, as Fowler3 notes, "Many UML relationships imply a dependency" (p. 48).
I maintain that all UML class relationships, including the three constructive ones, are distinct and individually identifiable, and the differences are sufficient to justify using all five in object-oriented models and programs. Again, I recommend completing a blank relationship table (linked in the yellow box above) as you study each relationship in the following sections. I believe that knowing the relationships' similarities and differences will help you use them more effectively, enabling your models and programs to reflect the original problem with greater fidelity.