Managing Object Dependencies
Handle temporary usage of objects within function scope.
What Is a Dependency?
A dependency is what is left when one type needs a second type only while a single call is running. Nothing is stored, nothing is owned, and once the call returns neither side holds anything to show the two ever met.
That makes it the weakest of the four relationships in this chapter, and the last one left to cover. Composition owns a part. Aggregation refers to a part it does not own. Association keeps a two-way-capable link to something that is not a part at all. A dependency keeps nothing: the collaborator arrives, does its job, and is gone.
Everyday dependencies have the same shape. You depend on a taxi to reach the airport, and once you are there the relationship is over. A courier depends on a lift to reach the ninth floor, and neither the courier nor the lift retains anything about the other afterwards. Dependencies are unidirectional in every case: the caller knows about the thing it uses, and the thing being used has no idea who called it.
Weak does not mean harmless. Your code still breaks if the type you depend on changes its interface. What makes a dependency weak is its lifetime, not its consequences.
std::ostream Is the Dependency You Already Use
You have written dozens of dependencies without naming one. Every class you have given an output operator depends on std::ostream.
#include <iostream>
#include <string>
#include <string_view>
class Ticket
{
private:
std::string m_route{};
int m_coach{};
int m_seat{};
public:
Ticket(std::string_view route, int coach, int seat)
: m_route{route}, m_coach{coach}, m_seat{seat}
{
}
friend std::ostream& operator<<(std::ostream& stream, const Ticket& ticket);
};
std::ostream& operator<<(std::ostream& stream, const Ticket& ticket)
{
stream << ticket.m_route << " [coach " << ticket.m_coach
<< " seat " << ticket.m_seat << ']';
return stream;
}
int main()
{
const Ticket commuter{"Leeds to York", 3, 42};
std::cout << commuter << '\n';
return 0;
}
Leeds to York [coach 3 seat 42]
Ticket has no std::ostream member. It has no pointer to one, and if you printed a Ticket a thousand times it would still hold nothing connecting it to any stream. What it has is a function that borrows a stream for the duration of one call in order to accomplish the task of printing. That is a dependency, and std::ostream is far and away the one you will meet most often.
The main function has a dependency of its own, on std::cout. It reaches for the console stream at the moment it wants output and abandons it immediately afterwards.
The Three Shapes a Dependency Takes
Dependencies show up in source code in three recognisable forms. The common thread is that the depended-upon object is never a data member.
| Shape | How the collaborator arrives | Example |
|---|---|---|
| Parameter | Handed in by the caller, one call at a time | std::ostream& in an overloaded operator<< |
| Local object | Constructed inside the function that needs it, destroyed at the closing brace | A lookup table built to answer one question |
| Global facility | Reached for by name wherever it is needed | std::cout |
The middle shape is worth seeing, because it is the one people misread as ownership. Turnstile below builds a FareTable, uses it, and lets it die at the end of the function.
#include <iostream>
#include <string>
#include <string_view>
class FareTable
{
private:
int m_peakPence{};
int m_offPeakPence{};
public:
FareTable(int peakPence, int offPeakPence)
: m_peakPence{peakPence}, m_offPeakPence{offPeakPence}
{
}
int priceFor(bool peak) const
{
return peak ? m_peakPence : m_offPeakPence;
}
};
class Turnstile
{
private:
std::string m_station{};
public:
explicit Turnstile(std::string_view station)
: m_station{station}
{
}
void admit(bool peak) const
{
const FareTable table{980, 640};
std::cout << m_station << " charged " << table.priceFor(peak) << " pence\n";
}
};
int main()
{
const Turnstile northGate{"York"};
northGate.admit(true);
northGate.admit(false);
return 0;
}
York charged 980 pence
York charged 640 pence
Turnstile constructs a FareTable, so it might look like composition. It is not, because the table is not part of a Turnstile. A Turnstile between calls to admit contains a station name and nothing else, and each call builds a fresh table. Composition requires the part to be contained in the whole; a local variable is contained in a function.
Dependency or Association? Ask What the Object Remembers
This is the distinction that trips people up, so here is a single question that settles it: after the function returns, can you still ask the object about its collaborator?
If yes, the class stored a link, and a stored link is an association. If no, the class used something and let it go, and that is a dependency. A dependency is the weaker of the two, because an association survives between calls and a dependency does not survive the call it was created for.
| Association | Dependency | |
|---|---|---|
| Stored as a member | Yes, a pointer, reference, or identifier | No |
| Reachable after the call | Yes, ask the object | No, nothing was kept |
| Lifetime of the relationship | As long as the member holds the link | The duration of one operation |
| Direction | May be one-way or two-way | Always one-way |
| Visible in the object's size | Yes, the member occupies storage | No |
The last row is not a metaphor. A Conductor assigned to a coach stores a pointer to it and pays for that pointer in every Conductor object ever created. An Inspector who walks whichever coach is handed to it stores nothing and pays nothing.
#include <iostream>
#include <string>
#include <string_view>
class Coach
{
private:
int m_number{};
public:
explicit Coach(int number)
: m_number{number}
{
}
int getNumber() const { return m_number; }
};
class Conductor
{
private:
std::string m_badge{};
const Coach* m_assigned{};
public:
Conductor(std::string_view badge, const Coach& assigned)
: m_badge{badge}, m_assigned{&assigned}
{
}
void announce() const
{
std::cout << m_badge << " is working coach " << m_assigned->getNumber() << '\n';
}
};
class Inspector
{
private:
std::string m_badge{};
public:
explicit Inspector(std::string_view badge)
: m_badge{badge}
{
}
void inspect(const Coach& coach) const
{
std::cout << m_badge << " inspected coach " << coach.getNumber() << '\n';
}
};
int main()
{
const Coach quietCoach{7};
const Conductor conductor{"C-118", quietCoach};
const Inspector inspector{"D-204"};
conductor.announce();
inspector.inspect(quietCoach);
std::cout << "Conductor carries " << sizeof(Conductor) - sizeof(std::string)
<< " bytes beyond its badge\n";
std::cout << "Inspector carries " << sizeof(Inspector) - sizeof(std::string)
<< " bytes beyond its badge\n";
return 0;
}
C-118 is working coach 7
D-204 inspected coach 7
Conductor carries 8 bytes beyond its badge
Inspector carries 0 bytes beyond its badge
Those eight bytes are the association. They are the storage the Conductor spends on remembering, and they are exactly what an association buys you: ask a Conductor which coach it works and it can answer. Ask an Inspector the same question and there is nobody to ask, because the coach was never anything more than an argument. The Inspector still depends on Coach, though. Rename getNumber and inspect stops compiling.
Do not read the exact byte counts as a rule of the language. The pointer costs eight bytes on this platform and could differ elsewhere. What generalises is the direction: a stored link adds to an object's size, and a dependency adds nothing.
Why the Weakest Relationship Still Matters
A dependency creates no lifetime obligations, which is precisely why it is easy to acquire without noticing. What it does create is coupling, and coupling is the thing that decides how expensive tomorrow's change is.
Every type your code names is a type whose interface your code has agreed to. Change FareTable::priceFor to take a string instead of a bool and Turnstile stops compiling, even though Turnstile never stored a FareTable and never will. Tracing dependencies is how you answer the question that comes up before every refactor: if I change this, what breaks?
That is the practical use of naming this relationship at all. Dependencies are the edges of your codebase's change-propagation graph, and they are the ones the compiler will not draw for you the way it draws inheritance or membership.
When a class only needs to use another type briefly, take it as a function parameter rather than storing it as a member. The parameter is the honest declaration that the relationship lasts for one call, and it keeps the class smaller and easier to test.
The Four Relationships Side by Side
With dependencies covered, the whole chapter fits in one table. Read it top to bottom as a ladder from strongest to weakest.
| Relationship | Other object is a part | This object manages its lifetime | Several users at once | Stored as a member | Direction |
|---|---|---|---|---|---|
| Composition | Yes | Yes | No | Yes, by value | One-way |
| Aggregation | Yes | No | Yes | Yes, by reference or pointer | One-way |
| Association | No | No | Yes | Yes, by any handle | One-way or two-way |
| Dependency | No | No | Yes | No | One-way only |
Every row down the ladder gives something up. Composition owns and manages. Aggregation keeps the containment and drops the ownership. Association keeps the link and drops the containment. Dependency drops the link too, which leaves nothing but the fact that one type calls another.
Looking Forward
That completes the four object relationships. The rest of the chapter puts them to work: container classes are composition applied deliberately, since a container exists to own the elements you hand it, and std::initializer_list is the mechanism that lets such a container be filled with brace syntax. Later chapters revisit dependencies constantly, because every standard library facility you call is one.
Key Terminology
- Dependency: The weakest of the four relationships, in which a type reaches for a collaborator during a call and keeps no member pointing at it afterwards
- Coupling: The degree to which one piece of code must change when another changes
- Unidirectional relationship: One where the user knows about the used type and the used type knows nothing of the user
- Association: A relationship in which the object keeps a stored link to a collaborator that is not part of it
std::ostream: The output stream type, and the dependency you will encounter more than any other
Summary
- Dependencies are the weakest of the four relationships and always run one way: a collaborator is borrowed for the length of one operation and never recorded as a member.
- The relationship is temporary. It exists for the duration of one operation and ends when that operation ends.
- Weak lifetime does not mean weak consequences: a change to the type you depend on can still break your code.
std::ostreamis the dependency you have used most. Every overloadedoperator<<borrows a stream to print with and stores nothing.- Dependencies are not data members. The collaborator is passed in as a parameter, constructed locally when it is needed, or reached for as a global facility such as
std::cout. - An association stores a link and can be asked about its collaborator afterwards. A dependency stores nothing, so there is nothing to ask. The stored link also shows up in the object's size.
- Constructing an object locally inside a function is a dependency, not composition. The object is contained in the function, not in the class.
- Dependencies are worth tracking because they map the coupling in a codebase, which is what tells you the blast radius of a change.
- Prefer taking a briefly used type as a function parameter over storing it as a member, so the code states the true length of the relationship.
How did you find this lesson?
Your rating helps us improve the content.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Managing Object Dependencies - Quiz
Test your understanding of the lesson.
Practice Exercises
Demonstrate Dependency Relationships
Create classes that demonstrate dependency relationships where one class uses another's functionality for a specific task, but doesn't maintain a persistent relationship. Implement a Report class that depends on a Printer for output.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!