10.1. Multi-Class Programs And The UML

Time: 00:10:14 | Download: Large, Large (CC), Small | Streaming (CC) | Slides (PDF)
Review
Six UML classes named A through F, connected by five class relationships. A and B are connected by inheritance: a line with an open three-sided arrowhead attached to the superclass. A and C are connected by composition: a line with a solid diamond attached to the whole class. A and D are connected by aggregation: a line with an open diamond attached to the whole class. A and E are connected by association: an undecorated solid line. A and F are connected by dependency: a dashed line with an open arrowhead attached to the dependent class. Please see Chapter 10 for more detail.
UML class relationships. The figure illustrates six UML classes connected by five UML class relationship symbols. While a class may participate in many relationships, each relationship connects only two.

Both ends of the association symbol are undecorated. The remaining relationship symbols are decorated at one end. We distinguish between the relationships based on their decorations.

Object-oriented programs consist of a collection of objects bound together and cooperating to solve a problem. We can represent the bindings with class relationships between pairs of classes. The UML defines five class relationships:

  1. inheritance (also known as generalization),
  2. association,
  3. aggregation,
  4. composition, and
  5. dependency (also known as using or delegation).

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

Sharing Responsibilities

I attended a conference in 1990, early in my studies of object orientation. One session was an open discussion quickly centering on how objects share the data they manage. The question is 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.

Sending And Receiving 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, a kind of whole-part relationship. 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 ClassWhole Class
class Engine
{
	private:

	public:
		void start()
		{
			. . . .
			. . . .
		}
};
class Car
{
	private:
		Engine motor;
	public:
		void drive()
		{
			motor.start();
			. . . .
		}
};
(a)(b)
Sending a message in a whole-part relationship. Objects communicate, cooperate, and share the program's responsibilities by sending messages to each other. "Sending a message" is an object-oriented term meaning that one object calls a member function in another object. Two classes, Car and Engine, form a whole-part relationship and illustrate message sending. Programmers create a whole-part relationship by defining a variable in the whole class (named Car) whose data type is the part class (named Engine and highlighted in blue).
  1. The Engine class has-a member function named start (in yellow), meaning that it can receive or respond to a "start" message.
  2. The Car class has-a member variable, motor (in red), through which it can send the "start" message.
Sending messages through the other relationships is similar and explored in detail later in the chapter. The example also demonstrates how one class shares its responsibilities with another. The Car class is responsible for the drive task but shares part of that responsibility with the Engine class by sending it the "start" message.

Categorizing Class Relationships

Taxonomy: "A taxonomy (or taxonomical classification) is a scheme of classification2, especially a hierarchical classification, in which things are organized into groups or types. Among other things, a taxonomy can be used to organize and index knowledge (stored as documents, articles, videos, etc.), such as in the form of a library classification system, or a search engine taxonomy, so that users can more easily find the information they are searching for. Many taxonomies are hierarchies (and thus, have an intrinsic tree structure), but not all are." - Wikipedia

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.

