CLOSE
Updated on 05 Jul, 202638 mins read 154 views

What happens when a class needs to use another class for a brief moment to get a job done, without needing to hold onto it forever?

That's dependency. It represents the weakest form of relationship between classes.

Unlike association, aggregation, or composition, a depdency isn't a structural “we belong together” relationship. There is no shared lifecycle and no long-term connection.

Instead, it reflects a one-time interaction, often through method parameters, local variables, or return types.

Historical Context

Before object-oriented programming became popular, software was primarily procedural. Programs looked something like this:

main()

↓

Read File

↓

Process Data

↓

Print Result

Everything happened sequentially.

Functions simply called other functions.

As software became larger, engineers realized something interesting.

Even though functions weren't storing other functions they still depended on them.

Example:

calculateSalary()
{
    readEmployee();
    calculateTax();
    saveSalary();
}

Here,

calculateSalary()

depends on

readEmployee()
calculateTax()
saveSalary()

If any of those functions changed significantly, the caller could break.

This idea eventually evolved into dependency.

When object-oriented programming emerged, functions became methods and data became objects.

Why Does Dependency Exist?

Imagine a restaurant.

There are:

  • Chef
  • Waiter
  • Customer
  • Cashier
  • Delivery Person

Does the chef own the waiter?

No.

Does the chef contain the waiter?

No.

Does the waiter belong to the chef?

No.

The chef simply needs the waiter to deliver food.

Once the food leaves the kitchen, their interaction is finished.

This is dependency.

The chef uses the waiter. Nothing more.

Software works the same way.

An object often needs another object only while performing a task. It does not permanently own it.

First Principles

Let's forget programming. Imagine building a house.

You need:

  • Hammer
  • Drill
  • Saw
  • Measuring tape

Do you become permanently connected to the hammer?

No.

You simply use it. After finishing, the hammer goes away.

You relationship with the hammer is temporary.

This is exactly what dependency represents.

Dependency answers only one question:

“Who needs whom to accomplish a task?”

Not

  • Who owns whom?
  • Who contains whom?
  • Who manages lifetime?

Only

Who temporarily uses whom?

What Is Dependency?

A Dependency exists when one class relies on another to fulfill a responsibility, but does so without retaining a permanent reference to it.

This is typically happens when:

  • A class accepts another class as a method parameter.
  • A class instantiates or uses another class inside a method.
  • A class returns an object of another class from a method.

Formal Definition

A dependency exists when

One software element requires another software element in order to perform some operation, but does not own or permemently maintain it.

The key word is requires.

Not:

  • contains
  • owns
  • manages
  • stores
  • inherits
  • just
  • requires

Key Characteristics of Dependency

  • Short-lived: The relationship exists only during method execution.
  • No ownership: The depdendent class does not sstore the otehr as a field.
  • “Uses-a” relationship: The class uses another to accomplish a task, but does not retain it.

The Mental Model

Imagine people in an office.

Manager

asks

Accountant

↓

Generate Report

↓

Accountant returns report

↓

Interaction ends

The manager depends on the accountant.

But the manager does not own the accountant.

This is dependency.

The Simplest Example

class Printer
{
public:

    void print()
    {
        std::cout << "Printing...\n";
    }

};

Now,

class Student
{
public:

    void submitAssignment(Printer& printer)
    {
        printer.print();
    }

};

Question: Does Student own Printer?

No.

Student merely needs Printer temporarily.

As soon as the function finishes, the dependency disappears.

UML Representation

In UML class diagrams, dependency is represented by a dashed arrow ( - - - - - - - >) pointing form the dependent class to the class it depends on.

This is the lightest notation in UML, reflecting the fact that dependency is the lightest relationship.

image-589.png

The diagram read: “Student” depends on Printer. The Student uses a Printer during its submitAssignment, but doesn't store it as a field. The dashed arrow is the visual shorthand for this temporary relationship.

Code Example

#include <iostream>
#include <string>

using namespace std;

class Printer {
public:
    void print(string& content)
    {
        cout << "Printing.....\n";
        cout << content << "\n";
    }
};

class Student {
private:
    string content;
public:
    Student(const string& content)
        : content(content)
    {}

    void submitAssignment(Printer& printer) {
        printer.print(content);
    }
};

int main()
{
    Printer printer;
    Student s1("Science Proj");
    s1.submitAssignment(printer);
    // After this function, the student as no reference of the printer.
    
    return 0;
}

Recognizing Dependencies in Code

Dependencies can appear several common forms within a class:

As Method Parameters

This is the most common and most recognizable form of dependency. The dependent class receives another class as a parameter, uses it during the methods, and lets it go.

