10.9. Dependency

Time: 00:04:39 | Download: Large, Large (CC), Small | Streaming (CC) | Slides (PDF)
Review

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.

UML Classes C++ Function Parameter
Two classes, 'fraction_calculator' and 'fraction,' connected by a UML dependency symbol. The dependency symbol is a dashed line with an open arrowhead at one end. The arrowhead attaches to the independent class (also known as the supplier or server). The plain or undecorated end attaches to the dependent class (also known as the user or the client).
class fraction_calculator
{
    public:
        fraction add(fraction f);
        fraction sub(fraction* f);
        void print(fraction& f);
};
(a)(b)
C++ Function Local Variable
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.
  1. The class diagram for a dependency relationship. We call the classes in a dependency relationship "dependent" and "independent." Less formal but often more meaningful names are also common: "user," "client," "supplier," and "server," as illustrated. I find the terms "client" and "supplier" easier to remember, and they characterize the class roles well.
  2. 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.
  3. 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). 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, therefore, scopes, but dependency relationships don't distinguish between them 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: These practices form our justification for narrowing dependency's scope and use. We can use dependency whenever we need to show a brief relationship between classes, but I generally reserve it for a particular case: when one object needs to 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).

We elaborate on Rumbaugh's cautioning remark with a concrete example. 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. The string class defines a function called compare that performs the needed task (please see the "Ordering" section in A Comparison of the String Class and C-Strings). The following figure illustrates some of the class's shared features that might tempt us to use inheritance to connect them.

Two classes, the string library class and Employee. The string class defines the compare function. Employee is a subclass of string and inherits the compare function. Two classes, the string library class and Employee. The classes are connected by composition: Employee is the whole class, and string is the part. Employee also depends on (or uses or delegates to) string. Both classes define a compare function.
int Employee::compare(string& other)
{
	return name.compare(other);
}
(a)(b)(c)
Incorrect inheritance. A design problem tempting us to use implementation inheritance.
  1. The Employee class needs a string to save the employee's name and a compare function to compare or order two names. We could build an inheritance relationship between the two classes and directly reuse the string class's function. But inheritance is an is-a relationship: the test phrase "an Employee is a string" doesn't sound correct. (Please realize that "real world" problems are often more subtle than this overt example.)
  2. Clearly, an employee has a name - that is, an Employee has a string - indicating a composition or aggregation relationship is more appropriate. (The UML composition symbol alone is sufficient to convey this information, but I explicitly include the name attribute to emphasize the adjacent code.)
  3. When an Employee object receives the compare message, it forwards it to the composed string.
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. In my opinion, 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.

  1. Semantics.
    • A fraction_calculator depends on a fraction
    • A fraction_calculator delegates to a fraction
    • A fraction_calculator uses a fraction
  2. Directionality. Dependency is a unidirectional or one-way relationship. Unidirectionality means that
    Two classes, labeled 'Peer1' and 'Peer2,' are connected with a simple, undecorated line denoting an association relationship. The double-ended arrow beneath the classes indicates association's operations work in both directions. The same UML association, but the arrow indicates that either peer can send a message to the other.
    (a)(b)
    The same UML association, but the arrow indicates that the peers 'know about' each other. The same UML association, but the arrow indicates that we can navigate from either peer to the other.
    (c)(d)
    1. The operations may act in only one direction - from the client to the supplier.
    2. The client can send a message to the supplier, which can reply but not initiate the exchange.
    3. The client "knows" about the supplier, but not the other way.
    4. A program can navigate from the client object to the supplier but not from the supplier to the client.
  3. Binding Strength. Temporary or transient (the relationship is created with a function call and only 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.
  4. 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 - the other relationships are implemented with class-scope variables.


1 Booch, G. (1994). Object-oriented analysis and design with applications (2 ed.). Menlo Park, CA: Addison-Wesley Publishing Co.
2 Rumbaugh, J., Blaha, M., Premerlani, W., Eddy, F., & Lorensen, W. (1991). Object-Oriented Modeling and Design. Englewood Cliffs, NJ: Prentice Hall.
3 Booch, G., Rumbaugh, J., & Jacobson, I. (2005). The unified modeling language user guide (2nd ed.). Upper Saddle River, NJ: Addison-Wesley.
4 Fowler, M. (2004). UML distilled Third Edition: A brief guide to the standard object modeling language. Boston: Addison-Wesley.
5 Schmuller, J. (2002). Teach yourself uml in 24 hours (2nd ed.). Indianapolis: Sams Publishing.