- Shift Elevate
- Posts
- Circular Dependencies: Dependency Inversion Refactoring | Clean Code
Circular Dependencies: Dependency Inversion Refactoring | Clean Code
Circular Dependencies create a web of interconnected classes that make code rigid and hard to test.
We will see how we can refactor using the Dependency Inversion technique.
Clean Code Reference
⚠️ Code Smell: Circular Dependencies
✅ Refactoring: Dependency Inversion
🎯 Goal: Classes depend on abstractions, not concrete implementations
The Circular Dependencies code smell occurs when two or more classes directly depend on each other, creating a tight coupling that makes the code difficult to modify, test, and maintain. The Dependency Inversion refactoring technique introduces abstractions (interfaces) to break these circular relationships and establish a clear dependency hierarchy.
The Code Smell: Circular Dependencies
Circular Dependencies create a complex web of interconnected classes where each class directly depends on the other, making the codebase rigid and difficult to modify. This smell violates the Dependency Inversion Principle and creates tight coupling that prevents independent testing and modification of individual components.
Symptoms | Impact |
---|---|
Class A imports Class B, Class B imports Class A | Creates tight coupling between classes |
Compilation errors when trying to modify one class | Makes code changes difficult and risky |
Unit tests become complex and interdependent | Reduces testability and increases maintenance cost |
Changes in one class force changes in others | Violates single responsibility principle |
Here's a typical example of Circular Dependencies:
// UserService directly depends on OrderService
public class UserService {
private OrderService orderService;
public UserService(OrderService orderService) {
this.orderService = orderService;
}
public User createUser(String name) {
User user = new User(name);
orderService.initializeUserOrders(user);
return user;
}
public List<Order> getUserOrders(User user) {
return orderService.getOrdersByUser(user);
}
public void updateUserOrderCount(User user) {
user.setOrderCount(user.getOrderCount() + 1);
}
}
// OrderService directly depends on UserService
public class OrderService {
private UserService userService;
public OrderService(UserService userService) {
this.userService = userService; // Circular dependency!
}
public void initializeUserOrders(User user) {
Order defaultOrder = new Order(user.getId(), "Welcome Order");
userService.updateUserOrderCount(user);
}
public List<Order> getOrdersByUser(User user) {
return new ArrayList<>();
}
}
public class User {
private String id;
private String name;
private int orderCount;
public User(String name) {
this.id = UUID.randomUUID().toString();
this.name = name;
this.orderCount = 0;
}
public String getId() { return id; }
public String getName() { return name; }
public int getOrderCount() { return orderCount; }
public void setOrderCount(int orderCount) { this.orderCount = orderCount; }
}
public class Order {
private String id;
private String userId;
private String description;
public Order(String userId, String description) {
this.id = UUID.randomUUID().toString();
this.userId = userId;
this.description = description;
}
public String getId() { return id; }
public String getUserId() { return userId; }
public String getDescription() { return description; }
}
In this example, UserService and OrderService directly depend on each other through their constructors, creating a circular dependency. This makes it impossible to instantiate either service (since each requires the other to be created first) and creates tight coupling that violates clean code principles.
The Refactoring: Dependency Inversion
The Dependency Inversion refactoring breaks circular dependencies by introducing abstractions (interfaces) and ensuring that high level modules depend on abstractions rather than concrete implementations.
Step by step refactoring process:
Identify the circular dependency between classes.
Extract a focused interface that represents only the specific functionality needed by the dependent class.
Make one class implement the interface and the other depend on the interface instead of the concrete class.
Ensure single responsibility and loose coupling for each class.
Here's the refactored code:
// Step 1: Extract interface to break circular dependency
public interface UserOrderNotifier {
void updateUserOrderCount(User user);
}
// Step 2: Refactor UserService to implement the interface
public class UserService implements UserOrderNotifier {
private OrderService orderService;
public UserService() {
this.orderService = new OrderService(this);
}
public User createUser(String name) {
User user = new User(name);
orderService.initializeUserOrders(user);
return user;
}
public List<Order> getUserOrders(User user) {
return orderService.getOrdersByUser(user);
}
@Override
public void updateUserOrderCount(User user) {
user.setOrderCount(user.getOrderCount() + 1);
}
}
// Step 3: Refactor OrderService to depend on interface instead of concrete class
public class OrderService {
private final UserOrderNotifier userOrderNotifier;
public OrderService(UserOrderNotifier userOrderNotifier) {
this.userOrderNotifier = userOrderNotifier;
}
public void initializeUserOrders(User user) {
Order defaultOrder = new Order(user.getId(), "Welcome Order");
userOrderNotifier.updateUserOrderCount(user);
}
public List<Order> getOrdersByUser(User user) {
return new ArrayList<>();
}
}
// Step 4: Usage example showing how the refactored code works
public class Main {
public static void main(String[] args) {
// Create UserService (which implements UserOrderNotifier)
UserService userService = new UserService();
// Create a user - this will automatically initialize orders
User user = userService.createUser("John Doe");
// Get user orders
List<Order> orders = userService.getUserOrders(user);
System.out.println("User: " + user.getName());
System.out.println("Order Count: " + user.getOrderCount());
System.out.println("Orders: " + orders.size());
}
}
Output:
User: John Doe
Order Count: 1
Orders: 0
Benefits of Dependency Inversion Refactoring
Benefit | Description |
---|---|
Eliminates Circular Dependencies | Breaks the tight coupling between classes by introducing interfaces as abstractions |
Improves Testability | Classes can be tested independently by mocking their dependencies through interfaces |
Enhances Maintainability | Changes to one class don't require changes to dependent classes, following the Open/Closed Principle |
Promotes Loose Coupling | Classes depend on abstractions rather than concrete implementations, making the system more flexible |
Facilitates Dependency Injection | Dependencies can be injected at runtime, making the system more configurable and testable |
When to Apply Dependency Inversion Refactoring
When you encounter compilation errors due to circular imports between classes.
When classes are tightly coupled and changes in one force changes in others.
When unit testing becomes difficult because of interdependent class relationships.
When you need to implement the Dependency Inversion Principle from SOLID.
When you want to make your code more modular and maintainable.
When you're implementing dependency injection patterns.
Repository & Resources
Complete Code Examples: Clean Code Repository
Find the complete implementation of Circular Dependencies refactoring and other clean code techniques in our dedicated repository. Each example includes:
Before and after code comparisons
Unit tests demonstrating the improvements
Found this helpful? Share it with a colleague who's struggling with Circular Dependencies. Have questions about refactoring Dependency Inversion in your codebase? Email us directly, we read every message and the best questions become future newsletter topics.