Design Patterns Dependency Injection
Dependency Injection (DI) supplies an object with the other objects it needs from the outside. The object never builds its own helpers. Swapping, testing, and reusing parts becomes easy because the connections live in one place.
The Real-World Analogy
A desk lamp does not grow its own bulb. Someone screws in the bulb, and the lamp works with a warm bulb, a bright bulb, or an energy-saving bulb. The lamp depends on a socket standard and accepts whatever bulb arrives.
Problem Without DI
class OrderService {
private EmailSender sender = new EmailSender(); // built inside
void confirm(Order o) { sender.send(o); }
}
OrderService ───creates──▶ EmailSender
Locked to email. A test sends real emails. SMS support needs edits.
Solution With DI
┌───────────────┐
Assembler ──▶│ OrderService │
(outside) └───────┬───────┘
│ uses
▼
┌───────────────┐
│ «interface» │
│ Notifier │
└───────▲───────┘
┌─────────┴────────┐
EmailNotifier SmsNotifier FakeNotifier (tests)
The assembler picks the real notifier and hands it over. OrderService only knows the Notifier interface.
Three Ways to Inject
Constructor Injection
Dependencies arrive through the constructor. The object becomes ready to use the moment it exists. Most teams prefer this style.
Setter Injection
Dependencies arrive through setter methods after creation. The style suits optional helpers.
Interface Injection
The dependency offers an injection method that the client implements. Daily work rarely uses it.
Code Example
interface Notifier { void send(String message); }
class EmailNotifier implements Notifier {
public void send(String message) { print("Email: " + message); }
}
class SmsNotifier implements Notifier {
public void send(String message) { print("SMS: " + message); }
}
class OrderService {
private final Notifier notifier;
OrderService(Notifier notifier) { // constructor injection
this.notifier = notifier;
}
void confirm(String orderId) {
notifier.send("Order " + orderId + " confirmed");
}
}
// the assembler decides
OrderService service = new OrderService(new SmsNotifier());
service.confirm("A-101");
// a test supplies a fake
class FakeNotifier implements Notifier {
String last;
public void send(String message) { last = message; }
}DI Containers
Large applications contain hundreds of connections. A DI container reads a configuration, builds objects, and wires them automatically. Spring for Java, ASP.NET Core for C#, and Angular for TypeScript all ship with containers.
Configuration: Notifier → SmsNotifier
│
▼
┌──────────────┐ builds ┌──────────────┐
Request ───▶ │ Container │ ─────────▶ │ OrderService │
└──────────────┘ injects └──────────────┘
SmsNotifier
Dependency Injection Versus Inversion
Dependency Inversion is the principle: depend on abstractions. Injection, the technique, supplies those abstractions at runtime. Developers use the technique to honor the principle.
Benefits in Daily Work
| Situation | Gain From DI |
|---|---|
| Unit tests | Fake helpers replace slow or risky ones |
| Feature switches | One line selects a new implementation |
| Team projects | Developers work against interfaces in parallel |
| Configuration | Development and production use different parts |
Advantages and Disadvantages
| Advantages | Disadvantages |
|---|---|
| Low coupling | More setup code |
| Easy testing | Containers hide object creation |
| Flexible configuration | Constructors with many parameters signal design trouble |
Step-by-Step Walkthrough
Follow the order example. The assembler creates an SmsNotifier and passes it into the OrderService constructor. OrderService stores the reference in a final field. Calling confirm() sends the message through whichever notifier arrived.
A test builds the service with a FakeNotifier and checks the last message after the call. No real message leaves the computer, and the test finishes in milliseconds.
Where to Assemble Objects
Wiring belongs near the entry point of the application, often called the composition root. Every other class receives its helpers and never builds them. A single place for wiring makes changes simple to find.
Key Takeaways
- DI hands dependencies to an object instead of letting it create them.
- Constructor injection suits required parts, and setter injection suits optional parts.
- Containers automate wiring in large systems.
- DI delivers the Dependency Inversion principle in practice.
