Course content
Dependency Inversion: Inject the OrderRepository
Medium·Tagssoliddependency-inversiondependency-injectionrefactoring
Problem Statement
The starter has an `OrderService` that internally constructs a `MySqlOrderRepository`:
```java
class OrderService {
private final MySqlOrderRepository repo;
public OrderService() {
this.repo = new MySqlOrderRepository();
}
public void placeOrder(String orderId, double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Order amount must be positive");
}
repo.save(orderId, amount);
}
}
```
This violates the Dependency Inversion Principle. The high-level `OrderService` (business logic) directly depends on the low-level `MySqlOrderRepository` (data access). The dependency is hardcoded and the field is typed against the concrete class, so swapping the repository for a different implementation, or for a fake in tests, requires editing `OrderService`.
Refactor the design so that `OrderService` depends on an abstraction and receives its repository through its constructor. The starter already declares an empty `OrderRepository` interface for you to populate. After your refactor, the design must expose this exact public API:
```java
interface OrderRepository {
void save(String orderId, double amount);
}
class MySqlOrderRepository implements OrderRepository {
public void save(String orderId, double amount) { ... }
}
class OrderService {
public OrderService(OrderRepository repo);
// No no-arg constructor. The repository must be injected.
public void placeOrder(String orderId, double amount);
// Validates amount, then delegates to repo.save.
}
```
The validator runs five checks:
1. `OrderService` actually uses the repository it was given. The validator constructs its own `OrderRepository` implementation, passes it into `OrderService`, calls `placeOrder`, and asserts the validator's repository received the save call with the right arguments. If `OrderService` is still constructing its own `MySqlOrderRepository` internally, the injected repository never sees the call and this check fails.
2. `OrderService` has a constructor that accepts an `OrderRepository`. Reflection check.
3. `OrderService` does not have a no-argument constructor. Reflection check. This forces constructor injection: callers must hand in a repository, they cannot rely on `OrderService` to allocate one.
4. `MySqlOrderRepository` implements the `OrderRepository` interface. Reflection check.
5. The `OrderRepository` interface declares `save(String, double)`. Reflection check, mostly a smoke test for the interface itself.
The smelly starter passes none of these out of the box. Each requires the actual refactor.
Starter code and validators are available in Java, Python, and C++.
Examples
Example 1
Input
OrderService svc = new OrderService(new MySqlOrderRepository()); svc.placeOrder("order-1", 250.0)Output
"(saves order-1 / 250.0 to MySQL)"Why
OrderService receives its repository through the constructor. In production, callers wire in the MySQL implementation.
Example 2
Input
OrderService svc = new OrderService(new InMemoryOrderRepository()); svc.placeOrder("order-2", 50.0)Output
"(saves to whatever in-memory store the test harness passed in)"Why
Tests can substitute any OrderRepository implementation. The high-level OrderService does not know or care which one.
Example 3
Input
new OrderService()Output
"compile error after the refactor"Why
The no-arg constructor is gone. The compiler now insists that the dependency be provided, which is exactly what dependency inversion is for.
Constraints
- •Refactor must keep the three declarations named: OrderRepository, MySqlOrderRepository, OrderService.
- •OrderRepository must be an interface declaring void save(String orderId, double amount).
- •MySqlOrderRepository must implement OrderRepository.
- •OrderService must have a constructor that accepts OrderRepository and store it in a field.
- •OrderService must not have a no-argument constructor.
- •OrderService.placeOrder must throw IllegalArgumentException for non-positive amounts and otherwise delegate to repo.save.
Hints
Stuck? Reveal a nudge toward the right pattern, one step at a time.
Hint 1
Start by adding the save method to the OrderRepository interface: `void save(String orderId, double amount);`. Without it, neither MySqlOrderRepository nor the validator's recording repository can implement the interface.
Hint 2
Make MySqlOrderRepository implement OrderRepository. Add `implements OrderRepository` to its declaration and an `@Override` annotation on save.
Hint 3
Replace the no-arg OrderService constructor with one that takes an OrderRepository: `public OrderService(OrderRepository repo) { this.repo = repo; }`. Delete the no-arg version.
Hint 4
Change the type of the OrderService field from MySqlOrderRepository to OrderRepository. The point is to depend on the abstraction.
Hint 5
If you want OrderService to retain a fallback default for production, do it from the call site, not from inside OrderService. The class itself should never allocate its own repository.