1. Semantics or meaning
Each class relationship has-a meaning summarized by a short natural-language phrase. (This method works in English, but I can't say how well it works in other languages.) We typically describe inheritance with one of the phrases is-a, is like a, or is-a-kind0of. For example, a Circle is-a Shape. We describe whole-part relationships with one of the phrases has-a, is-a part of, or contains. Following the example presented in Figure 2, a Car has-an Engine or an Engine is-a-part-of a Car. Unlike aggregation and composition, which read best in only one direction, association reads well in both directions. For example, a Contractor has-a Project and a Project has-a Contractor. Finally, dependency is suggested by depends on, uses, or delegates to. For example, a FractionCalculator depends on a Fraction or a FractionCalculator uses a Fraction.
2. Directionality or navigability
Every class relationship has-a sense of direction - the direction in which the relationship operates. We can describe a relationship as unidirectional, operating in only one direction, or as bidirectional, operating in both directions. Only one relationship, association, is bidirectional; the rest are unidirectional.
3. Lifetime
Lifetime is when the related objects exit and determines the relationship's persistence. Objects connected by a coincident lifetime relationship are created and destroyed simultaneously - one cannot exist without the other. The program builds the relationship when it creates the objects, and the relationship persists until it destroys them. Objects connected by an independent lifetime relationship are created and destroyed at different times. Furthermore, the program may create and destroy the relationship between the objects, without destroying either object, whenever it is convenient. Dependency differs from other relationships because its lifetime is always temporary.
4. Sharing
Two objects abstractly represented as circles and connected by a line. They are instances of the Car and Engine classes, respectively. The line represents that the objects are bound together.
A class relationship binds two objects instantiated from the related classes. We always view sharing from the perspective of one of these objects, which we'll call the "owner." Sharing indicates whether or not the owner can share the related object with other objects in the program. For example, if a Car is the owner, can it share its Engine? If the relationship is exclusive, then sharing may not take place, but sharing is allowed if the relationship is shareable.
Binding Strength
The object-oriented literature doesn't typically recognize or describe binding strength as a relationship property. Nevertheless, it is a useful way of summarizing lifetime and sharing:
  • Strong/tight binding ⇒ coincident (same) lifetimes and exclusive (no) sharing of bound objects
  • Weak/loose binding ⇒ independent (distinct) lifetimes and bound objects are shareable
  • Transient/brief/short-lived ⇒ a temporary binding to an object that is shared
Relationship properties: A categorization system. Each relationship (category) has four properties. Relationships may have some property values in common, but at least one value is different between any two relationships. In subsequent chapter sections, we explore the class relationships and their properties in more detail.
This figure presents the four properties characterizing the UML class relationships. Subsequent chapter sections explore the relationships and their properties in greater detail. To help you memorize each class's properties, I recommend downloading a blank version of the Class Relationship Summary and filling in the table as you study each relationship. The exercise will help you familiarize yourself with the similarities and differences between the various relationships and help you retain the information longer. The blank table is available in multiple formats: The completed table, also in various formats and levels of detail, is available at the end of the chapter.

Identifying Class Relationships With A Dichotomous Key

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

The key begins by asking if 'x is-a y.' If it is, the relationship is inheritance. The next or 'no' stage asks if 'x has-a y.' If it does, the relationship is a dependency. The next stage asks if 'y has-an x.' Only association reads as a 'has-a' in both directions, so the relationship is association. The last stage asks if the 'binding is weak?' Aggregation has weak binding while composition has strong binding.
A dichotomous key for identifying UML class relationships. For stages (a) through (c), say the identifying phase inside the "branching" diamond while replacing x and y with the class names. If the phrase "sounds good" in English, and perhaps equivalently in other languages - i.e., it's a sensible statement - follow the "yes" path; otherwise, follow the "no" path.
  1. Equivalently, we can ask if x and y overlap? Do they have any common features the program can reuse through inheritance?
  2. It's not always easy to identify when x depends on/uses/delegates to y. To refine the question, ask, "does x always use the same y?" Two examples will further clarify this stage. First, imagine that x is a FractionCalculator that has an add function, and y is a Fraction. Does the fraction calculator always add the same fraction, or should it add different fractions? Now imagine that x is a Person and y is a Date - perhaps the person's birth date. Does a person's birth date change? Dependency or uses best represents the first example, while the second is a "has-a" relationship.
  3. If both statements make sense (at least in English), we should use an association relationship; otherwise, aggregation or composition is a better choice.
  4. Both aggregation and composition are "has-a" relationships. For example, imagine the problem has the classes Car and Engine. The statement "a Car has-an Engine" satisfies both relationships, forcing us to examine another property to make the final choice. If we think about a Car driving down the road, it seems reasonable to have a strong binding between the classes, suggesting composition. But imagine that the problem adds a third class, Warehouse, representing a database tracking spare parts. Car and Warehouse objects could share an Engine or a damaged a damaged Engine could be removed from a Car. Both situations indicate a loose binding or aggregation. Conditions in the original problem suggest the best relationship.

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.

Disagreements About The Class Relationships

Inheritance further distinguishes itself as the only relationship whose meaning is clear, well-established, and undisputed. Unfortunately, practitioners sometimes disagree 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 languages, aggregation is easier to program than association and always easier to maintain. But more significantly, aggregation and association have different meanings, reflecting different kinds of 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 rather than 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 I believe the differences are sufficient to justify using all five in object-oriented models and programs. Once again, let me recommend that you complete 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.