19 · 32 · 5 min
Passing in what a class needs
A class that builds its own dependencies decides them permanently. A class that receives them lets the caller decide, and the caller includes your tests.
The problem
public class OrderService {
private final EmailSender email = new SmtpEmailSender("smtp.example.com");
private final Clock clock = Clock.systemUTC();
public void place(Order o) {
repository.save(o, clock.instant());
email.send(o.customerEmail(), "Order confirmed");
}
}Testing this sends a real email to a real address, and the timestamp cannot be controlled so any assertion about it is flaky. Nothing can be substituted, because the construction is inside.
The change
public class OrderService {
private final EmailSender email;
private final Clock clock;
public OrderService(EmailSender email, Clock clock) {
this.email = email;
this.clock = clock;
}
}// production
new OrderService(new SmtpEmailSender(host), Clock.systemUTC());
// test
var sent = new RecordingEmailSender();
new OrderService(sent, Clock.fixed(instant, UTC));The class is unchanged between the two. The test controls time exactly and inspects what would have been sent, with no network involved.
Why constructor injection specifically
| Form | Property |
|---|---|
| Constructor | dependencies are required and the object is never half built |
| Setter | optional, and the object can exist unusable |
| Field, by a framework | concise, and untestable without the framework |
Constructor injection is preferred because a missing dependency is a compile error rather than a null pointer exception later, and the fields can be final.
It also surfaces a design signal. A constructor with nine dependencies is unpleasant to write, and that unpleasantness is accurate: the class is doing nine things. Hiding them with field injection removes the signal without removing the problem.
Frameworks are not the point
Spring, Guice and similar tools wire the graph automatically. They are convenient and they are not what makes this work. The technique is just passing arguments, and it applies in any language with or without a container.
The general principle
This is the dependency inversion principle in concrete form: depend on an abstraction, and let something above choose the implementation. The class becomes a description of what it needs rather than a commitment to specific collaborators, and both testing and substitution follow from that.