class ReportGenerator {
public:
    string generate(DataSource& source) {
        auto data = source.fetchAll();
        // Format data into report...
        return formattedReport;
    }
};

ReportGenerator depends on DataSource, but doesn't store it.

The DataSource comes in, gets uses, and is gone once generate() returns.

As Local Variables

Sometimes a class creates another class inside a method, uses it and discards it. This created object never escapes the method scope.

class OrderProcessor {
public:
    void process(Order& order) {
        JsonFormatter formatter;
        string json = formatter.format(order);
        // Send json to external API...
    }
};

OrderProcessor depends on JsonFormatter, but the formatter is a local variable. It's created inside the method and disappears when the method ends. No field, no structural link.

As Return Types

A method can return an object of another class, creating a depedency on that type even if the class doesn't store it.

class UserFactory {
public:
    User createUser(const string& name, const string& email) {
        return User(name, email);
    }
};

UserFactory depends on User because it creates and returns User objects, but it doesn't store any User as a field. The factory's job is to produce users, not to hold onto them.

As Static Method Calls

A class can depends on another class by calling its static methods. There's no object reference at all, just a class-level depdency.

class PasswordService {
public:
    bool verify(const string& input, const string& hash) {
        return HashUtils::sha256(input) == hash;
    }
};

PasswordService depends on HashUtils, but there's no instance of HashUtils stored anywhere. The depdency is purely at the class level through a static call.

Dependency Injection

In real-world applications, classes often depend on other classes to get their work done.

A UserService might rely on a DatabaseClient to fetch users, or a NotificationService might rely on an EmailSender to send messages.

But how should these dependencies be provided?

You could let the class create its own dependencies internally but that leads to tight coupling, making your code rigid and hard to test.

A better approach is to inject those dependencies from the outside.

This is called Dependency Injection (DI), one of the most powerful principles in modern software design.

Dependency Injection is a design technique where a class receives the objects it depends on, instead of creating them itself.

This leads to:

  • Better testability: You can inject mock dependencies during unit tests.
  • Greater modularity: Swap implementations (e.g., EmailSender → SMSSender) without changing core logic.
  • Loose coupling: Classes only depend on abstract contracts (interfaces), not concrete implementations.

Example

Consider a NotificationService that sends email notifications. A straightforward implementation might create its own EmailSender internally:

class NotificationService {
private:
    EmailSender sender; // Creates its own dependency
public:
    NotificationService() : sender() {}

    void notifyUser(const string& message) {
        sender.send(message);
    }
};

This looks reasonable, but it has several problems:

  • Can't switch implementations. Want to send SMS instead of email? We have to modify the NotificationService class itself.
  • Can't test in isolation. Unit tests will actually send real email (or fail trying) because there's no way t substitute a mock.
  • Violates the Open/Closed Principle. Adding a new notification channel requires changing existing code rather than extending it.

The NotificationService is tightly coupled to EmailSender. It controls what sender it uses, and nobody from the outside can change that.

The Solution : Inject from Outside

Instead of letting the class create its own dependencies, you provide them from outside. This is Dependency Injection: a design technique where a class receives the objects it depends on rather than creating them itself.

class Sender {
public:
    virtual void send(const string& message) = 0;
    virtual ~Sender() = default;
};

class EmailSender : public Sender {
public:
    void send(const string& message) override {
        cout << "Email: " << message << endl;
    }
};

class SmsSender : public Sender {
public:
    void send(const string& message) override {
        cout << "SMS: " << message << endl;
    }
};

class NotificationService {
private:
    Sender* sender;
public:
    NotificationService(Sender* sender) : sender(sender) {} // Injected

    void notifyUser(const string& message) {
        sender->send(message);
    }
};

Now the NotificationService depends on a Sender interface, not a concrete EmailSender. The specific implementation is provided from outside through the constructor.

This gives you three concrete benefits:

  • Swappable implementations. Pass EmailSender in production, SmsSender for mobile users, or PushSender for in-app notifications. No code changes needed inside NotificationService.
  • Easy testing. Pass a MockSender in unit tests that records messages instead of actually sending them. You can verify what was sent without side effects.
  • Loose coupling. NotificationService depends only on the Sender interface. It doesn't know or care how messages are actually delivered.

In real-world applications, frameworks like Spring (Java), ASP.NET (C#), and NestJS (TypeScript) handle DI for you. They automatically resolve and inject dependencies based on configuration or annotations, so you don't have to wire everything manually. But the underlying concept is the same: classes declare what they need, and something external provides it.

Buy Me A Coffee

Leave a comment

Your email address will not be published. Required fields are marked *

Your experience on this site will be improved by allowing cookies Cookie Policy