Dependency in OOP: Uses-A Relationship Guide
TL;DR
Dependency is the weakest form of class relationship where one class temporarily uses another, typically through method parameters, local variables, or return types. Unlike association, the dependent class doesn’t store a reference to the other class as an instance variable. Understanding dependency helps you design loosely coupled systems that are easier to test and maintain.
Core Concept
What is Dependency?
Dependency occurs when one class (the dependent) uses another class (the dependency) temporarily within a method, but doesn’t maintain a lasting relationship. The dependent class “knows about” the other class only during method execution.
Think of it like using a calculator app on your phone. You open it, perform a calculation, and close it. Your phone depends on the calculator temporarily, but doesn’t keep it running in the background.
How Dependencies Manifest in Code
Dependencies appear in three primary ways:
- Method Parameters: A class receives an object of another class as a parameter
- Local Variables: A class creates or uses an object of another class within a method scope
- Return Types: A method returns an object of another class
Why Dependencies Matter
Dependencies represent coupling between classes. High coupling makes code harder to:
- Test: You need the dependency available to test the dependent class
- Change: Modifying the dependency might break the dependent class
- Reuse: The dependent class can’t work without access to its dependencies
Dependency vs. Association
The key difference: lifetime and storage.
- Dependency: Temporary relationship, no instance variable storing the reference
- Association: Lasting relationship, stored as an instance variable
If a class has self.other_object as an instance variable, that’s association. If it only uses other_object within a method, that’s dependency.
Dependency Direction
Dependencies have direction. If class A depends on class B, we say “A depends on B” or “A uses B”. The arrow in UML diagrams points from the dependent to the dependency, showing the direction of knowledge.
Visual Guide
Dependency Relationship Diagram
classDiagram
class EmailService {
+send_email(message: EmailMessage)
}
class EmailMessage {
+to: string
+subject: string
+body: string
}
EmailService ..> EmailMessage : depends on
note for EmailService "Uses EmailMessage temporarily\nas a method parameter"
Dependency shown with a dashed arrow. EmailService depends on EmailMessage but doesn’t store it.
Dependency vs Association
classDiagram
class ReportGenerator {
+generate(data: DataSource) Report
}
class DataSource {
+fetch_data()
}
class Report {
+content: string
}
class Logger {
-log_file: File
+write(message)
}
class File {
+write()
}
ReportGenerator ..> DataSource : dependency
ReportGenerator ..> Report : dependency
Logger --> File : association
note for ReportGenerator "Temporary usage only"
Dependency (dashed) is temporary; Association (solid) is persistent storage.
Examples
Example 1: Method Parameter Dependency
class EmailMessage:
def __init__(self, to, subject, body):
self.to = to
self.subject = subject
self.body = body
class EmailService:
"""Depends on EmailMessage through method parameter"""
def send_email(self, message: EmailMessage) -> bool:
# EmailService uses EmailMessage temporarily
print(f"Sending to: {message.to}")
print(f"Subject: {message.subject}")
print(f"Body: {message.body}")
return True
# Usage
service = EmailService()
msg = EmailMessage("user@example.com", "Hello", "Test message")
service.send_email(msg)
# Expected Output:
# Sending to: user@example.com
# Subject: Hello
# Body: Test message
Key Point: EmailService doesn’t store EmailMessage as an instance variable. It only uses it within the send_email method.
Java Equivalent:
public class EmailService {
public boolean sendEmail(EmailMessage message) {
System.out.println("Sending to: " + message.getTo());
return true;
}
}
Example 2: Local Variable Dependency
class DatabaseConnection:
def __init__(self, connection_string):
self.connection_string = connection_string
def execute(self, query):
return f"Executing: {query}"
class UserRepository:
"""Depends on DatabaseConnection through local variable creation"""
def __init__(self, connection_string):
self.connection_string = connection_string
def get_user(self, user_id: int) -> dict:
# Creates dependency locally - not stored as instance variable
db = DatabaseConnection(self.connection_string)
result = db.execute(f"SELECT * FROM users WHERE id = {user_id}")
print(result)
return {"id": user_id, "name": "John Doe"}
# Usage
repo = UserRepository("localhost:5432")
user = repo.get_user(123)
print(user)
# Expected Output:
# Executing: SELECT * FROM users WHERE id = 123
# {'id': 123, 'name': 'John Doe'}
Key Point: DatabaseConnection is created inside the method and doesn’t persist beyond that method call.
Example 3: Return Type Dependency
class Report:
def __init__(self, title, content):
self.title = title
self.content = content
def display(self):
return f"{self.title}\n{'-' * len(self.title)}\n{self.content}"
class ReportGenerator:
"""Depends on Report through return type"""
def generate_sales_report(self, sales_data: list) -> Report:
# Creates and returns Report - dependency through return type
total = sum(sales_data)
content = f"Total Sales: ${total}\nTransactions: {len(sales_data)}"
return Report("Sales Report", content)
# Usage
generator = ReportGenerator()
report = generator.generate_sales_report([100, 250, 175, 300])
print(report.display())
# Expected Output:
# Sales Report
# ------------
# Total Sales: $825
# Transactions: 4
Try it yourself: Modify ReportGenerator to create a MonthlyReport class that extends Report and includes a month field. Update the generator to return this new type.
Example 4: Dependency Injection (Reducing Coupling)
class Logger:
def log(self, message):
print(f"[LOG] {message}")
class PaymentProcessor:
"""Better design: inject dependency rather than create it"""
def process_payment(self, amount: float, logger: Logger) -> bool:
# Dependency injected as parameter - more testable
logger.log(f"Processing payment of ${amount}")
if amount > 0:
logger.log("Payment successful")
return True
else:
logger.log("Payment failed: invalid amount")
return False
# Usage
logger = Logger()
processor = PaymentProcessor()
result = processor.process_payment(99.99, logger)
print(f"Payment result: {result}")
# Expected Output:
# [LOG] Processing payment of $99.99
# [LOG] Payment successful
# Payment result: True
Key Point: By injecting Logger as a parameter, we can easily substitute a mock logger during testing.
Try it yourself: Create a FileLogger class that writes to a file instead of printing. Test that PaymentProcessor works with both Logger and FileLogger without modification.
Common Mistakes
1. Confusing Dependency with Association
Mistake: Thinking any use of another class is a dependency.
# This is ASSOCIATION, not dependency
class ShoppingCart:
def __init__(self):
self.items = [] # Stored as instance variable = association
def add_item(self, item):
self.items.append(item)
# This is DEPENDENCY
class OrderProcessor:
def process(self, cart: ShoppingCart):
# Only uses cart temporarily = dependency
return len(cart.items)
Why it matters: Misidentifying relationships leads to incorrect design decisions. Association implies ownership and lifecycle management; dependency doesn’t.
2. Creating Dependencies Inside Methods (Tight Coupling)
Mistake: Instantiating dependencies directly within methods instead of injecting them.
# BAD: Hard to test, tightly coupled
class OrderService:
def create_order(self, items):
# Creates dependency internally - can't substitute for testing
email_service = EmailService()
email_service.send_confirmation(items)
# GOOD: Dependency injection
class OrderService:
def create_order(self, items, email_service: EmailService):
# Injected dependency - easy to mock in tests
email_service.send_confirmation(items)
Why it matters: Internal instantiation makes unit testing nearly impossible. You can’t test OrderService without a real EmailService.
3. Ignoring Dependency Direction
Mistake: Creating circular dependencies where A depends on B and B depends on A.
# BAD: Circular dependency
class UserService:
def get_orders(self, user_id):
order_service = OrderService()
return order_service.get_by_user(user_id)
class OrderService:
def get_user_info(self, order_id):
user_service = UserService() # Circular!
return user_service.get_user(order_id)
Why it matters: Circular dependencies create fragile code that’s hard to understand, test, and modify. They often indicate poor separation of concerns.
4. Over-Depending on Concrete Classes
Mistake: Depending on specific implementations instead of abstractions (interfaces/abstract classes).
# BAD: Depends on concrete MySQLDatabase
class UserRepository:
def save(self, user, database: MySQLDatabase):
database.insert(user)
# GOOD: Depends on abstraction
from abc import ABC, abstractmethod
class Database(ABC):
@abstractmethod
def insert(self, data): pass
class UserRepository:
def save(self, user, database: Database):
database.insert(user) # Works with any Database implementation
Why it matters: Depending on abstractions (Dependency Inversion Principle) makes code flexible and testable. You can swap implementations without changing dependent code.
5. Not Documenting Dependencies
Mistake: Failing to make dependencies explicit through type hints or documentation.
# BAD: Unclear what type message should be
class NotificationService:
def send(self, message):
print(message.content)
# GOOD: Type hint makes dependency explicit
class NotificationService:
def send(self, message: EmailMessage):
print(message.content)
Why it matters: Explicit dependencies improve code readability and help IDEs provide better autocomplete and error checking.
Interview Tips
What Interviewers Look For
1. Can you identify dependency relationships in code?
Interviewers often show code and ask you to identify relationships. Practice saying: “Class A depends on class B because it uses B as a method parameter/local variable/return type, but doesn’t store it as an instance variable.”
Example question: “Looking at this code, what’s the relationship between PaymentProcessor and CreditCard?”
Strong answer: “It’s a dependency relationship. PaymentProcessor uses CreditCard as a method parameter in process_payment(), but doesn’t store it as an instance variable. This is temporary usage, making it a dependency rather than association.”
2. Explain the benefits of dependency injection
Be ready to discuss why injecting dependencies is better than creating them internally.
Key points to mention:
- Testability: You can inject mock objects during testing
- Flexibility: Easy to swap implementations without changing code
- Loose coupling: Classes don’t need to know how to create their dependencies
Example question: “How would you make this class more testable?”
class OrderService:
def process(self, order):
db = Database() # Hard-coded dependency
db.save(order)
Strong answer: “I’d use dependency injection by passing the database as a parameter: def process(self, order, db: Database). This allows me to inject a mock database during testing without modifying the OrderService class. It follows the Dependency Inversion Principle by depending on an abstraction rather than a concrete implementation.”
3. Discuss the Dependency Inversion Principle (DIP)
Interviewers at senior levels expect you to connect dependencies to SOLID principles.
DIP states: “Depend on abstractions, not concretions.”
Be ready to explain: High-level modules shouldn’t depend on low-level modules. Both should depend on abstractions (interfaces/abstract classes).
4. Recognize when dependencies indicate design problems
If a class has many dependencies (5+), that’s a code smell suggesting the class has too many responsibilities.
Example question: “This class depends on 8 other classes. What might that indicate?”
Strong answer: “That’s likely a violation of the Single Responsibility Principle. The class is doing too much and should be split into smaller, more focused classes. I’d look for cohesive groups of dependencies that could be extracted into separate classes.”
5. Compare dependency with other relationships
Be prepared to create a comparison table:
| Relationship | Storage | Lifetime | Coupling Strength |
|---|---|---|---|
| Dependency | No instance variable | Temporary (method scope) | Weakest |
| Association | Instance variable | Object lifetime | Moderate |
| Composition | Instance variable | Managed lifecycle | Strongest |
Practice Problem: Given a class diagram or code snippet, identify all dependencies and suggest how to reduce coupling through dependency injection or interface abstraction.
Red flags to avoid:
- Confusing dependency with association
- Not mentioning testability when discussing dependency injection
- Failing to recognize circular dependencies as problematic
- Not knowing the Dependency Inversion Principle
Key Takeaways
- Dependency is temporary usage of one class by another through method parameters, local variables, or return types—not stored as instance variables
- Dependencies create coupling that affects testability, flexibility, and maintainability; minimize dependencies and depend on abstractions, not concrete classes
- Dependency injection (passing dependencies as parameters) is superior to internal instantiation because it enables testing with mock objects and allows implementation swapping
- Distinguish dependency from association by checking for instance variable storage: if stored, it’s association; if only used within method scope, it’s dependency
- Circular dependencies are a design smell indicating poor separation of concerns; refactor to establish clear dependency direction or introduce an intermediary abstraction