Every class bears some of the program's responsibilities. Considering how a class manages its responsibilities is the easiest way to understand the dependency relationship. Graddy Booch, James Rumbaugh, and Ivar Jacobson unified or merged three earlier object-oriented modeling languages to create the Unified Modeling Language (UML). One predecessor language, the Booch Technique1 (p. 107), formally named the relationship "using" but also called it "delegation" and "dependency." Booch described it as one class "uses the services of the" other class (p.130). Alternatively, to fulfill its responsibilities, the first class delegates some of its responsibility to the second class or depends on the second class to fulfill its responsibilities. The Object Modeling Technique (OMT)2 called the relationship "delegation" (p. 284). From this perspective, one class delegates some of its responsibilities to another.
A basic premise of the chapter is that we can characterize the class relationships with a set of properties and that each relationship has a unique set of property values. While some relationships share some property values, any two relationships differ by at least one property value, making each relationship unique. Dependency's most significant property is its binding strength, which affects the objects' relative lifetimes, shareability, and how programmers implement it. While we implement inheritance with dedicated syntax and the three constructive relationships with class-scope variables, dependency is the only relationship we implement with local-scope variables.
void Deck::shuffle()
{
default_random_engine rng((unsigned)(system_clock::now().time_since_epoch().count()));
for (int bottom = count - 1; bottom > 0; bottom--)
{
uniform_int_distribution<int> range(0, bottom);
int random = range(rng);
swap(random, bottom);
}
}
(c)
Implementing a dependency relationship. A program creates a dependency relationship when it calls a function that creates a local object (i.e., a variable of class type). The local variable is often an object passed to one of the function's parameters. Local variables, including parameters, go out of scope when a function returns. So, a dependency relationship only exists while a function is active or running, making it a temporary or transient relationship.
The class diagram for a dependency relationship. The object-oriented paradigm formally labels the classes in a dependency relationship as "dependent" and "independent." The less formal but often more meaningful names "user," "client," "supplier," and "server" are also common. (The informal names do not form pairs; for example, we can use "client" in conjunction with "supplier" or "server.") I find the terms "client" and "supplier" easier to remember, and they characterize the class roles well.
Implementing a dependency relationship as a function parameter is a well-accepted UML (versions 1 and 2) practice. The program can pass the supplier object by value, pointer, or reference. The function's return type is insignificant - it may be any type, including void. The parameter (f in this example) is a local variable that goes out of scope when the function returns, breaking the relationship.
The UML 1 standard also described creating a dependency relationship with non-parameter local variables and included stereotypes to specify the implementing variable's scope: «parameter», «local», and «global» (Fowler4, p. 49). Newer UML standards do not include this usage. The shuffle function from the previous multiplicity discussion illustrates this version. default_random_engine is a class name, and rng is a local variable that goes out of scope when the function ends. Most block-structured programming languages, including C++, allow programmers to nest blocks and their scopes, but dependency relationships don't distinguish between nested and wider scopes.
Dependency Problems
Booch, Rumbaugh, and Jacobson3 formally define dependency as
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)
Fowler4 uses different role names but otherwise offers the same definition:
A dependency exists between two elements if changes to the definition of one element (the supplier or target) may cause changes to the other (the client or source). (p. 47)
The breadth of this definition is unfortunate because, as Fowler4 notes, "Many UML relationships imply a dependency" (p. 48). I take Fowler's criticism further, claiming that, based on the above definition, all class relationships imply a dependency between the related classes. This situation makes dependency the least well-defined of the class relationships. Fowler4 concludes his critique, saying,
Trying to show all the dependencies in a class diagram is an exercise in futility; there are too many, and they change too much. (p. 49)
Explicitly showing the dependency relationships in a UML diagram is messy and contributes little to our understanding of the original problem or the resulting software system. Software developers deal with the futility and messiness by narrowing dependency's application:
The most common case I use for dependencies with classes is when illustrating a transient relationship, such as when one object is passed to another as a parameter. (Fowler4, p. 49)
The most common usage of a dependency is to show that the signature of one class's operation uses another class. (Schmuller5, p. 54)
These practices justify narrowing dependency's scope and use. We use dependency whenever we need to show a brief relationship between classes. Nevertheless, I typically reserve it for a particular case: when one object must forward a message to another.
The Best Use For Dependency
Rumbaugh2 cautions class developers not to misuse inheritance:
Sometimes it appears that the use of inheritance would increase code reuse within a program, when a true superclass/subclass relationship does not exist. Do not give in to the temptation to use this implementation inheritance; use delegation instead (pp. 284-285).
The following figure elaborates on Rumbaugh's caution. Imagine that we are writing a program that specifies an Employee class that has a member saving the employee's name: string name. Further, imagine that the program must compare an employee's name to another string. The string class defines a function called compare that performs the needed task. The following figure illustrates some of the class's shared features that might tempt us to use inheritance to connect them.
int Employee::compare(string& other)
{
return name.compare(other);
}
(a)
(b)
(c)
Message forwarding. A design problem that tempts us to use implementation inheritance. Class designers don't typically include the member variables implementing class relationships in the class symbols; instead, programmers translate the connector symbols (e.g., the UML aggregation or composition symbol) into appropriate members. The class diagrams include the members for clarity and to emphasize the Employeecompare function.
The Employee class has a string saving the employee's name and needs a way to compare the names of two employees. The string class has a compare function, which Employee objects can use. Building an (implementation) inheritance relationship between the classes allows Employee to inherit and directly use the stringcompare function.
However, inheritance is an is-a relationship: the test phrase "an Employee is a string" doesn't sound correct.
Composition is a better relationship connecting an Employee and string. The test phrase "an Employee has a name" - it has-astring saving its name - sounds correct, suggesting that a composition or an aggregation relationship is more appropriate. The dashed-line arrow represents the dependency relationship that exists while the Employee::compare function runs.
When an Employee object receives the compare message, it forwards or relays it to the string compare function.
Although illustrated here with composition, we can use dependency with any relationship, even inheritance, or by itself.
I agree with Fowler that "trying to show all the dependencies in a class diagram is an exercise in futility." Illustration (b) notwithstanding, I rarely include them in my diagrams. Understanding objects' behavior in a dependency relationship is more important than diagramming it.
Dependency Summary: Filling In The Table
Dependency is a temporary relationship helpfully characterized by the phrases "depends on," "delegates to," or "uses." Most of its property values are unique and unshared by the other relationships. The following figure summarizes those property values; use it to check and complete your entries in one of the blank Class Relationship Tables located at the end of the chapter.
Class Roles. Dependency is a one-way relationship between a dependent and an independent class. The relationship's historical names are often more meaningful, easier to remember, and clarify the relationship's behavior. Class developers often call the dependent class the "client" or the "user" and the independent class the "server" or "supplier." I prefer the terms "client" and "supplier."
Semantics.
A fraction_calculator depends on a fraction
A fraction_calculator delegates to a fraction
A fraction_calculator uses a fraction
Directionality. Dependency is a unidirectional or one-way relationship. Unidirectionality means that
(a)
(b)
(c)
(d)
The operations may act in only one direction - from the client to the supplier.
The client can send a message to the supplier, which can reply but not initiate the exchange.
The client "knows" about the supplier, but not the other way.
A program can navigate from the client object to the supplier but not from the supplier to the client.
Binding Strength. Temporary or transient (the relationship is created with a function call and persists until the function returns). The binding strength implies the final two characteristics:
Lifetime. Independent, the client exists before the functions call, but the function defines variable naming the client. The variable goes out of scope when the function returns, breaking the relationship.
Sharing. Shareable. Supplier objects are shared when the program passes them by pointer or reference to a parameter, but they are not shared when passed by value or created as a local, non-parameter object.
Implementation. T denotes an unspecified data type.
class Supplier
{
T f1();
T f2();
};
class Client
{
T g1(Supplier s) // (a)
{
s.f1();
}
T g2()
{
Supplier s; // (b)
s.f1();
}
T f2(Supplier s) // (c)
{
T s.f2(...)
}
};
Dependency property values. Dependency is the only class relationship implemented with local-scope variables - C++ implements the other relationships with class-scope variables.
A Supplier object, s, is a Client function parameter.
The Supplier object, s, is a local variable defined in the Client function g2.
Similar to (a) but illustrating a common programming construct where one object relays a message to a related object, replacing "implementation inheritance."
Booch, G. (1994). Object-oriented analysis and design with applications (2 ed.). Menlo Park, CA: Addison-Wesley Publishing Co.
Rumbaugh, J., Blaha, M., Premerlani, W., Eddy, F., & Lorensen, W. (1991). Object-Oriented Modeling and Design. Englewood Cliffs, NJ: Prentice Hall.
Booch, G., Rumbaugh, J., & Jacobson, I. (2005). The unified modeling language user guide (2nd ed.). Upper Saddle River, NJ: Addison-Wesley.
Fowler, M. (2004). UML distilled Third Edition: A brief guide to the standard object modeling language. Boston: Addison-Wesley.
Schmuller, J. (2002). Teach yourself uml in 24 hours (2nd ed.). Indianapolis: Sams Publishing.